diff --git a/.github/gmagick.sh b/.github/gmagick.sh new file mode 100755 index 0000000000000..afd80473823be --- /dev/null +++ b/.github/gmagick.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -xe + +if [ -z "$1" ] +then + echo "You must provide the PHP version as first argument" + exit 1 +fi + +if [ -z "$2" ] +then + echo "You must provide the php.ini path as second argument" + exit 1 +fi + +PHP_VERSION=$1 +PHP_INI_FILE=$2 + +GRAPHICSMAGIC_VERSION="1.3.23" +if [ $PHP_VERSION = '7.0' ] || [ $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=`pwd`/modules/gmagick.so" >> $PHP_INI_FILE +php --ri gmagick diff --git a/.github/imagick.sh b/.github/imagick.sh new file mode 100755 index 0000000000000..c532cc31773f2 --- /dev/null +++ b/.github/imagick.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -xe + +if [ -z "$1" ] +then + echo "You must provide the php.ini path as first argument" + exit 1 +fi + +PHP_INI_FILE=$1 + +IMAGEMAGICK_VERSION="6.8.9-10" +IMAGICK_VERSION="3.4.3" + +mkdir -p cache +cd cache + +if [ ! -e ./ImageMagick-$IMAGEMAGICK_VERSION ] +then + wget https://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=`pwd`/modules/imagick.so" >> $PHP_INI_FILE +php --ri imagick diff --git a/.travis.yml b/.travis.yml index a527f770d51e2..651a16cab1f34 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: @@ -26,16 +32,18 @@ matrix: group: edge - php: 5.5 - php: 5.6 + env: IMAGE_DRIVER=gmagick - php: 7.0 env: deps=high - php: 7.1 - env: deps=low + env: deps=low IMAGE_DRIVER=imagick fast_finish: true cache: directories: - .phpunit - php-$MIN_PHP + - cache services: - memcached @@ -90,6 +98,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 ./.github/imagick.sh $INI_FILE; fi + - if [[ $IMAGE_DRIVER = gmagick ]]; then ./.github/gmagick.sh $TRAVIS_PHP_VERSION $INI_FILE; fi script: - REPORT=' && echo -e "\\e[32mOK\\e[0m {}\\n\\n" || (echo -e "\\e[41mKO\\e[0m {}\\n\\n" && $(exit 1))' diff --git a/appveyor.yml b/appveyor.yml index d5c663d5a041e..d9f8ddfb9bce4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -39,6 +39,8 @@ install: - echo apc.enable_cli=1 >> php.ini-max - echo extension=php_memcache.dll >> php.ini-max - echo extension=php_intl.dll >> php.ini-max + - echo extension=php_exif.dll >> php.ini-max + - echo extension=php_gd2.dll >> php.ini-max - echo extension=php_mbstring.dll >> php.ini-max - echo extension=php_fileinfo.dll >> php.ini-max - echo extension=php_pdo_sqlite.dll >> php.ini-max diff --git a/composer.json b/composer.json index e1287757a44e7..6dab81a1d6920 100644 --- a/composer.json +++ b/composer.json @@ -92,6 +92,7 @@ "ocramius/proxy-manager": "~0.4|~1.0|~2.0", "predis/predis": "~1.0", "egulias/email-validator": "~1.2,>=1.2.8|~2.0", + "symfony/image-fixtures": "dev-master@dev", "symfony/phpunit-bridge": "~3.2", "symfony/polyfill-apcu": "~1.1", "symfony/security-acl": "~2.8|~3.0", diff --git a/src/Symfony/Component/Image/.gitignore b/src/Symfony/Component/Image/.gitignore new file mode 100644 index 0000000000000..5414c2c655e72 --- /dev/null +++ b/src/Symfony/Component/Image/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/Symfony/Component/Image/CHANGELOG.md b/src/Symfony/Component/Image/CHANGELOG.md new file mode 100644 index 0000000000000..8a39cfe439e0f --- /dev/null +++ b/src/Symfony/Component/Image/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +3.3.0 +----- + + * [EXPERIMENTAL] added the component 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..113f9ce55e91b --- /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 $loader; + + /** + * Constructs Canvas filter with given width and height and the placement of the current image + * inside the new canvas. + * + * @param LoaderInterface $loader + * @param BoxInterface $size + * @param PointInterface $placement + * @param ColorInterface $background + */ + public function __construct(LoaderInterface $loader, BoxInterface $size, PointInterface $placement = null, ColorInterface $background = null) + { + $this->loader = $loader; + $this->size = $size; + $this->placement = $placement ?: new Point(0, 0); + $this->background = $background; + } + + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image) + { + $canvas = $this->loader->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..5234abe4f57ef --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Basic/Autorotate.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\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. + */ +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..08beab5f004c1 --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php @@ -0,0 +1,66 @@ + + * + * 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); + + foreach (array('resolution-x', 'resolution-y', 'resolution-units') as $option) { + if (isset($this->options[$option])) { + @trigger_error(sprintf('"%s" as been deprecated in Symfony 3.3 in favor of "%"', $option, str_replace('-', '_', $option)), E_USER_DEPRECATED); + $this->options[str_replace('-', '_', $option)] = $this->options[$option]; + unset($this->options[$option]); + } + } + + $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..c09e623941674 --- /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 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..a92161b48f0b5 --- /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 $loader; + + /** + * Set LoaderInterface instance. + * + * @param LoaderInterface $loader An LoaderInterface instance + */ + public function setLoader(LoaderInterface $loader) + { + $this->loader = $loader; + } + + /** + * Get LoaderInterface instance. + * + * @return LoaderInterface + * + * @throws InvalidArgumentException + */ + public function getLoader() + { + if (!$this->loader 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->loader; + } +} diff --git a/src/Symfony/Component/Image/Filter/Transformation.php b/src/Symfony/Component/Image/Filter/Transformation.php new file mode 100644 index 0000000000000..2bad5343996e0 --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Transformation.php @@ -0,0 +1,242 @@ + + * + * 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 $loader; + + /** + * Class constructor. + * + * @param LoaderInterface $loader An LoaderInterface instance + */ + public function __construct(LoaderInterface $loader = null) + { + $this->loader = $loader; + } + + /** + * 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->loader === 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->loader); + } + + 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..09d62a16a2eba --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Drawer.php @@ -0,0 +1,329 @@ + + * + * 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; + } + + /** + * 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(); + } + + /** + * 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..e3de2d4d56ddf --- /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..5c0465182d390 --- /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..e01e3cf47214a --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Image.php @@ -0,0 +1,706 @@ + + * + * 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 self($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; + } + + /** + * 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); + + $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']; + } + + 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)); + }); + + try { + if (false === call_user_func_array($save, $args)) { + throw new RuntimeException('Save operation failed'); + } + } finally { + restore_error_handler(); + } + } + + /** + * 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; + } + + /** + * Generates a GD color from Color instance. + * + * @param ColorInterface $color + * + * @return int 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; + } + + /** + * 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; + } + + /** + * Checks whether a given format is supported by GD library. + * + * @param string $format + * + * @return bool + */ + private function supported($format = null) + { + $formats = array('gif', 'jpeg', 'png', 'wbmp', 'xbm'); + + if (null === $format) { + return $formats; + } + + return in_array($format, $formats); + } + + /** + * 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..ed3d5f1a31fdf --- /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..c67f8eceeebaa --- /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..d14a01ccace18 --- /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..6709403f08fe9 --- /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 int $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..86872a49afb84 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Image.php @@ -0,0 +1,784 @@ + + * + * 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; + } + + /** + * 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(self::$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 = (bool) $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; + } + + /** + * 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); + } + + /** + * 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(self::$colorspaceMapping[$palette->name()])) { + throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver', $palette->name())); + } + + $this->gmagick->setimagecolorspace(self::$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..3f513d12b6b02 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Layers.php @@ -0,0 +1,274 @@ + + * + * 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 int + */ + 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 int $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..a415a65580b5c --- /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..1609d79adcda0 --- /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 int + */ + 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 int $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..cc86b0b22ef1e --- /dev/null +++ b/src/Symfony/Component/Image/Image/AbstractImage.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\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) + { + if (isset($options['quality'])) { + @trigger_error('Using the "quality" option is deprecated in Symfony 3.3. Use the "jpeg_quality" or "png_compression_level" instead.', E_USER_DEPRECATED); + } + + if (isset($options['filters'])) { + @trigger_error('Using the "filters" option is deprecated in Symfony 3.3. Use the "png_compression_filter" instead.', E_USER_DEPRECATED); + } + + foreach (array('resolution-x', 'resolution-y', 'resolution-units', 'resampling-filter') as $option) { + if (isset($options[$option])) { + @trigger_error(sprintf('"%s" as been deprecated in Symfony 3.3 in favor of "%"', $option, str_replace('-', '_', $option)), E_USER_DEPRECATED); + $options[str_replace('-', '_', $option)] = $options[$option]; + unset($options[$option]); + } + } + + if (isset($options['quality']) && !isset($options['jpeg_quality'])) { + $options['jpeg_quality'] = $options['quality']; + } + + 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']; + } + + 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..f1b7cc7f7e814 --- /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..d0b427846f947 --- /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 int + */ + private $width; + + /** + * @var int + */ + private $height; + + /** + * Constructs the Size with given width and height. + * + * @param int $width + * @param int $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 self(round($ratio * $this->width), round($ratio * $this->height)); + } + + /** + * {@inheritdoc} + */ + public function increase($size) + { + return new self((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..de6a6f3692732 --- /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 int + */ + public function getHeight(); + + /** + * Gets current image width. + * + * @return int + */ + 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 int $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 bool + */ + public function contains(BoxInterface $box, PointInterface $start = null); + + /** + * Gets current box square, useful for getting total number of pixels in a + * given box. + * + * @return int + */ + 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 int $width + * + * @return BoxInterface + */ + public function widen($width); + + /** + * Resizes box to given height, constraining proportions and returns the new box. + * + * @param int $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..822adf2cffdab --- /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..9d40d76b4a3a0 --- /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..14da307afc111 --- /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 int + */ + 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 int $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 int + */ + 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..a537ee5e43baf --- /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..6caf053eda8f2 --- /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 int + */ + 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 int $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..c030b65214eba --- /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 int + */ + private $count; + + /** + * @param Range $range + * @param int $count + */ + public function __construct(Range $range, $count = 0) + { + $this->range = $range; + $this->count = $count; + } + + /** + * @param int $value + */ + public function add($value) + { + if ($this->range->contains($value)) { + ++$this->count; + } + } + + /** + * @return int 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..0a48b9a259441 --- /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 int + */ + private $start; + + /** + * @var int + */ + private $end; + + /** + * @param int $start + * @param int $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 int $value + * + * @return bool + */ + 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..95f2d1de4130d --- /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..f47915f18ee52 --- /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 int $delay The delay in milliseconds between two frames + * @param int $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 int $offset + * @param ImageInterface $image + * + * @return LayersInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + * @throws OutOfBoundsException + */ + public function set($offset, ImageInterface $image); + + /** + * Removes the image at offset. + * + * @param int $offset + * + * @return LayersInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function remove($offset); + + /** + * Returns the image at offset. + * + * @param int $offset + * + * @return ImageInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function get($offset); + + /** + * Returns true if a layer at offset is preset. + * + * @param int $offset + * + * @return bool + */ + 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..39c0d5125a896 --- /dev/null +++ b/src/Symfony/Component/Image/Image/LoaderInterface.php @@ -0,0 +1,81 @@ + + * + * 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 loader interface. + */ +interface LoaderInterface +{ + /** + * 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 int $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..af087e3348114 --- /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 int $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..7a4916fcbf57a --- /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..c9d2ae7b5361e --- /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..77ac4dff13e16 --- /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..e18ce99da59ae --- /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..57ab344dc0ea3 --- /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..fc015571745d7 --- /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..6b2e941ce07d7 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php @@ -0,0 +1,218 @@ + + * + * 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 int + */ + private $c; + + /** + * @var int + */ + private $m; + + /** + * @var int + */ + private $y; + + /** + * @var int + */ + 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 int + */ + public function getCyan() + { + return $this->c; + } + + /** + * Returns Magenta value of the color. + * + * @return int + */ + public function getMagenta() + { + return $this->m; + } + + /** + * Returns Yellow value of the color. + * + * @return int + */ + public function getYellow() + { + return $this->y; + } + + /** + * Returns Key value of the color. + * + * @return int + */ + 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); + } + + /** + * 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..427b19e375ac5 --- /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 int + */ + public function getValue($component); + + /** + * Returns percentage of transparency of the color. + * + * @return int + */ + 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 int $alpha + * + * @return ColorInterface + */ + public function dissolve($alpha); + + /** + * Returns a copy of the current color, lightened by the specified number + * of shades. + * + * @param int $shade + * + * @return ColorInterface + */ + public function lighten($shade); + + /** + * Returns a copy of the current color, darkened by the specified number of + * shades. + * + * @param int $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 bool + */ + 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..fc57519f2f598 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php @@ -0,0 +1,163 @@ + + * + * 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 int + */ + private $gray; + + /** + * @var int + */ + 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 int + */ + 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 int $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..fe7e2f30e911d --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php @@ -0,0 +1,215 @@ + + * + * 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 int + */ + private $r; + + /** + * @var int + */ + private $g; + + /** + * @var int + */ + private $b; + + /** + * @var int + */ + 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 int + */ + public function getRed() + { + return $this->r; + } + + /** + * Returns GREEN value of the color. + * + * @return int + */ + public function getGreen() + { + return $this->g; + } + + /** + * Returns BLUE value of the color. + * + * @return int + */ + 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); + } + + /** + * Performs checks for validity of given alpha value and sets it. + * + * @param int $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(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'); + } + + foreach ($color as $c) { + if ($c > 255 || $c < 0) { + 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..73136b61a576f --- /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|int $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|int $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|int $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|int $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..275ada8060f14 --- /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..f4cdceba85569 --- /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|int $color A color + * @param int|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 bool + */ + 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..80ce34248e9eb --- /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..609c80ae52922 --- /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 int + */ + private $x; + + /** + * @var int + */ + private $y; + + /** + * Constructs a point of coordinates. + * + * @param int $x + * @param int $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 self($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..63a01a9a3d9be --- /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..19b50a015ee14 --- /dev/null +++ b/src/Symfony/Component/Image/Image/PointInterface.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\Image; + +/** + * The point interface. + */ +interface PointInterface +{ + /** + * Gets points x coordinate. + * + * @return int + */ + public function getX(); + + /** + * Gets points y coordinate. + * + * @return int + */ + public function getY(); + + /** + * Checks if current coordinate is inside a given box. + * + * @param BoxInterface $box + * + * @return bool + */ + public function in(BoxInterface $box); + + /** + * Returns another point, moved by a given amount from current coordinates. + * + * @param int $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..6a0ad0c033d31 --- /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..5aeff94ec2b90 --- /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..03d450983a030 --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Drawer.php @@ -0,0 +1,402 @@ + + * + * 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; + } + + /** + * 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..11fa3ef7861e8 --- /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..4114505f8573c --- /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 int $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..32f5758f37022 --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Image.php @@ -0,0 +1,898 @@ + + * + * 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 bool + */ + 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 (self::$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(self::$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 (!self::$supportsColorspaceConversion) { + throw new RuntimeException('Your version of Imagick does not support colorspace conversions.'); + } + + try { + try { + $hasICCProfile = (bool) $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; + } + + /** + * 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); + } + } + + /** + * 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 bool + */ + 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(self::$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(self::$colorspaceMapping[$palette->name()]); + $this->palette = $palette; + } + + /** + * Older imagemagick versions does not support colorspace conversions. + * Let's detect if it is supported. + * + * @return bool + */ + private function detectColorspaceConversionSupport() + { + if (null !== self::$supportsColorspaceConversion) { + return self::$supportsColorspaceConversion; + } + + return self::$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..3e5af706a880c --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/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\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 int + */ + 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 int $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..378b54bccd1b5 --- /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..6671e15ee48fb --- /dev/null +++ b/src/Symfony/Component/Image/README.md @@ -0,0 +1,11 @@ +Symfony Image Component +======================= + +Resources +--------- + + * [Documentation](https://symfony.com/doc/master/components/image.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) 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/LICENSE b/src/Symfony/Component/Image/Resources/Adobe/LICENSE new file mode 100644 index 0000000000000..fbe7c40e6b491 --- /dev/null +++ b/src/Symfony/Component/Image/Resources/Adobe/LICENSE @@ -0,0 +1 @@ +https://www.adobe.com/support/downloads/iccprofiles/icc_eula_mac_dist.html 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/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..f3de42290d0ef --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php @@ -0,0 +1,177 @@ + + * + * 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 PHPUnit\Util\InvalidArgumentHelper; +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 + { + } +} + +if (class_exists(\PHPUnit_Util_InvalidArgumentHelper::class)) { + abstract class PHPUnitInvalidArgumentHelper extends \PHPUnit_Util_InvalidArgumentHelper + { + } +} else { + abstract class PHPUnitInvalidArgumentHelper extends InvalidArgumentHelper + { + } +} + +class IsImageEqual extends PHPUnitConstraint +{ + /** + * @var \Symfony\Component\Image\Image\ImageInterface + */ + private $value; + + /** + * @var float + */ + private $delta; + + /** + * @var int + */ + private $buckets; + + /** + * @param \Symfony\Component\Image\Image\ImageInterface $value + * @param float $delta + * @param int $buckets + * + * @throws InvalidArgumentException + */ + public function __construct($value, $delta = 0.1, $buckets = 4) + { + if (!$value instanceof ImageInterface) { + throw PHPUnitInvalidArgumentHelper::factory(1, ImageInterface::class); + } + + if (!is_numeric($delta)) { + throw PHPUnitInvalidArgumentHelper::factory(2, 'numeric'); + } + + if (!is_integer($buckets) || $buckets <= 0) { + throw PHPUnitInvalidArgumentHelper::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 PHPUnitInvalidArgumentHelper::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..ec0daa883b7b9 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php @@ -0,0 +1,229 @@ + + * + * 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\Fixtures\Loader; +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() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/smiley.png'); + + $this->assertFileExists($this->getTempDir().'/smiley.png'); + } + + public function testDrawAnEllipse() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/ellipse.png'); + + $this->assertFileExists($this->getTempDir().'/ellipse.png'); + } + + public function testDrawAPieSlice() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/pie.png'); + + $this->assertFileExists($this->getTempDir().'/pie.png'); + } + + public function testDrawAChord() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/chord.png'); + + $this->assertFileExists($this->getTempDir().'/chord.png'); + } + + public function testDrawALine() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/lines.png'); + + $this->assertFileExists($this->getTempDir().'/lines.png'); + } + + public function testDrawAPolygon() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/polygon.png'); + + $this->assertFileExists($this->getTempDir().'/polygon.png'); + } + + public function testDrawADot() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/dot.png'); + + $this->assertFileExists($this->getTempDir().'/dot.png'); + } + + public function testDrawAnArc() + { + $loader = $this->getLoader(); + + $canvas = $loader->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($this->getTempDir().'/arc.png'); + + $this->assertFileExists($this->getTempDir().'/arc.png'); + } + + public function testDrawText() + { + if (!$this->isFontTestSupported()) { + $this->markTestSkipped('This install does not support font tests'); + } + + $path = Loader::getFixture('font/Arial.ttf'); + $black = $this->getColor('000'); + $file36 = $this->getTempDir().'/bulat36.png'; + $file24 = $this->getTempDir().'/bulat24.png'; + $file18 = $this->getTempDir().'/bulat18.png'; + $file12 = $this->getTempDir().'/bulat12.png'; + + $loader = $this->getLoader(); + $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); + $font = $loader->font($path, 36, $black); + + $canvas->draw() + ->text('Bulat', $font, new Point(0, 0), 135); + + $canvas->save($file36); + + unset($canvas); + + $this->assertFileExists($file36); + + $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); + $font = $loader->font($path, 24, $black); + + $canvas->draw() + ->text('Bulat', $font, new Point(24, 24)); + + $canvas->save($file24); + + unset($canvas); + + $this->assertFileExists($file24); + + $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); + $font = $loader->font($path, 18, $black); + + $canvas->draw() + ->text('Bulat', $font, new Point(18, 18)); + + $canvas->save($file18); + + unset($canvas); + + $this->assertFileExists($file18); + + $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); + $font = $loader->font($path, 12, $black); + + $canvas->draw() + ->text('Bulat', $font, new Point(12, 12)); + + $canvas->save($file12); + + unset($canvas); + + $this->assertFileExists($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..b7b08e5e68022 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php @@ -0,0 +1,140 @@ + + * + * 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(); + $loader = $this->getLoader(); + + $image = $loader->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(); + $loader = $this->getLoader(); + + $r = 20; + $g = 90; + $b = 240; + + $image = $loader->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(); + $loader = $this->getLoader(); + + $r = 20; + $g = 90; + $b = 240; + + $image = $loader->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(); + $loader = $this->getLoader(); + + $blue = $palette->color('#0000FF'); + + $image = $loader->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(); + $loader = $this->getLoader(); + + $image = $loader->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..5727db3e22961 --- /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..493d4493e3dba --- /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); + + $loader = $this->getLoader(); + $loader->expects($this->once())->method('create')->with($size, $background)->will($this->returnValue($canvas)); + + $command = new Canvas($loader, $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..21877a322e154 --- /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..56a12ab593180 --- /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..ed7709ca03530 --- /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..331b57e49ad90 --- /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..501bd19ad77ab --- /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..0abcc069aad49 --- /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..75a39e8a55128 --- /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..5ea21e1471db1 --- /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..fb2ace3eeef45 --- /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..b70d162cb0c7b --- /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..5e95374e8b5ae --- /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..688de7d1fe420 --- /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..d0acc2dee177f --- /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..585594dd5354e --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php @@ -0,0 +1,117 @@ + + * + * 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..d415c2bd8c35a --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php @@ -0,0 +1,25 @@ +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..56cbc4db47548 --- /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() + { + $loaderMock = $this->getMockBuilder(LoaderInterface::class)->getMock(); + $loaderMock->expects($this->once()) + ->method('create') + ->will($this->returnValue($this->getImage())); + + return $loaderMock; + } +} 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..5dce68e8dc2c5 --- /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/Functional/GdTransparentGifHandlingTest.php b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php new file mode 100644 index 0000000000000..71ed4d0cd7865 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.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\Functional; + +use Symfony\Component\Image\Fixtures\Loader as FixturesLoader; +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 { + $loader = new Loader(); + } catch (RuntimeException $e) { + $this->markTestSkipped($e->getMessage()); + } + + return $loader; + } + + public function testShouldResize() + { + $loader = $this->getLoader(); + $new = sys_get_temp_dir().'/sample.jpeg'; + + $image = $loader->open(FixturesLoader::getFixture('xparent.gif')); + $size = $image->getSize()->scale(0.5); + + $image + ->resize($size) + ; + + $image = $loader + ->create($size) + ->paste($image, new Point(0, 0)) + ->save($new) + ; + + $this->assertSame(272, $image->getSize()->getWidth()); + $this->assertSame(171, $image->getSize()->getHeight()); + } +} 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..b4a2fc895577e --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Gd/LayersTest.php @@ -0,0 +1,128 @@ + + * + * 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\Fixtures\Loader as FixturesLoader; +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(FixturesLoader::getFixture('pink.gif')); + $layers = $image->layers(); + + $this->assertLayersEquals($image, $layers[0]); + $this->assertTrue(isset($layers[0])); + } + + public function testLayerAddGetSetRemove() + { + $image = $this->getImage(FixturesLoader::getFixture('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..595faf99113f3 --- /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(112, 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..49a5cb16ab17f --- /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\Fixtures\Loader as FixturesLoader; +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(FixturesLoader::getFixture('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..b70a14d1bba34 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php @@ -0,0 +1,798 @@ + + * + * 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\Fixtures\Loader as FixturesLoader; +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\Imagick\Loader as ImagickLoader; +use Symfony\Component\Image\Gmagick\Loader as GmagickLoader; + +abstract class AbstractImageTest extends TestCase +{ + public function testPaletteIsRGBIfRGBImage() + { + $image = $this->getLoader()->open(FixturesLoader::getFixture('google.png')); + $this->assertInstanceOf(RGB::class, $image->palette()); + } + + public function testPaletteIsCMYKIfCMYKImage() + { + $image = $this->getLoader()->open(FixturesLoader::getFixture('pixel-CMYK.jpg')); + $this->assertInstanceOf(CMYK::class, $image->palette()); + } + + public function testPaletteIsGrayIfGrayImage() + { + $image = $this->getLoader()->open(FixturesLoader::getFixture('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($this->getTempDir().'/tmp.jpg'); + + $image = $this->getLoader()->open($this->getTempDir().'/tmp.jpg'); + + $this->assertInstanceOf($to, $image->palette()); + } + + public function testSaveWithoutFormatShouldSaveInOriginalFormat() + { + if (!extension_loaded('exif')) { + $this->markTestSkipped('The EXIF extension is required for this test'); + } + + $tmpFile = $this->getTempDir().'/tmpfile'; + + $this + ->getLoader() + ->open(FixturesLoader::getFixture('large.jpg')) + ->save($tmpFile); + + $data = exif_read_data($tmpFile); + $this->assertEquals('image/jpeg', $data['MimeType']); + } + + public function testSaveWithoutPathFileFromImageLoadShouldBeOkay() + { + $source = FixturesLoader::getFixture('google.png'); + $tmpFile = $this->getTempDir().'/google.tmp.png'; + + 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)); + } + + public function testSaveWithoutPathFileFromImageCreationShouldFail() + { + $image = $this->getLoader()->create(new Box(20, 20)); + $this->setExpectedException(RuntimeException::class); + $image->save(); + } + + public function provideFromAndToPalettes() + { + $palettes = 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), + ), + ); + + if (!defined('HHVM_VERSION')) { + $palettes[] = array( + Grayscale::class, + RGB::class, + array(10), + ); + $palettes[] = array( + Grayscale::class, + CMYK::class, + array(10), + ); + } + + return $palettes; + } + + public function testProfile() + { + $image = $this + ->getLoader() + ->create(new Box(10, 10)) + ->profile(Profile::fromPath(FixturesLoader::getFixture('ICCProfiles/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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('google.png')); + $thumbnail = $image->thumbnail(new Box(20, 20)); + + $this->assertNotSame($image, $thumbnail); + } + + public function testThumbnailWithInvalidModeShouldThrowAnException() + { + $factory = $this->getLoader(); + $image = $factory->open(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('google.png')); + + $image->applyMask($image->mask()) + ->save($this->getTempDir().'/mask.png'); + + $size = $factory->open($this->getTempDir().'/mask.png') + ->getSize(); + + $this->assertEquals(364, $size->getWidth()); + $this->assertEquals(126, $size->getHeight()); + } + + public function testColorHistogram() + { + $factory = $this->getLoader(); + + $image = $factory->open(FixturesLoader::getFixture('google.png')); + + $this->assertEquals(6438, count($image->histogram())); + } + + public function testImageResolutionChange() + { + $loader = $this->getLoader(); + $image = $loader->open(FixturesLoader::getFixture('resize/210-design-19933.jpg')); + $outfile = $this->getTempDir().'/reduced.jpg'; + $image->save($outfile, array( + 'resolution_units' => ImageInterface::RESOLUTION_PIXELSPERINCH, + 'resolution_x' => 144, + 'resolution_y' => 144, + )); + + if ($loader instanceof ImagickLoader) { + $i = new \Imagick($outfile); + $info = $i->identifyimage(); + $this->assertEquals(144, $info['resolution']['x']); + $this->assertEquals(144, $info['resolution']['y']); + } + if ($loader instanceof GmagickLoader) { + $i = new \Gmagick($outfile); + $info = $i->getimageresolution(); + $this->assertEquals(144, $info['x']); + $this->assertEquals(144, $info['y']); + } + } + + 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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pixel-CMYK.jpg')) + ->usePalette(new RGB()) + ->strip() + ->getColorAt(new Point(0, 0)); + + $this->assertEquals('#0082a2', (string) $color); + } + + public function testStripImageWithInvalidProfile() + { + $image = $this + ->getLoader() + ->open(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pixel-grayscale.jpg')) + ->getColorAt(new Point(0, 0)); + + $this->assertEquals('#4d4d4d', (string) $color); + $this->assertTrue($color->isOpaque()); + } + + public function testGetColorAtCMYK() + { + $color = $this + ->getLoader() + ->open(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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(FixturesLoader::getFixture('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'); + } + + $loader = $this->getLoader(); + + $image = $loader->open(FixturesLoader::getFixture('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($this->getTempDir().'/anima-half-size.gif', array('animated' => true)); + + $image = $loader->open(FixturesLoader::getFixture('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 = $this->getTempDir().'/anima2-half-size.gif'; + $image->save($target, array('animated' => true)); + + $this->assertFileExists($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() + { + $loader = $this->getLoader(); + + $image = $loader->open(FixturesLoader::getFixture('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)); + } + + public function provideVariousSources() + { + return array( + array(FixturesLoader::getFixture('example.svg')), + array(FixturesLoader::getFixture('100-percent-black.png')), + ); + } + + public function testFillAlphaPrecision() + { + $loader = $this->getLoader(); + $palette = new RGB(); + $image = $loader->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(FixturesLoader::getFixture('google.png')); + } + + private function getMultiLayeredImage() + { + return $this->getLoader()->open(FixturesLoader::getFixture('cat.gif')); + } + + protected function processInOut($file, $in, $out) + { + $factory = $this->getLoader(); + $class = preg_replace('/\\\\/', '_', get_called_class()); + $image = $factory->open(FixturesLoader::getFixture($file.'.'.$in)); + $thumb = $image->thumbnail(new Box(50, 50), ImageInterface::THUMBNAIL_OUTBOUND); + $target = $this->getTempDir()."/{$class}_{$file}_from_{$in}_to.{$out}"; + $thumb->save($target); + + $this->assertFileExists($target); + } + + /** + * @return \Symfony\Component\Image\Image\LoaderInterface + */ + abstract protected function getLoader(); + + /** + * @return bool + */ + 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..225c2180ac460 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php @@ -0,0 +1,278 @@ + + * + * 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\Fixtures\Loader as FixturesLoader; +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(FixturesLoader::getFixture('pink.gif')); + $secondImage = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $thirdImage = $this->getImage(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pink.gif')); + $secondImage = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $thirdImage = $this->getImage(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pink.gif')); + $secondImage = $this->getImage(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pink.gif')); + $secondImage = $this->getImage(FixturesLoader::getFixture('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(FixturesLoader::getFixture('pink.gif')); + $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); + + $target = $this->getTempDir().'/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + )); + + $this->assertFileExists($target); + } + + /** + * @dataProvider provideAnimationParameters + */ + public function testAnimateWithParameters($delay, $loops) + { + $image = $this->getImage(FixturesLoader::getFixture('pink.gif')); + $layers = $image->layers(); + + $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); + + $target = $this->getTempDir().'/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + 'animated.delay' => $delay, + 'animated.loops' => $loops, + )); + + $this->assertFileExists($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(FixturesLoader::getFixture('pink.gif')); + $layers = $image->layers(); + + $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); + + $target = $this->getTempDir().'/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + 'animated.delay' => $delay, + 'animated.loops' => $loops, + )); + } + + 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..45f90a4b8d43c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.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\Fixtures\Loader as FixturesLoader; +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 = FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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(240, $size->getWidth()); + $this->assertEquals(60, $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(FixturesLoader::getFixture('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 testShouldCreateImageFromResource() + { + $source = FixturesLoader::getFixture('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(240, $size->getWidth()); + $this->assertEquals(60, $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 = FixturesLoader::getFixture('font/Arial.ttf'); + $black = $palette->color('000'); + $factory = $this->getLoader(); + + $this->assertEquals($this->getEstimatedFontBox(), $factory->font($path, 36, $black)->box('string')); + } + + public function testCreateAlphaPrecision() + { + $loader = $this->getLoader(); + $palette = new RGB(); + $image = $loader->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..996e4a6d97c8d --- /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 int $width + * @param int $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 int $width + * @param int $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 bool $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 int $width + * @param int $height + * @param int $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 int $width + * @param int $height + * @param int $targetWidth + * @param int $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..cdec4b8a40789 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.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\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..b1ec5b527647d --- /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 int $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..6b79c3672dedf --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.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\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..d810c5814bc89 --- /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 int $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..20a680d49d0d8 --- /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 bool $contains + * @param int $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..7691f17cd1d7c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.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\Tests\Image\Metadata; + +use Symfony\Component\Image\Fixtures\Loader as FixturesLoader; +use Symfony\Component\Image\Image\Metadata\ExifMetadataReader; + +class ExifMetadataReaderTest extends MetadataReaderTestCase +{ + protected function setUp() + { + parent::setUp(); + if (!function_exists('exif_read_data')) { + $this->markTestSkipped('exif extension is not loaded'); + } + } + + protected function getReader() + { + return new ExifMetadataReader(); + } + + public function testExifDataAreReadWithReadFile() + { + $metadata = $this->getReader()->readFile(FixturesLoader::getFixture('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(null, $metadata['ifd0.Orientation']); + } + + public function testExifDataAreReadWithReadData() + { + $metadata = $this->getReader()->readData(file_get_contents(FixturesLoader::getFixture('exifOrientation/90.jpg'))); + $this->assertTrue(isset($metadata['ifd0.Orientation'])); + $this->assertEquals(6, $metadata['ifd0.Orientation']); + } + + public function testExifDataAreReadWithReadStream() + { + $metadata = $this->getReader()->readStream(fopen(FixturesLoader::getFixture('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..ad4b541623d6f --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.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\Tests\Image\Metadata; + +use Symfony\Component\Image\Fixtures\Loader as FixturesLoader; +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 = FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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 = FixturesLoader::getFixture('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..8b6cb711130d2 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php @@ -0,0 +1,111 @@ + + * + * 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..e7c63f5299e81 --- /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..ef4c82f4daaad --- /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..27584ed792d7c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php @@ -0,0 +1,66 @@ + + * + * 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..a22bfd19590fc --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.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\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..7eb573777c162 --- /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 int $move + * @param int $x1 + * @param int $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..6ffc159728303 --- /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 int $x + * @param int $y + * @param BoxInterface $box + * @param bool $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 int $x + * @param int $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 int $x + * @param int $y + * @param int $move + * @param int $x1 + * @param int $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..a1b6c9bf7d13a --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/ProfileTest.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\Tests\Image; + +use Symfony\Component\Image\Fixtures\Loader as FixturesLoader; +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 = FixturesLoader::getFixture('ICCProfiles/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..8993fef77c124 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Imagick/EffectsTest.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\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() + { + if (defined('HHVM_VERSION')) { + return '#292929'; + } + + return '#555555'; + } + + protected function getComponentGrayValue() + { + if (defined('HHVM_VERSION')) { + return 41; + } + + 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..4f41b9436c960 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php @@ -0,0 +1,149 @@ + + * + * 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\Fixtures\Loader as FixturesLoader; +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() + { + $loader = $this->getLoader(); + + $image = $loader->open(FixturesLoader::getFixture('resize/210-design-19933.jpg')); + + $image + ->resize(new Box(1500, 750)) + ->save($this->getTempDir().'/large.png') + ; + + $this->assertSame(1500, $image->getSize()->getWidth()); + $this->assertSame(750, $image->getSize()->getHeight()); + + $image + ->resize(new Box(100, 50)) + ->save($this->getTempDir().'/small.png') + ; + + $this->assertSame(100, $image->getSize()->getWidth()); + $this->assertSame(50, $image->getSize()->getHeight()); + } + + public function testAnimatedGifResize() + { + $loader = $this->getLoader(); + $image = $loader->open(FixturesLoader::getFixture('anima3.gif')); + $image + ->resize(new Box(150, 100)) + ->save($this->getTempDir().'/anima3-150x100-actual.gif', array('animated' => true)) + ; + $this->assertImageEquals( + $loader->open(FixturesLoader::getFixture('resize/anima3-150x100.gif')), + $loader->open($this->getTempDir().'/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() + { + $loader = $this->getLoader(); + $image = $loader->open(FixturesLoader::getFixture('anima3.gif')); + $image + ->crop( + new Point(0, 0), + new Box(150, 100) + ) + ->save($this->getTempDir().'/anima3-topleft-actual.gif', array('animated' => true)) + ; + $this->assertImageEquals( + $loader->open(FixturesLoader::getFixture('crop/anima3-topleft.gif')), + $loader->open($this->getTempDir().'/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..0b1ce8e483844 --- /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..8ecd6900f563f --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.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\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() + { + if (defined('HHVM_VERSION_ID')) { + return new Box(116, 55); + } + + return new Box(117, 55); + } + + protected function getLoader() + { + return new Loader(); + } + + protected function isFontTestSupported() + { + return true; + } +} diff --git a/src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php b/src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php new file mode 100644 index 0000000000000..c79f832551f59 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php @@ -0,0 +1,35 @@ +markTestSkipped($e->getMessage()); + } + + return $loader; + } + + /** + * @expectedException \Symfony\Component\Image\Exception\RuntimeException + */ + public function testShouldThrowExceptionNotError() + { + $invalidPath = '/thispathdoesnotexist'; + + $loader = $this->getLoader(); + + $loader->open(FixturesLoader::getFixture('large.jpg')) + ->save($invalidPath.'/myfile.jpg'); + } +} diff --git a/src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php b/src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php new file mode 100644 index 0000000000000..4ffb979ca678d --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php @@ -0,0 +1,36 @@ +markTestSkipped($e->getMessage()); + } + + return $loader; + } + + public function testShouldSaveGifImageWithMoreThan256TransparentPixels() + { + $loader = $this->getLoader(); + $new = sys_get_temp_dir().'/sample.jpeg'; + + $image = $loader + ->open(FixturesLoader::getFixture('sample.gif')) + ->save($new) + ; + + $this->assertSame(700, $image->getSize()->getWidth()); + $this->assertSame(440, $image->getSize()->getHeight()); + } +} diff --git a/src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php b/src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php new file mode 100644 index 0000000000000..7873169b49c68 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php @@ -0,0 +1,40 @@ +markTestSkipped($e->getMessage()); + } + + return $loader; + } + + public function testShouldResize() + { + $size = new Box(100, 10); + $loader = $this->getLoader(); + + $loader->open(FixturesLoader::getFixture('large.jpg')) + ->thumbnail($size, ImageInterface::THUMBNAIL_OUTBOUND) + ->save($this->getTempDir().'/resized.jpg'); + + $this->assertFileExists($this->getTempDir().'/resized.jpg'); + $this->assertEquals( + $size, + $loader->open($this->getTempDir().'/resized.jpg')->getSize() + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php b/src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php new file mode 100644 index 0000000000000..46d0e07c3adbb --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php @@ -0,0 +1,106 @@ +isFile()) { + $filenames[] = $fileinfo->getPathname(); + } + } + + return $filenames; + } + + private function getImagickLoader($file) + { + try { + $loader = new ImagickLoader(); + $image = $loader->open($file); + } catch (RuntimeException $e) { + $this->markTestSkipped($e->getMessage()); + } + + return $image; + } + + private function getGmagickLoader($file) + { + try { + $loader = new GmagickLoader(); + $image = $loader->open($file); + } catch (RuntimeException $e) { + $this->markTestSkipped($e->getMessage()); + } + + return $image; + } + + public function testShouldSaveOneFileWithImagick() + { + $dir = realpath($this->getTemporaryDir()); + $targetFile = $dir.'/myfile.png'; + + $loader = $this->getImagickLoader(FixturesLoader::getFixture('multi-layer.psd')); + + $loader->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'; + + $loader = $this->getGmagickLoader(FixturesLoader::getFixture('multi-layer.psd')); + + $loader->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/TestCase.php b/src/Symfony/Component/Image/Tests/TestCase.php new file mode 100644 index 0000000000000..5cf653ed11b62 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/TestCase.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\Tests; + +use PHPUnit\Framework\TestCase as PHPUnitTestCase; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Image\Tests\Constraint\IsImageEqual; + +class TestCase extends PHPUnitTestCase +{ + const HTTP_IMAGE = 'http://symfony.com/images/common/logo/logo_symfony_header.png'; + + private $tmpDir; + private static $supportMockingImagick; + + protected function tearDown() + { + if ($this->tmpDir !== null) { + $fs = new Filesystem(); + $fs->remove($this->tmpDir); + $this->tmpDir = null; + } + + parent::tearDown(); + } + + public function getTempDir() + { + if ($this->tmpDir === null) { + $fs = new Filesystem(); + $this->tmpDir = sys_get_temp_dir().'/sf-image-'.microtime(true); + $fs->mkdir($this->tmpDir); + } + return $this->tmpDir; + } + + /** + * Asserts that two images are equal using color histogram comparison method. + * + * @param ImageInterface $expected + * @param ImageInterface $actual + * @param string $message + * @param float $delta + * @param int $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/Tests/results/in_out/.placeholder b/src/Symfony/Component/Image/Tests/results/in_out/.placeholder new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Image/composer.json b/src/Symfony/Component/Image/composer.json new file mode 100644 index 0000000000000..16ae5d91f3213 --- /dev/null +++ b/src/Symfony/Component/Image/composer.json @@ -0,0 +1,52 @@ +{ + "name": "symfony/image", + "type": "library", + "description": "Symfony Image Component", + "keywords": [ + "image manipulation", + "image processing", + "drawing", + "graphics" + ], + "homepage": "https://symfony.com/", + "license": "MIT", + "authors": [ + { + "name": "Bulat Shakirzyanov", + "email": "mallluhuct@gmail.com", + "homepage": "http://avalanche123.com" + }, + { + "name": "Romain Neutron", + "email": "imprec@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/filesystem": "^2.8", + "symfony/image-fixtures": "dev-master@dev" + }, + "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 + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy