From 916aff89bc9feb7a5faeed02c974d61f657f8c99 Mon Sep 17 00:00:00 2001 From: Bulat Shakirzyanov Date: Mon, 27 Mar 2017 17:46:26 +0200 Subject: [PATCH 1/2] Add Symfony Image Component --- .../Component/Image/Draw/DrawerInterface.php | 146 +++ .../Image/Effects/EffectsInterface.php | 78 ++ .../Image/Exception/ExceptionInterface.php | 16 + .../Exception/InvalidArgumentException.php | 16 + .../Image/Exception/NotSupportedException.php | 19 + .../Image/Exception/OutOfBoundsException.php | 16 + .../Image/Exception/RuntimeException.php | 16 + .../Image/Filter/Advanced/Border.php | 98 ++ .../Image/Filter/Advanced/Canvas.php | 74 ++ .../Image/Filter/Advanced/Grayscale.php | 30 + .../Image/Filter/Advanced/OnPixelBased.php | 57 ++ .../Image/Filter/Advanced/RelativeResize.php | 50 + .../Image/Filter/Basic/ApplyMask.php | 42 + .../Image/Filter/Basic/Autorotate.php | 85 ++ .../Component/Image/Filter/Basic/Copy.php | 29 + .../Component/Image/Filter/Basic/Crop.php | 54 ++ .../Component/Image/Filter/Basic/Fill.php | 43 + .../Image/Filter/Basic/FlipHorizontally.php | 29 + .../Image/Filter/Basic/FlipVertically.php | 29 + .../Component/Image/Filter/Basic/Paste.php | 53 ++ .../Component/Image/Filter/Basic/Resize.php | 48 + .../Component/Image/Filter/Basic/Rotate.php | 52 + .../Component/Image/Filter/Basic/Save.php | 51 + .../Component/Image/Filter/Basic/Show.php | 51 + .../Component/Image/Filter/Basic/Strip.php | 29 + .../Image/Filter/Basic/Thumbnail.php | 59 ++ .../Image/Filter/Basic/WebOptimization.php | 57 ++ .../Image/Filter/FilterInterface.php | 30 + .../Component/Image/Filter/LoaderAware.php | 54 ++ .../Component/Image/Filter/Transformation.php | 240 +++++ src/Symfony/Component/Image/Gd/Drawer.php | 333 +++++++ src/Symfony/Component/Image/Gd/Effects.php | 109 +++ src/Symfony/Component/Image/Gd/Font.php | 41 + src/Symfony/Component/Image/Gd/Image.php | 735 ++++++++++++++ src/Symfony/Component/Image/Gd/Layers.php | 144 +++ src/Symfony/Component/Image/Gd/Loader.php | 195 ++++ .../Component/Image/Gmagick/Drawer.php | 356 +++++++ .../Component/Image/Gmagick/Effects.php | 106 +++ src/Symfony/Component/Image/Gmagick/Font.php | 63 ++ src/Symfony/Component/Image/Gmagick/Image.php | 790 +++++++++++++++ .../Component/Image/Gmagick/Layers.php | 272 ++++++ .../Component/Image/Gmagick/Loader.php | 165 ++++ .../Component/Image/Image/AbstractFont.php | 75 ++ .../Component/Image/Image/AbstractImage.php | 120 +++ .../Component/Image/Image/AbstractLayers.php | 61 ++ .../Component/Image/Image/AbstractLoader.php | 79 ++ src/Symfony/Component/Image/Image/Box.php | 122 +++ .../Component/Image/Image/BoxInterface.php | 94 ++ .../Image/Image/Fill/FillInterface.php | 30 + .../Image/Image/Fill/Gradient/Horizontal.php | 28 + .../Image/Image/Fill/Gradient/Linear.php | 95 ++ .../Image/Image/Fill/Gradient/Vertical.php | 28 + .../Component/Image/Image/FontInterface.php | 51 + .../Image/Image/Histogram/Bucket.php | 56 ++ .../Component/Image/Image/Histogram/Range.php | 56 ++ .../Component/Image/Image/ImageInterface.php | 173 ++++ .../Component/Image/Image/LayersInterface.php | 107 +++ .../Component/Image/Image/LoaderInterface.php | 81 ++ .../Image/Image/ManipulatorInterface.php | 181 ++++ .../Image/Metadata/AbstractMetadataReader.php | 105 ++ .../Image/Metadata/DefaultMetadataReader.php | 42 + .../Image/Metadata/ExifMetadataReader.php | 115 +++ .../Image/Image/Metadata/MetadataBag.php | 97 ++ .../Metadata/MetadataReaderInterface.php | 49 + .../Component/Image/Image/Palette/CMYK.php | 118 +++ .../Image/Image/Palette/Color/CMYK.php | 219 +++++ .../Image/Palette/Color/ColorInterface.php | 95 ++ .../Image/Image/Palette/Color/Gray.php | 164 ++++ .../Image/Image/Palette/Color/RGB.php | 214 +++++ .../Image/Image/Palette/ColorParser.php | 153 +++ .../Image/Image/Palette/Grayscale.php | 123 +++ .../Image/Image/Palette/PaletteInterface.php | 87 ++ .../Component/Image/Image/Palette/RGB.php | 129 +++ src/Symfony/Component/Image/Image/Point.php | 88 ++ .../Component/Image/Image/Point/Center.php | 77 ++ .../Component/Image/Image/PointInterface.php | 56 ++ src/Symfony/Component/Image/Image/Profile.php | 60 ++ .../Image/Image/ProfileInterface.php | 29 + .../Component/Image/Imagick/Drawer.php | 404 ++++++++ .../Component/Image/Imagick/Effects.php | 119 +++ src/Symfony/Component/Image/Imagick/Font.php | 68 ++ src/Symfony/Component/Image/Imagick/Image.php | 900 ++++++++++++++++++ .../Component/Image/Imagick/Layers.php | 271 ++++++ .../Component/Image/Imagick/Loader.php | 184 ++++ .../Resources/Adobe/CMYK/USWebUncoated.icc | Bin 0 -> 557164 bytes .../Component/Image/Resources/Adobe/LICENSE | 1 + .../sRGB_IEC61966-2-1_black_scaled.icc | Bin 0 -> 3048 bytes .../ISOcoated_v2_grey1c_bas.ICC | Bin 0 -> 936 bytes .../Image/Tests/Constraint/IsImageEqual.php | 162 ++++ .../Image/Tests/Draw/AbstractDrawerTest.php | 253 +++++ .../Tests/Effects/AbstractEffectsTest.php | 141 +++ .../Tests/Filter/Advanced/BorderTest.php | 54 ++ .../Tests/Filter/Advanced/CanvasTest.php | 62 ++ .../Tests/Filter/Advanced/GrayscaleTest.php | 85 ++ .../Tests/Filter/Basic/AutorotateTest.php | 62 ++ .../Image/Tests/Filter/Basic/CopyTest.php | 31 + .../Image/Tests/Filter/Basic/CropTest.php | 57 ++ .../Filter/Basic/FlipHorizontallyTest.php | 30 + .../Tests/Filter/Basic/FlipVerticallyTest.php | 30 + .../Image/Tests/Filter/Basic/PasteTest.php | 34 + .../Image/Tests/Filter/Basic/ResizeTest.php | 56 ++ .../Image/Tests/Filter/Basic/RotateTest.php | 32 + .../Image/Tests/Filter/Basic/SaveTest.php | 32 + .../Image/Tests/Filter/Basic/ShowTest.php | 32 + .../Image/Tests/Filter/Basic/StripTest.php | 30 + .../Tests/Filter/Basic/ThumbnailTest.php | 35 + .../Filter/Basic/WebOptimizationTest.php | 115 +++ .../Tests/Filter/DummyLoaderAwareFilter.php | 24 + .../Image/Tests/Filter/FilterTestCase.php | 47 + .../Image/Tests/Filter/LoaderAwareTest.php | 86 ++ .../Image/Tests/Filter/TransformationTest.php | 182 ++++ .../GdTransparentGifHandlingTest.php | 56 ++ .../Component/Image/Tests/Gd/DrawerTest.php | 39 + .../Component/Image/Tests/Gd/EffectsTest.php | 32 + .../Component/Image/Tests/Gd/ImageTest.php | 132 +++ .../Component/Image/Tests/Gd/LayersTest.php | 128 +++ .../Component/Image/Tests/Gd/LoaderTest.php | 53 ++ .../Image/Tests/Gmagick/DrawerTest.php | 37 + .../Image/Tests/Gmagick/EffectsTest.php | 38 + .../Image/Tests/Gmagick/ImageTest.php | 109 +++ .../Image/Tests/Gmagick/LayersTest.php | 97 ++ .../Image/Tests/Gmagick/LoaderTest.php | 48 + .../Image/Tests/Image/AbstractImageTest.php | 817 ++++++++++++++++ .../Image/Tests/Image/AbstractLayersTest.php | 283 ++++++ .../Image/Tests/Image/AbstractLoaderTest.php | 187 ++++ .../Component/Image/Tests/Image/BoxTest.php | 187 ++++ .../Image/Fill/Gradient/HorizontalTest.php | 57 ++ .../Tests/Image/Fill/Gradient/LinearTest.php | 98 ++ .../Image/Fill/Gradient/VerticalTest.php | 57 ++ .../Tests/Image/Histogram/BucketTest.php | 52 + .../Image/Tests/Image/Histogram/RangeTest.php | 52 + .../Metadata/DefaultMetadataReaderTest.php | 22 + .../Image/Metadata/ExifMetadataReaderTest.php | 60 ++ .../Tests/Image/Metadata/MetadataBagTest.php | 42 + .../Image/Metadata/MetadataReaderTestCase.php | 96 ++ .../Image/Palette/AbstractPaletteTest.php | 112 +++ .../Image/Tests/Image/Palette/CMYKTest.php | 68 ++ .../Image/Palette/Color/AbstractColorTest.php | 130 +++ .../Tests/Image/Palette/Color/CMYKTest.php | 75 ++ .../Tests/Image/Palette/Color/GrayTest.php | 65 ++ .../Tests/Image/Palette/Color/RGBTest.php | 67 ++ .../Tests/Image/Palette/ColorParserTest.php | 165 ++++ .../Tests/Image/Palette/GrayscaleTest.php | 64 ++ .../Image/Tests/Image/Palette/RGBTest.php | 64 ++ .../Image/Tests/Image/Point/CenterTest.php | 90 ++ .../Component/Image/Tests/Image/PointTest.php | 125 +++ .../Image/Tests/Image/ProfileTest.php | 49 + .../Image/Tests/Imagick/DrawerTest.php | 37 + .../Image/Tests/Imagick/EffectsTest.php | 50 + .../Image/Tests/Imagick/ImageTest.php | 155 +++ .../Image/Tests/Imagick/LayersTest.php | 125 +++ .../Image/Tests/Imagick/LoaderTest.php | 56 ++ .../Image/Tests/Issues/Issue131Test.php | 107 +++ .../Image/Tests/Issues/Issue17Test.php | 42 + .../Image/Tests/Issues/Issue59Test.php | 38 + .../Image/Tests/Issues/Issue67Test.php | 35 + .../Component/Image/Tests/TestCase.php | 71 ++ .../Image/Tests/results/in_out/.placeholder | 0 158 files changed, 16605 insertions(+) create mode 100644 src/Symfony/Component/Image/Draw/DrawerInterface.php create mode 100644 src/Symfony/Component/Image/Effects/EffectsInterface.php create mode 100644 src/Symfony/Component/Image/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/Image/Exception/InvalidArgumentException.php create mode 100644 src/Symfony/Component/Image/Exception/NotSupportedException.php create mode 100644 src/Symfony/Component/Image/Exception/OutOfBoundsException.php create mode 100644 src/Symfony/Component/Image/Exception/RuntimeException.php create mode 100644 src/Symfony/Component/Image/Filter/Advanced/Border.php create mode 100644 src/Symfony/Component/Image/Filter/Advanced/Canvas.php create mode 100644 src/Symfony/Component/Image/Filter/Advanced/Grayscale.php create mode 100644 src/Symfony/Component/Image/Filter/Advanced/OnPixelBased.php create mode 100644 src/Symfony/Component/Image/Filter/Advanced/RelativeResize.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/ApplyMask.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Autorotate.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Copy.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Crop.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Fill.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/FlipHorizontally.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/FlipVertically.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Paste.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Resize.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Rotate.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Save.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Show.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Strip.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/Thumbnail.php create mode 100644 src/Symfony/Component/Image/Filter/Basic/WebOptimization.php create mode 100644 src/Symfony/Component/Image/Filter/FilterInterface.php create mode 100644 src/Symfony/Component/Image/Filter/LoaderAware.php create mode 100644 src/Symfony/Component/Image/Filter/Transformation.php create mode 100644 src/Symfony/Component/Image/Gd/Drawer.php create mode 100644 src/Symfony/Component/Image/Gd/Effects.php create mode 100644 src/Symfony/Component/Image/Gd/Font.php create mode 100644 src/Symfony/Component/Image/Gd/Image.php create mode 100644 src/Symfony/Component/Image/Gd/Layers.php create mode 100644 src/Symfony/Component/Image/Gd/Loader.php create mode 100644 src/Symfony/Component/Image/Gmagick/Drawer.php create mode 100644 src/Symfony/Component/Image/Gmagick/Effects.php create mode 100644 src/Symfony/Component/Image/Gmagick/Font.php create mode 100644 src/Symfony/Component/Image/Gmagick/Image.php create mode 100644 src/Symfony/Component/Image/Gmagick/Layers.php create mode 100644 src/Symfony/Component/Image/Gmagick/Loader.php create mode 100644 src/Symfony/Component/Image/Image/AbstractFont.php create mode 100644 src/Symfony/Component/Image/Image/AbstractImage.php create mode 100644 src/Symfony/Component/Image/Image/AbstractLayers.php create mode 100644 src/Symfony/Component/Image/Image/AbstractLoader.php create mode 100644 src/Symfony/Component/Image/Image/Box.php create mode 100644 src/Symfony/Component/Image/Image/BoxInterface.php create mode 100644 src/Symfony/Component/Image/Image/Fill/FillInterface.php create mode 100644 src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php create mode 100644 src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php create mode 100644 src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php create mode 100644 src/Symfony/Component/Image/Image/FontInterface.php create mode 100644 src/Symfony/Component/Image/Image/Histogram/Bucket.php create mode 100644 src/Symfony/Component/Image/Image/Histogram/Range.php create mode 100644 src/Symfony/Component/Image/Image/ImageInterface.php create mode 100644 src/Symfony/Component/Image/Image/LayersInterface.php create mode 100644 src/Symfony/Component/Image/Image/LoaderInterface.php create mode 100644 src/Symfony/Component/Image/Image/ManipulatorInterface.php create mode 100644 src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php create mode 100644 src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php create mode 100644 src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php create mode 100644 src/Symfony/Component/Image/Image/Metadata/MetadataBag.php create mode 100644 src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php create mode 100644 src/Symfony/Component/Image/Image/Palette/CMYK.php create mode 100644 src/Symfony/Component/Image/Image/Palette/Color/CMYK.php create mode 100644 src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php create mode 100644 src/Symfony/Component/Image/Image/Palette/Color/Gray.php create mode 100644 src/Symfony/Component/Image/Image/Palette/Color/RGB.php create mode 100644 src/Symfony/Component/Image/Image/Palette/ColorParser.php create mode 100644 src/Symfony/Component/Image/Image/Palette/Grayscale.php create mode 100644 src/Symfony/Component/Image/Image/Palette/PaletteInterface.php create mode 100644 src/Symfony/Component/Image/Image/Palette/RGB.php create mode 100644 src/Symfony/Component/Image/Image/Point.php create mode 100644 src/Symfony/Component/Image/Image/Point/Center.php create mode 100644 src/Symfony/Component/Image/Image/PointInterface.php create mode 100644 src/Symfony/Component/Image/Image/Profile.php create mode 100644 src/Symfony/Component/Image/Image/ProfileInterface.php create mode 100644 src/Symfony/Component/Image/Imagick/Drawer.php create mode 100644 src/Symfony/Component/Image/Imagick/Effects.php create mode 100644 src/Symfony/Component/Image/Imagick/Font.php create mode 100644 src/Symfony/Component/Image/Imagick/Image.php create mode 100644 src/Symfony/Component/Image/Imagick/Layers.php create mode 100644 src/Symfony/Component/Image/Imagick/Loader.php create mode 100644 src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc create mode 100644 src/Symfony/Component/Image/Resources/Adobe/LICENSE create mode 100644 src/Symfony/Component/Image/Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc create mode 100644 src/Symfony/Component/Image/Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC create mode 100644 src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php create mode 100644 src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php create mode 100644 src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php create mode 100644 src/Symfony/Component/Image/Tests/Filter/TransformationTest.php create mode 100644 src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gd/DrawerTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gd/EffectsTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gd/ImageTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gd/LayersTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gd/LoaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gmagick/DrawerTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gmagick/EffectsTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gmagick/LayersTest.php create mode 100644 src/Symfony/Component/Image/Tests/Gmagick/LoaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/BoxTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Metadata/MetadataBagTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/PointTest.php create mode 100644 src/Symfony/Component/Image/Tests/Image/ProfileTest.php create mode 100644 src/Symfony/Component/Image/Tests/Imagick/DrawerTest.php create mode 100644 src/Symfony/Component/Image/Tests/Imagick/EffectsTest.php create mode 100644 src/Symfony/Component/Image/Tests/Imagick/ImageTest.php create mode 100644 src/Symfony/Component/Image/Tests/Imagick/LayersTest.php create mode 100644 src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php create mode 100644 src/Symfony/Component/Image/Tests/Issues/Issue131Test.php create mode 100644 src/Symfony/Component/Image/Tests/Issues/Issue17Test.php create mode 100644 src/Symfony/Component/Image/Tests/Issues/Issue59Test.php create mode 100644 src/Symfony/Component/Image/Tests/Issues/Issue67Test.php create mode 100644 src/Symfony/Component/Image/Tests/TestCase.php create mode 100644 src/Symfony/Component/Image/Tests/results/in_out/.placeholder diff --git a/src/Symfony/Component/Image/Draw/DrawerInterface.php b/src/Symfony/Component/Image/Draw/DrawerInterface.php new file mode 100644 index 0000000000000..33d85a57db8af --- /dev/null +++ b/src/Symfony/Component/Image/Draw/DrawerInterface.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Draw; + +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Exception\RuntimeException; + +interface DrawerInterface +{ + /** + * Draws an arc on a starting at a given x, y coordinates under a given + * start and end angles. + * + * @param PointInterface $center + * @param BoxInterface $size + * @param int $start + * @param int $end + * @param ColorInterface $color + * @param int $thickness + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1); + + /** + * Same as arc, but also connects end points with a straight line. + * + * @param PointInterface $center + * @param BoxInterface $size + * @param int $start + * @param int $end + * @param ColorInterface $color + * @param bool $fill + * @param int $thickness + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1); + + /** + * Draws and ellipse with center at the given x, y coordinates, and given + * width and height. + * + * @param PointInterface $center + * @param BoxInterface $size + * @param ColorInterface $color + * @param bool $fill + * @param int $thickness + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1); + + /** + * Draws a line from start(x, y) to end(x, y) coordinates. + * + * @param PointInterface $start + * @param PointInterface $end + * @param ColorInterface $outline + * @param int $thickness + * + * @return DrawerInterface + */ + public function line(PointInterface $start, PointInterface $end, ColorInterface $outline, $thickness = 1); + + /** + * Same as arc, but connects end points and the center. + * + * @param PointInterface $center + * @param BoxInterface $size + * @param int $start + * @param int $end + * @param ColorInterface $color + * @param bool $fill + * @param int $thickness + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1); + + /** + * Places a one pixel point at specific coordinates and fills it with + * specified color. + * + * @param PointInterface $position + * @param ColorInterface $color + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function dot(PointInterface $position, ColorInterface $color); + + /** + * Draws a polygon using array of x, y coordinates. Must contain at least + * three coordinates. + * + * @param array $coordinates + * @param ColorInterface $color + * @param bool $fill + * @param int $thickness + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1); + + /** + * Annotates image with specified text at a given position starting on the + * top left of the final text box. + * + * The rotation is done CW + * + * @param string $string + * @param AbstractFont $font + * @param PointInterface $position + * @param int $angle + * @param int $width + * + * @throws RuntimeException + * + * @return DrawerInterface + */ + public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null); +} diff --git a/src/Symfony/Component/Image/Effects/EffectsInterface.php b/src/Symfony/Component/Image/Effects/EffectsInterface.php new file mode 100644 index 0000000000000..c327f010b601c --- /dev/null +++ b/src/Symfony/Component/Image/Effects/EffectsInterface.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Effects; + +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +interface EffectsInterface +{ + /** + * Apply gamma correction. + * + * @param float $correction + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function gamma($correction); + + /** + * Invert the colors of the image. + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function negative(); + + /** + * Grayscale the image. + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function grayscale(); + + /** + * Colorize the image. + * + * @param ColorInterface $color + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function colorize(ColorInterface $color); + + /** + * Sharpens the image. + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function sharpen(); + + /** + * Blur the image. + * + * @param float|int $sigma + * + * @return EffectsInterface + * + * @throws RuntimeException + */ + public function blur($sigma); +} diff --git a/src/Symfony/Component/Image/Exception/ExceptionInterface.php b/src/Symfony/Component/Image/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..0f26b9acce285 --- /dev/null +++ b/src/Symfony/Component/Image/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Exception; + +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Image/Exception/InvalidArgumentException.php b/src/Symfony/Component/Image/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..75e5da1e81f46 --- /dev/null +++ b/src/Symfony/Component/Image/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Exception; + +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Image/Exception/NotSupportedException.php b/src/Symfony/Component/Image/Exception/NotSupportedException.php new file mode 100644 index 0000000000000..bd3e8a90506f4 --- /dev/null +++ b/src/Symfony/Component/Image/Exception/NotSupportedException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Exception; + +/** + * Should be used when a driver does not support an operation. + */ +class NotSupportedException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Image/Exception/OutOfBoundsException.php b/src/Symfony/Component/Image/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000000..13d6dc497d896 --- /dev/null +++ b/src/Symfony/Component/Image/Exception/OutOfBoundsException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Exception; + +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Image/Exception/RuntimeException.php b/src/Symfony/Component/Image/Exception/RuntimeException.php new file mode 100644 index 0000000000000..51a63c07155a0 --- /dev/null +++ b/src/Symfony/Component/Image/Exception/RuntimeException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Exception; + +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Image/Filter/Advanced/Border.php b/src/Symfony/Component/Image/Filter/Advanced/Border.php new file mode 100644 index 0000000000000..60ed1facf6989 --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Advanced/Border.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Filter\Advanced; + +use Symfony\Component\Image\Filter\FilterInterface; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Point; + +/** + * A border filter. + */ +class Border implements FilterInterface +{ + /** + * @var ColorInterface + */ + private $color; + + /** + * @var int + */ + private $width; + + /** + * @var int + */ + private $height; + + /** + * Constructs Border filter with given color, width and height. + * + * @param ColorInterface $color + * @param int $width Width of the border on the left and right sides of the image + * @param int $height Height of the border on the top and bottom sides of the image + */ + public function __construct(ColorInterface $color, $width = 1, $height = 1) + { + $this->color = $color; + $this->width = $width; + $this->height = $height; + } + + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image) + { + $size = $image->getSize(); + $width = $size->getWidth(); + $height = $size->getHeight(); + + $draw = $image->draw(); + + // Draw top and bottom lines + $draw + ->line( + new Point(0, 0), + new Point($width - 1, 0), + $this->color, + $this->height + ) + ->line( + new Point($width - 1, $height - 1), + new Point(0, $height - 1), + $this->color, + $this->height + ) + ; + + // Draw sides + $draw + ->line( + new Point(0, 0), + new Point(0, $height - 1), + $this->color, + $this->width + ) + ->line( + new Point($width - 1, 0), + new Point($width - 1, $height - 1), + $this->color, + $this->width + ) + ; + + return $image; + } +} diff --git a/src/Symfony/Component/Image/Filter/Advanced/Canvas.php b/src/Symfony/Component/Image/Filter/Advanced/Canvas.php new file mode 100644 index 0000000000000..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..06fb5102d423d --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Filter\Basic; + +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Filter\FilterInterface; + +/** + * A filter to render web-optimized images + */ +class WebOptimization implements FilterInterface +{ + private $palette; + private $path; + private $options; + + public function __construct($path = null, array $options = array()) + { + $this->path = $path; + $this->options = array_replace(array( + 'resolution-units' => ImageInterface::RESOLUTION_PIXELSPERINCH, + 'resolution-y' => 72, + 'resolution-x' => 72, + ), $options); + $this->palette = new RGB(); + } + + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image) + { + $image + ->usePalette($this->palette) + ->strip(); + + if (is_callable($this->path)) { + $path = call_user_func($this->path, $image); + } elseif (null !== $this->path) { + $path = $this->path; + } else { + return $image; + } + + return $image->save($path, $this->options); + } +} diff --git a/src/Symfony/Component/Image/Filter/FilterInterface.php b/src/Symfony/Component/Image/Filter/FilterInterface.php new file mode 100644 index 0000000000000..3313b4cedcf01 --- /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..834853dd7c775 --- /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..c6bc5ae5f89e2 --- /dev/null +++ b/src/Symfony/Component/Image/Filter/Transformation.php @@ -0,0 +1,240 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Filter; + +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Filter\Basic\ApplyMask; +use Symfony\Component\Image\Filter\Basic\Copy; +use Symfony\Component\Image\Filter\Basic\Crop; +use Symfony\Component\Image\Filter\Basic\Fill; +use Symfony\Component\Image\Filter\Basic\FlipVertically; +use Symfony\Component\Image\Filter\Basic\FlipHorizontally; +use Symfony\Component\Image\Filter\Basic\Paste; +use Symfony\Component\Image\Filter\Basic\Resize; +use Symfony\Component\Image\Filter\Basic\Rotate; +use Symfony\Component\Image\Filter\Basic\Save; +use Symfony\Component\Image\Filter\Basic\Show; +use Symfony\Component\Image\Filter\Basic\Strip; +use Symfony\Component\Image\Filter\Basic\Thumbnail; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\LoaderInterface; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; +use Symfony\Component\Image\Image\ManipulatorInterface; +use Symfony\Component\Image\Image\PointInterface; + +/** + * A transformation filter + */ +final class Transformation implements FilterInterface, ManipulatorInterface +{ + /** + * @var array + */ + private $filters = array(); + + /** + * @var array + */ + private $sorted; + + /** + * An LoaderInterface instance. + * + * @var LoaderInterface + */ + private $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..b543d4c8e0aba --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Drawer.php @@ -0,0 +1,333 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Draw\DrawerInterface; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Drawer implementation using the GD library + */ +final class Drawer implements DrawerInterface +{ + /** + * @var resource + */ + private $resource; + + /** + * @var array + */ + private $info; + + /** + * Constructs Drawer with a given gd image resource + * + * @param resource $resource + */ + public function __construct($resource) + { + $this->loadGdInfo(); + $this->resource = $resource; + } + + /** + * {@inheritdoc} + */ + public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw arc operation failed'); + } + + if (false === imagearc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color))) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw arc operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw arc operation failed'); + } + + return $this; + } + + /** + * This function does not work properly because of a bug in GD + * + * {@inheritdoc} + */ + public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if ($fill) { + $style = IMG_ARC_CHORD; + } else { + $style = IMG_ARC_CHORD | IMG_ARC_NOFILL; + } + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw chord operation failed'); + } + + if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw chord operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw chord operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if ($fill) { + $callback = 'imagefilledellipse'; + } else { + $callback = 'imageellipse'; + } + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw ellipse operation failed'); + } + + if (false === $callback($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $this->getColor($color))) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw ellipse operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw ellipse operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw line operation failed'); + } + + if (false === imageline($this->resource, $start->getX(), $start->getY(), $end->getX(), $end->getY(), $this->getColor($color))) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw line operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw line operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if ($fill) { + $style = IMG_ARC_EDGED; + } else { + $style = IMG_ARC_EDGED | IMG_ARC_NOFILL; + } + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw chord operation failed'); + } + + if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw chord operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw chord operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function dot(PointInterface $position, ColorInterface $color) + { + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw point operation failed'); + } + + if (false === imagesetpixel($this->resource, $position->getX(), $position->getY(), $this->getColor($color))) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw point operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw point operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1) + { + imagesetthickness($this->resource, max(1, (int) $thickness)); + + if (count($coordinates) < 3) { + throw new InvalidArgumentException(sprintf('A polygon must consist of at least 3 points, %d given', count($coordinates))); + } + + $points = call_user_func_array('array_merge', array_map(function (PointInterface $p) { + return array($p->getX(), $p->getY()); + }, $coordinates)); + + if ($fill) { + $callback = 'imagefilledpolygon'; + } else { + $callback = 'imagepolygon'; + } + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Draw polygon operation failed'); + } + + if (false === $callback($this->resource, $points, count($coordinates), $this->getColor($color))) { + imagealphablending($this->resource, false); + throw new RuntimeException('Draw polygon operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Draw polygon operation failed'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null) + { + if (!$this->info['FreeType Support']) { + throw new RuntimeException('GD is not compiled with FreeType support'); + } + + $angle = -1 * $angle; + $fontsize = $font->getSize(); + $fontfile = $font->getFile(); + $x = $position->getX(); + $y = $position->getY() + $fontsize; + + if ($width !== null) { + $string = $this->wrapText($string, $font, $angle, $width); + } + + if (false === imagealphablending($this->resource, true)) { + throw new RuntimeException('Font mask operation failed'); + } + + if (false === imagefttext($this->resource, $fontsize, $angle, $x, $y, $this->getColor($font->getColor()), $fontfile, $string)) { + imagealphablending($this->resource, false); + throw new RuntimeException('Font mask operation failed'); + } + + if (false === imagealphablending($this->resource, false)) { + throw new RuntimeException('Font mask operation failed'); + } + + return $this; + } + + /** + * Internal + * + * Generates a GD color from Color instance + * + * @param ColorInterface $color + * + * @return resource + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + private function getColor(ColorInterface $color) + { + if (!$color instanceof RGBColor) { + throw new InvalidArgumentException('GD driver only supports RGB colors'); + } + + $gdColor = imagecolorallocatealpha($this->resource, $color->getRed(), $color->getGreen(), $color->getBlue(), (100 - $color->getAlpha()) * 127 / 100); + if (false === $gdColor) { + throw new RuntimeException(sprintf('Unable to allocate color "RGB(%s, %s, %s)" with transparency of %d percent', $color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha())); + } + + return $gdColor; + } + + private function loadGdInfo() + { + if (!function_exists('gd_info')) { + throw new RuntimeException('Gd not installed'); + } + + $this->info = gd_info(); + } + + /** + * Internal + * + * Fits a string into box with given width + */ + private function wrapText($string, AbstractFont $font, $angle, $width) + { + $result = ''; + $words = explode(' ', $string); + foreach ($words as $word) { + $teststring = $result . ' ' . $word; + $testbox = imagettfbbox($font->getSize(), $angle, $font->getFile(), $teststring); + if ($testbox[2] > $width) { + $result .= ($result == '' ? '' : "\n") . $word; + } else { + $result .= ($result == '' ? '' : ' ') . $word; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/Image/Gd/Effects.php b/src/Symfony/Component/Image/Gd/Effects.php new file mode 100644 index 0000000000000..4044ba0489593 --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Effects.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Effects\EffectsInterface; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; + +/** + * Effects implementation using the GD library + */ +class Effects implements EffectsInterface +{ + private $resource; + + public function __construct($resource) + { + $this->resource = $resource; + } + + /** + * {@inheritdoc} + */ + public function gamma($correction) + { + if (false === imagegammacorrect($this->resource, 1.0, $correction)) { + throw new RuntimeException('Failed to apply gamma correction to the image'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function negative() + { + if (false === imagefilter($this->resource, IMG_FILTER_NEGATE)) { + throw new RuntimeException('Failed to negate the image'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + if (false === imagefilter($this->resource, IMG_FILTER_GRAYSCALE)) { + throw new RuntimeException('Failed to grayscale the image'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function colorize(ColorInterface $color) + { + if (!$color instanceof RGBColor) { + throw new RuntimeException('Colorize effects only accepts RGB color in GD context'); + } + + if (false === imagefilter($this->resource, IMG_FILTER_COLORIZE, $color->getRed(), $color->getGreen(), $color->getBlue())) { + throw new RuntimeException('Failed to colorize the image'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function sharpen() + { + $sharpenMatrix = array(array(-1,-1,-1), array(-1,16,-1), array(-1,-1,-1)); + $divisor = array_sum(array_map('array_sum', $sharpenMatrix)); + + if (false === imageconvolution($this->resource, $sharpenMatrix, $divisor, 0)) { + throw new RuntimeException('Failed to sharpen the image'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function blur($sigma = 1) + { + if (false === imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR)) { + throw new RuntimeException('Failed to blur the image'); + } + + return $this; + } +} diff --git a/src/Symfony/Component/Image/Gd/Font.php b/src/Symfony/Component/Image/Gd/Font.php new file mode 100644 index 0000000000000..bb141af92ae46 --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Font.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\Box; + +/** + * Font implementation using the GD library + */ +final class Font extends AbstractFont +{ + /** + * {@inheritdoc} + */ + public function box($string, $angle = 0) + { + if (!function_exists('imageftbbox')) { + throw new RuntimeException('GD must have been compiled with `--with-freetype-dir` option to use the Font feature.'); + } + + $angle = -1 * $angle; + $info = imageftbbox($this->size, $angle, $this->file, $string); + $xs = array($info[0], $info[2], $info[4], $info[6]); + $ys = array($info[1], $info[3], $info[5], $info[7]); + $width = abs(max($xs) - min($xs)); + $height = abs(max($ys) - min($ys)); + + return new Box($width, $height); + } +} diff --git a/src/Symfony/Component/Image/Gd/Image.php b/src/Symfony/Component/Image/Gd/Image.php new file mode 100644 index 0000000000000..051c89408c791 --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Image.php @@ -0,0 +1,735 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Image\AbstractImage; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\RuntimeException; + +/** + * Image implementation using the GD library + */ +final class Image extends AbstractImage +{ + /** + * @var resource + */ + private $resource; + + /** + * @var Layers|null + */ + private $layers; + + /** + * @var PaletteInterface + */ + private $palette; + + /** + * Constructs a new Image instance + * + * @param resource $resource + * @param PaletteInterface $palette + * @param MetadataBag $metadata + */ + public function __construct($resource, PaletteInterface $palette, MetadataBag $metadata) + { + $this->metadata = $metadata; + $this->palette = $palette; + $this->resource = $resource; + } + + /** + * Makes sure the current image resource is destroyed + */ + public function __destruct() + { + if (is_resource($this->resource) && 'gd' === get_resource_type($this->resource)) { + imagedestroy($this->resource); + } + } + + /** + * Returns Gd resource + * + * @return resource + */ + public function getGdResource() + { + return $this->resource; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function copy() + { + $size = $this->getSize(); + $copy = $this->createImage($size, 'copy'); + + if (false === imagecopy($copy, $this->resource, 0, 0, 0, 0, $size->getWidth(), $size->getHeight())) { + throw new RuntimeException('Image copy operation failed'); + } + + return new Image($copy, $this->palette, $this->metadata); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function crop(PointInterface $start, BoxInterface $size) + { + if (!$start->in($this->getSize())) { + throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders'); + } + + $width = $size->getWidth(); + $height = $size->getHeight(); + + $dest = $this->createImage($size, 'crop'); + + if (false === imagecopy($dest, $this->resource, 0, 0, $start->getX(), $start->getY(), $width, $height)) { + throw new RuntimeException('Image crop operation failed'); + } + + imagedestroy($this->resource); + + $this->resource = $dest; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function paste(ImageInterface $image, PointInterface $start) + { + if (!$image instanceof self) { + throw new InvalidArgumentException(sprintf('Gd\Image can only paste() Gd\Image instances, %s given', get_class($image))); + } + + $size = $image->getSize(); + if (!$this->getSize()->contains($size, $start)) { + throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box'); + } + + imagealphablending($this->resource, true); + imagealphablending($image->resource, true); + + if (false === imagecopy($this->resource, $image->resource, $start->getX(), $start->getY(), 0, 0, $size->getWidth(), $size->getHeight())) { + throw new RuntimeException('Image paste operation failed'); + } + + imagealphablending($this->resource, false); + imagealphablending($image->resource, false); + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED) + { + if (ImageInterface::FILTER_UNDEFINED !== $filter) { + throw new InvalidArgumentException('Unsupported filter type, GD only supports ImageInterface::FILTER_UNDEFINED filter'); + } + + $width = $size->getWidth(); + $height = $size->getHeight(); + + $dest = $this->createImage($size, 'resize'); + + imagealphablending($this->resource, true); + imagealphablending($dest, true); + + if (false === imagecopyresampled($dest, $this->resource, 0, 0, 0, 0, $width, $height, imagesx($this->resource), imagesy($this->resource))) { + throw new RuntimeException('Image resize operation failed'); + } + + imagealphablending($this->resource, false); + imagealphablending($dest, false); + + imagedestroy($this->resource); + + $this->resource = $dest; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function rotate($angle, ColorInterface $background = null) + { + $color = $background ? $background : $this->palette->color('fff'); + $resource = imagerotate($this->resource, -1 * $angle, $this->getColor($color)); + + if (false === $resource) { + throw new RuntimeException('Image rotate operation failed'); + } + + imagedestroy($this->resource); + $this->resource = $resource; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function save($path = null, array $options = array()) + { + $path = null === $path ? (isset($this->metadata['filepath']) ? $this->metadata['filepath'] : $path) : $path; + + if (null === $path) { + throw new RuntimeException('You can omit save path only if image has been open from a file'); + } + + if (isset($options['format'])) { + $format = $options['format']; + } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) { + $format = $extension; + } else { + $originalPath = isset($this->metadata['filepath']) ? $this->metadata['filepath'] : null; + $format = pathinfo($originalPath, \PATHINFO_EXTENSION); + } + + $this->saveOrOutput($format, $options, $path); + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function show($format, array $options = array()) + { + header('Content-type: '.$this->getMimeType($format)); + + $this->saveOrOutput($format, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($format, array $options = array()) + { + ob_start(); + $this->saveOrOutput($format, $options); + + return ob_get_clean(); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->get('png'); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function flipHorizontally() + { + $size = $this->getSize(); + $width = $size->getWidth(); + $height = $size->getHeight(); + $dest = $this->createImage($size, 'flip'); + + for ($i = 0; $i < $width; $i++) { + if (false === imagecopy($dest, $this->resource, $i, 0, ($width - 1) - $i, 0, 1, $height)) { + throw new RuntimeException('Horizontal flip operation failed'); + } + } + + imagedestroy($this->resource); + + $this->resource = $dest; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function flipVertically() + { + $size = $this->getSize(); + $width = $size->getWidth(); + $height = $size->getHeight(); + $dest = $this->createImage($size, 'flip'); + + for ($i = 0; $i < $height; $i++) { + if (false === imagecopy($dest, $this->resource, 0, $i, 0, ($height - 1) - $i, $width, 1)) { + throw new RuntimeException('Vertical flip operation failed'); + } + } + + imagedestroy($this->resource); + + $this->resource = $dest; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + final public function strip() + { + // GD strips profiles and comment, so there's nothing to do here + return $this; + } + + /** + * {@inheritdoc} + */ + public function draw() + { + return new Drawer($this->resource); + } + + /** + * {@inheritdoc} + */ + public function effects() + { + return new Effects($this->resource); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return new Box(imagesx($this->resource), imagesy($this->resource)); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function applyMask(ImageInterface $mask) + { + if (!$mask instanceof self) { + throw new InvalidArgumentException('Cannot mask non-gd images'); + } + + $size = $this->getSize(); + $maskSize = $mask->getSize(); + + if ($size != $maskSize) { + throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, Current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size)); + } + + for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) { + for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) { + $position = new Point($x, $y); + $color = $this->getColorAt($position); + $maskColor = $mask->getColorAt($position); + $round = (int) round(max($color->getAlpha(), (100 - $color->getAlpha()) * $maskColor->getRed() / 255)); + + if (false === imagesetpixel($this->resource, $x, $y, $this->getColor($color->dissolve($round - $color->getAlpha())))) { + throw new RuntimeException('Apply mask operation failed'); + } + } + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function fill(FillInterface $fill) + { + $size = $this->getSize(); + + for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) { + for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) { + if (false === imagesetpixel($this->resource, $x, $y, $this->getColor($fill->getColor(new Point($x, $y))))) { + throw new RuntimeException('Fill operation failed'); + } + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function mask() + { + $mask = $this->copy(); + + if (false === imagefilter($mask->resource, IMG_FILTER_GRAYSCALE)) { + throw new RuntimeException('Mask operation failed'); + } + + return $mask; + } + + /** + * {@inheritdoc} + */ + public function histogram() + { + $size = $this->getSize(); + $colors = array(); + + for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) { + for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) { + $colors[] = $this->getColorAt(new Point($x, $y)); + } + } + + return array_unique($colors); + } + + /** + * {@inheritdoc} + */ + public function getColorAt(PointInterface $point) + { + if (!$point->in($this->getSize())) { + throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight())); + } + + $index = imagecolorat($this->resource, $point->getX(), $point->getY()); + $info = imagecolorsforindex($this->resource, $index); + + return $this->palette->color(array($info['red'], $info['green'], $info['blue']), max(min(100 - (int) round($info['alpha'] / 127 * 100), 100), 0)); + } + + /** + * {@inheritdoc} + */ + public function layers() + { + if (null === $this->layers) { + $this->layers = new Layers($this, $this->palette, $this->resource); + } + + return $this->layers; + } + + /** + * {@inheritdoc} + **/ + public function interlace($scheme) + { + static $supportedInterlaceSchemes = array( + ImageInterface::INTERLACE_NONE => 0, + ImageInterface::INTERLACE_LINE => 1, + ImageInterface::INTERLACE_PLANE => 1, + ImageInterface::INTERLACE_PARTITION => 1, + ); + + if (!array_key_exists($scheme, $supportedInterlaceSchemes)) { + throw new InvalidArgumentException('Unsupported interlace type'); + } + + imageinterlace($this->resource, $supportedInterlaceSchemes[$scheme]); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function palette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function profile(ProfileInterface $profile) + { + throw new RuntimeException('GD driver does not support color profiles'); + } + + /** + * {@inheritdoc} + */ + public function usePalette(PaletteInterface $palette) + { + if (!$palette instanceof RGB) { + throw new RuntimeException('GD driver only supports RGB palette'); + } + + $this->palette = $palette; + + return $this; + } + + /** + * Internal + * + * Performs save or show operation using one of GD's image... functions + * + * @param string $format + * @param array $options + * @param string $filename + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + private function saveOrOutput($format, array $options, $filename = null) + { + $format = $this->normalizeFormat($format); + + if (!$this->supported($format)) { + throw new InvalidArgumentException(sprintf('Saving image in "%s" format is not supported, please use one of the following extensions: "%s"', $format, implode('", "', $this->supported()))); + } + + $save = 'image'.$format; + $args = array(&$this->resource, $filename); + + // Preserve BC until version 1.0 + if (isset($options['quality']) && !isset($options['png_compression_level'])) { + $options['png_compression_level'] = round((100 - $options['quality']) * 9 / 100); + } + if (isset($options['filters']) && !isset($options['png_compression_filter'])) { + $options['png_compression_filter'] = $options['filters']; + } + + $options = $this->updateSaveOptions($options); + + if ($format === 'jpeg' && isset($options['jpeg_quality'])) { + $args[] = $options['jpeg_quality']; + } + + if ($format === 'png') { + if (isset($options['png_compression_level'])) { + if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) { + throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9'); + } + $args[] = $options['png_compression_level']; + } else { + $args[] = -1; // use default level + } + + if (isset($options['png_compression_filter'])) { + if (~PNG_ALL_FILTERS & $options['png_compression_filter']) { + throw new InvalidArgumentException('png_compression_filter option should be a combination of the PNG_FILTER_XXX constants'); + } + $args[] = $options['png_compression_filter']; + } + } + + if (($format === 'wbmp' || $format === 'xbm') && isset($options['foreground'])) { + $args[] = $options['foreground']; + } + + $this->setExceptionHandler(); + + if (false === call_user_func_array($save, $args)) { + throw new RuntimeException('Save operation failed'); + } + + $this->resetExceptionHandler(); + } + + /** + * Internal + * + * Generates a GD image + * + * @param BoxInterface $size + * @param string the operation initiating the creation + * + * @return resource + * + * @throws RuntimeException + * + */ + private function createImage(BoxInterface $size, $operation) + { + $resource = imagecreatetruecolor($size->getWidth(), $size->getHeight()); + + if (false === $resource) { + throw new RuntimeException('Image '.$operation.' failed'); + } + + if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) { + throw new RuntimeException('Image '.$operation.' failed'); + } + + if (function_exists('imageantialias')) { + imageantialias($resource, true); + } + + $transparent = imagecolorallocatealpha($resource, 255, 255, 255, 127); + imagefill($resource, 0, 0, $transparent); + imagecolortransparent($resource, $transparent); + + return $resource; + } + + /** + * Internal + * + * Generates a GD color from Color instance + * + * @param ColorInterface $color + * + * @return integer A color identifier + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + private function getColor(ColorInterface $color) + { + if (!$color instanceof RGBColor) { + throw new InvalidArgumentException('GD driver only supports RGB colors'); + } + + $index = imagecolorallocatealpha($this->resource, $color->getRed(), $color->getGreen(), $color->getBlue(), round(127 * (100 - $color->getAlpha()) / 100)); + + if (false === $index) { + throw new RuntimeException(sprintf('Unable to allocate color "RGB(%s, %s, %s)" with transparency of %d percent', $color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha())); + } + + return $index; + } + + /** + * Internal + * + * Normalizes a given format name + * + * @param string $format + * + * @return string + */ + private function normalizeFormat($format) + { + $format = strtolower($format); + + if ('jpg' === $format || 'pjpeg' === $format) { + $format = 'jpeg'; + } + + return $format; + } + + /** + * Internal + * + * Checks whether a given format is supported by GD library + * + * @param string $format + * + * @return Boolean + */ + private function supported($format = null) + { + $formats = array('gif', 'jpeg', 'png', 'wbmp', 'xbm'); + + if (null === $format) { + return $formats; + } + + return in_array($format, $formats); + } + + private function setExceptionHandler() + { + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + if (0 === error_reporting()) { + return; + } + + throw new RuntimeException($errstr, $errno, new \ErrorException($errstr, 0, $errno, $errfile, $errline)); + }, E_WARNING | E_NOTICE); + } + + private function resetExceptionHandler() + { + restore_error_handler(); + } + + /** + * Internal + * + * Get the mime type based on format. + * + * @param string $format + * + * @return string mime-type + * + * @throws RuntimeException + */ + private function getMimeType($format) + { + $format = $this->normalizeFormat($format); + + if (!$this->supported($format)) { + throw new RuntimeException('Invalid format'); + } + + static $mimeTypes = array( + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'wbmp' => 'image/vnd.wap.wbmp', + 'xbm' => 'image/xbm', + ); + + return $mimeTypes[$format]; + } +} diff --git a/src/Symfony/Component/Image/Gd/Layers.php b/src/Symfony/Component/Image/Gd/Layers.php new file mode 100644 index 0000000000000..4c13e0a2dea28 --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Layers.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Image\AbstractLayers; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Exception\NotSupportedException; + +class Layers extends AbstractLayers +{ + private $image; + private $offset; + private $resource; + private $palette; + + public function __construct(Image $image, PaletteInterface $palette, $resource) + { + if (!is_resource($resource)) { + throw new RuntimeException('Invalid Gd resource provided'); + } + + $this->image = $image; + $this->resource = $resource; + $this->offset = 0; + $this->palette = $palette; + } + + /** + * {@inheritdoc} + */ + public function merge() + { + } + + /** + * {@inheritdoc} + */ + public function coalesce() + { + } + + /** + * {@inheritdoc} + */ + public function animate($format, $delay, $loops) + { + return $this; + } + + /** + * {@inheritdoc} + */ + public function current() + { + return new Image($this->resource, $this->palette, new MetadataBag()); + } + + /** + * {@inheritdoc} + */ + public function key() + { + return $this->offset; + } + + /** + * {@inheritdoc} + */ + public function next() + { + ++$this->offset; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->offset = 0; + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return $this->offset < 1; + } + + /** + * {@inheritdoc} + */ + public function count() + { + return 1; + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return 0 === $offset; + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + if (0 === $offset) { + return new Image($this->resource, $this->palette, new MetadataBag()); + } + + throw new RuntimeException('GD only supports one layer at offset 0'); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + throw new NotSupportedException('GD does not support layer set'); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + throw new NotSupportedException('GD does not support layer unset'); + } +} diff --git a/src/Symfony/Component/Image/Gd/Loader.php b/src/Symfony/Component/Image/Gd/Loader.php new file mode 100644 index 0000000000000..85437cad45c78 --- /dev/null +++ b/src/Symfony/Component/Image/Gd/Loader.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gd; + +use Symfony\Component\Image\Image\AbstractLoader; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; + +/** + * Loader implementation using the GD library + */ +final class Loader extends AbstractLoader +{ + /** + * @var array + */ + private $info; + + /** + * @throws RuntimeException + */ + public function __construct() + { + $this->loadGdInfo(); + $this->requireGdVersion('2.0.1'); + } + + /** + * {@inheritdoc} + */ + public function create(BoxInterface $size, ColorInterface $color = null) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + $resource = imagecreatetruecolor($width, $height); + + if (false === $resource) { + throw new RuntimeException('Create operation failed'); + } + + $palette = null !== $color ? $color->getPalette() : new RGB(); + $color = $color ? $color : $palette->color('fff'); + + if (!$color instanceof RGBColor) { + throw new InvalidArgumentException('GD driver only supports RGB colors'); + } + + $index = imagecolorallocatealpha($resource, $color->getRed(), $color->getGreen(), $color->getBlue(), round(127 * (100 - $color->getAlpha()) / 100)); + + if (false === $index) { + throw new RuntimeException('Unable to allocate color'); + } + + if (false === imagefill($resource, 0, 0, $index)) { + throw new RuntimeException('Could not set background color fill'); + } + + if ($color->getAlpha() >= 95) { + imagecolortransparent($resource, $index); + } + + return $this->wrap($resource, $palette, new MetadataBag()); + } + + /** + * {@inheritdoc} + */ + public function open($path) + { + $path = $this->checkPath($path); + $data = @file_get_contents($path); + + if (false === $data) { + throw new RuntimeException(sprintf('Failed to open file %s', $path)); + } + + $resource = @imagecreatefromstring($data); + + if (!is_resource($resource)) { + throw new RuntimeException(sprintf('Unable to open image %s', $path)); + } + + return $this->wrap($resource, new RGB(), $this->getMetadataReader()->readFile($path)); + } + + /** + * {@inheritdoc} + */ + public function load($string) + { + return $this->doLoad($string, $this->getMetadataReader()->readData($string)); + } + + /** + * {@inheritdoc} + */ + public function read($resource) + { + if (!is_resource($resource)) { + throw new InvalidArgumentException('Variable does not contain a stream resource'); + } + + $content = stream_get_contents($resource); + + if (false === $content) { + throw new InvalidArgumentException('Cannot read resource content'); + } + + return $this->doLoad($content, $this->getMetadataReader()->readData($content, $resource)); + } + + /** + * {@inheritdoc} + */ + public function font($file, $size, ColorInterface $color) + { + if (!$this->info['FreeType Support']) { + throw new RuntimeException('GD is not compiled with FreeType support'); + } + + return new Font($file, $size, $color); + } + + private function wrap($resource, PaletteInterface $palette, MetadataBag $metadata) + { + if (!imageistruecolor($resource)) { + list($width, $height) = array(imagesx($resource), imagesy($resource)); + + // create transparent truecolor canvas + $truecolor = imagecreatetruecolor($width, $height); + $transparent = imagecolorallocatealpha($truecolor, 255, 255, 255, 127); + + imagefill($truecolor, 0, 0, $transparent); + imagecolortransparent($truecolor, $transparent); + + imagecopymerge($truecolor, $resource, 0, 0, 0, 0, $width, $height, 100); + + imagedestroy($resource); + $resource = $truecolor; + } + + if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) { + throw new RuntimeException('Could not set alphablending, savealpha and antialias values'); + } + + if (function_exists('imageantialias')) { + imageantialias($resource, true); + } + + return new Image($resource, $palette, $metadata); + } + + private function loadGdInfo() + { + if (!function_exists('gd_info')) { + throw new RuntimeException('Gd not installed'); + } + + $this->info = gd_info(); + } + + private function requireGdVersion($version) + { + if (version_compare(GD_VERSION, $version, '<')) { + throw new RuntimeException(sprintf('GD2 version %s or higher is required, %s provided', $version, GD_VERSION)); + } + } + + private function doLoad($string, MetadataBag $metadata) + { + $resource = @imagecreatefromstring($string); + + if (!is_resource($resource)) { + throw new RuntimeException('An image could not be created from the given input'); + } + + return $this->wrap($resource, new RGB(), $metadata); + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Drawer.php b/src/Symfony/Component/Image/Gmagick/Drawer.php new file mode 100644 index 0000000000000..327d880639bda --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Drawer.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Draw\DrawerInterface; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\NotSupportedException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Drawer implementation using the Gmagick PHP extension + */ +final class Drawer implements DrawerInterface +{ + /** + * @var \Gmagick + */ + private $gmagick; + + /** + * @param \Gmagick $gmagick + */ + public function __construct(\Gmagick $gmagick) + { + $this->gmagick = $gmagick; + } + + /** + * {@inheritdoc} + */ + public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) + { + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $arc = new \GmagickDraw(); + + $arc->setstrokecolor($pixel); + $arc->setstrokewidth(max(1, (int) $thickness)); + $arc->setfillcolor('transparent'); + $arc->arc( + $x - $width / 2, + $y - $height / 2, + $x + $width / 2, + $y + $height / 2, + $start, + $end + ); + + $this->gmagick->drawImage($arc); + + $pixel = null; + + $arc = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $chord = new \GmagickDraw(); + + $chord->setstrokecolor($pixel); + $chord->setstrokewidth(max(1, (int) $thickness)); + + if ($fill) { + $chord->setfillcolor($pixel); + } else { + $x1 = round($x + $width / 2 * cos(deg2rad($start))); + $y1 = round($y + $height / 2 * sin(deg2rad($start))); + $x2 = round($x + $width / 2 * cos(deg2rad($end))); + $y2 = round($y + $height / 2 * sin(deg2rad($end))); + + $this->line(new Point($x1, $y1), new Point($x2, $y2), $color); + + $chord->setfillcolor('transparent'); + } + + $chord->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end); + + $this->gmagick->drawImage($chord); + + $pixel = null; + + $chord = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $ellipse = new \GmagickDraw(); + + $ellipse->setstrokecolor($pixel); + $ellipse->setstrokewidth(max(1, (int) $thickness)); + + if ($fill) { + $ellipse->setfillcolor($pixel); + } else { + $ellipse->setfillcolor('transparent'); + } + + $ellipse->ellipse( + $center->getX(), + $center->getY(), + $width / 2, + $height / 2, + 0, 360 + ); + + $this->gmagick->drawImage($ellipse); + + $pixel = null; + + $ellipse = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1) + { + try { + $pixel = $this->getColor($color); + $line = new \GmagickDraw(); + + $line->setstrokecolor($pixel); + $line->setstrokewidth(max(1, (int) $thickness)); + $line->setfillcolor($pixel); + $line->line( + $start->getX(), + $start->getY(), + $end->getX(), + $end->getY() + ); + + $this->gmagick->drawImage($line); + + $pixel = null; + + $line = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Draw line operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start))); + $y1 = round($center->getY() + $height / 2 * sin(deg2rad($start))); + $x2 = round($center->getX() + $width / 2 * cos(deg2rad($end))); + $y2 = round($center->getY() + $height / 2 * sin(deg2rad($end))); + + if ($fill) { + $this->chord($center, $size, $start, $end, $color, true, $thickness); + $this->polygon( + array( + $center, + new Point($x1, $y1), + new Point($x2, $y2), + ), + $color, + true, + $thickness + ); + } else { + $this->arc($center, $size, $start, $end, $color, $thickness); + $this->line($center, new Point($x1, $y1), $color, $thickness); + $this->line($center, new Point($x2, $y2), $color, $thickness); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function dot(PointInterface $position, ColorInterface $color) + { + $x = $position->getX(); + $y = $position->getY(); + + try { + $pixel = $this->getColor($color); + $point = new \GmagickDraw(); + + $point->setfillcolor($pixel); + $point->point($x, $y); + + $this->gmagick->drawimage($point); + + $pixel = null; + $point = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Draw point operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1) + { + if (count($coordinates) < 3) { + throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates))); + } + + $points = array_map(function (PointInterface $p) { + return array('x' => $p->getX(), 'y' => $p->getY()); + }, $coordinates); + + try { + $pixel = $this->getColor($color); + $polygon = new \GmagickDraw(); + + $polygon->setstrokecolor($pixel); + $polygon->setstrokewidth(max(1, (int) $thickness)); + + if ($fill) { + $polygon->setfillcolor($pixel); + } else { + $polygon->setfillcolor('transparent'); + } + + $polygon->polygon($points); + + $this->gmagick->drawImage($polygon); + + unset($pixel, $polygon); + } catch (\GmagickException $e) { + throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null) + { + try { + $pixel = $this->getColor($font->getColor()); + $text = new \GmagickDraw(); + + $text->setfont($font->getFile()); + /** + * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027 + * + * ensure font resolution is the same as GD's hard-coded 96 + */ + $text->setfontsize((int) ($font->getSize() * (96 / 72))); + $text->setfillcolor($pixel); + + $info = $this->gmagick->queryfontmetrics($text, $string); + $rad = deg2rad($angle); + $cos = cos($rad); + $sin = sin($rad); + + $x1 = round(0 * $cos - 0 * $sin); + $x2 = round($info['textWidth'] * $cos - $info['textHeight'] * $sin); + $y1 = round(0 * $sin + 0 * $cos); + $y2 = round($info['textWidth'] * $sin + $info['textHeight'] * $cos); + + $xdiff = 0 - min($x1, $x2); + $ydiff = 0 - min($y1, $y2); + + if ($width !== null) { + throw new NotSupportedException('Gmagick doesn\'t support queryfontmetrics function for multiline text', 1); + } + + $this->gmagick->annotateimage($text, $position->getX() + $x1 + $xdiff, $position->getY() + $y2 + $ydiff, $angle, $string); + + unset($pixel, $text); + } catch (\GmagickException $e) { + throw new RuntimeException('Draw text operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * Gets specifically formatted color string from Color instance + * + * @param ColorInterface $color + * + * @return \GmagickPixel + * + * @throws InvalidArgumentException In case a non-opaque color is passed + */ + private function getColor(ColorInterface $color) + { + if (!$color->isOpaque()) { + throw new InvalidArgumentException('Gmagick doesn\'t support transparency'); + } + + return new \GmagickPixel((string) $color); + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Effects.php b/src/Symfony/Component/Image/Gmagick/Effects.php new file mode 100644 index 0000000000000..488a00b560a0a --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Effects.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Effects\EffectsInterface; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Exception\NotSupportedException; + +/** + * Effects implementation using the Gmagick PHP extension + */ +class Effects implements EffectsInterface +{ + private $gmagick; + + public function __construct(\Gmagick $gmagick) + { + $this->gmagick = $gmagick; + } + + /** + * {@inheritdoc} + */ + public function gamma($correction) + { + try { + $this->gmagick->gammaimage($correction); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function negative() + { + if (!method_exists($this->gmagick, 'negateimage')) { + throw new NotSupportedException('Gmagick version 1.1.0 RC3 is required for negative effect'); + } + + try { + $this->gmagick->negateimage(false, \Gmagick::CHANNEL_ALL); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to negate the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + try { + $this->gmagick->setImageType(2); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function colorize(ColorInterface $color) + { + throw new NotSupportedException('Gmagick does not support colorize'); + } + + /** + * {@inheritdoc} + */ + public function sharpen() + { + throw new NotSupportedException('Gmagick does not support sharpen yet'); + } + + /** + * {@inheritdoc} + */ + public function blur($sigma = 1) + { + try { + $this->gmagick->blurImage(0, $sigma); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to blur the image', $e->getCode(), $e); + } + + return $this; + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Font.php b/src/Symfony/Component/Image/Gmagick/Font.php new file mode 100644 index 0000000000000..7641426bbf185 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Font.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +/** + * Font implementation using the Gmagick PHP extension + */ +final class Font extends AbstractFont +{ + /** + * @var \Gmagick + */ + private $gmagick; + + /** + * @param \Gmagick $gmagick + * @param string $file + * @param integer $size + * @param ColorInterface $color + */ + public function __construct(\Gmagick $gmagick, $file, $size, ColorInterface $color) + { + $this->gmagick = $gmagick; + + parent::__construct($file, $size, $color); + } + + /** + * {@inheritdoc} + */ + public function box($string, $angle = 0) + { + $text = new \GmagickDraw(); + + $text->setfont($this->file); + /** + * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027 + * + * ensure font resolution is the same as GD's hard-coded 96 + */ + $text->setfontsize((int) ($this->size * (96 / 72))); + $text->setfontstyle(\Gmagick::STYLE_OBLIQUE); + + $info = $this->gmagick->queryfontmetrics($text, $string); + + $box = new Box($info['textWidth'], $info['textHeight']); + + return $box; + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Image.php b/src/Symfony/Component/Image/Gmagick/Image.php new file mode 100644 index 0000000000000..72ff395c92298 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Image.php @@ -0,0 +1,790 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractImage; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Image\ProfileInterface; + +/** + * Image implementation using the Gmagick PHP extension + */ +final class Image extends AbstractImage +{ + /** + * @var \Gmagick + */ + private $gmagick; + /** + * @var Layers + */ + private $layers; + + /** + * @var PaletteInterface + */ + private $palette; + + private static $colorspaceMapping = array( + PaletteInterface::PALETTE_CMYK => \Gmagick::COLORSPACE_CMYK, + PaletteInterface::PALETTE_RGB => \Gmagick::COLORSPACE_RGB, + ); + + /** + * Constructs a new Image instance + * + * @param \Gmagick $gmagick + * @param PaletteInterface $palette + * @param MetadataBag $metadata + */ + public function __construct(\Gmagick $gmagick, PaletteInterface $palette, MetadataBag $metadata) + { + $this->metadata = $metadata; + $this->gmagick = $gmagick; + $this->setColorspace($palette); + $this->layers = new Layers($this, $this->palette, $this->gmagick); + } + + /** + * Destroys allocated gmagick resources + */ + public function __destruct() + { + if ($this->gmagick instanceof \Gmagick) { + $this->gmagick->clear(); + $this->gmagick->destroy(); + } + } + + /** + * Returns gmagick instance + * + * @return \Gmagick + */ + public function getGmagick() + { + return $this->gmagick; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function copy() + { + return new self(clone $this->gmagick, $this->palette, clone $this->metadata); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function crop(PointInterface $start, BoxInterface $size) + { + if (!$start->in($this->getSize())) { + throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders'); + } + + try { + $this->gmagick->cropimage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY()); + } catch (\GmagickException $e) { + throw new RuntimeException('Crop operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function flipHorizontally() + { + try { + $this->gmagick->flopimage(); + } catch (\GmagickException $e) { + throw new RuntimeException('Horizontal flip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function flipVertically() + { + try { + $this->gmagick->flipimage(); + } catch (\GmagickException $e) { + throw new RuntimeException('Vertical flip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function strip() + { + try { + try { + $this->profile($this->palette->profile()); + } catch (\Exception $e) { + // here we discard setting the profile as the previous incorporated profile + // is corrupted, let's now strip the image + } + $this->gmagick->stripimage(); + } catch (\GmagickException $e) { + throw new RuntimeException('Strip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function paste(ImageInterface $image, PointInterface $start) + { + if (!$image instanceof self) { + throw new InvalidArgumentException(sprintf('Gmagick\Image can only paste() Gmagick\Image instances, %s given', get_class($image))); + } + + if (!$this->getSize()->contains($image->getSize(), $start)) { + throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box'); + } + + try { + $this->gmagick->compositeimage($image->gmagick, \Gmagick::COMPOSITE_DEFAULT, $start->getX(), $start->getY()); + } catch (\GmagickException $e) { + throw new RuntimeException('Paste operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED) + { + static $supportedFilters = array( + ImageInterface::FILTER_UNDEFINED => \Gmagick::FILTER_UNDEFINED, + ImageInterface::FILTER_BESSEL => \Gmagick::FILTER_BESSEL, + ImageInterface::FILTER_BLACKMAN => \Gmagick::FILTER_BLACKMAN, + ImageInterface::FILTER_BOX => \Gmagick::FILTER_BOX, + ImageInterface::FILTER_CATROM => \Gmagick::FILTER_CATROM, + ImageInterface::FILTER_CUBIC => \Gmagick::FILTER_CUBIC, + ImageInterface::FILTER_GAUSSIAN => \Gmagick::FILTER_GAUSSIAN, + ImageInterface::FILTER_HANNING => \Gmagick::FILTER_HANNING, + ImageInterface::FILTER_HAMMING => \Gmagick::FILTER_HAMMING, + ImageInterface::FILTER_HERMITE => \Gmagick::FILTER_HERMITE, + ImageInterface::FILTER_LANCZOS => \Gmagick::FILTER_LANCZOS, + ImageInterface::FILTER_MITCHELL => \Gmagick::FILTER_MITCHELL, + ImageInterface::FILTER_POINT => \Gmagick::FILTER_POINT, + ImageInterface::FILTER_QUADRATIC => \Gmagick::FILTER_QUADRATIC, + ImageInterface::FILTER_SINC => \Gmagick::FILTER_SINC, + ImageInterface::FILTER_TRIANGLE => \Gmagick::FILTER_TRIANGLE + ); + + if (!array_key_exists($filter, $supportedFilters)) { + throw new InvalidArgumentException('Unsupported filter type'); + } + + try { + $this->gmagick->resizeimage($size->getWidth(), $size->getHeight(), $supportedFilters[$filter], 1); + } catch (\GmagickException $e) { + throw new RuntimeException('Resize operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function rotate($angle, ColorInterface $background = null) + { + try { + $background = $background ?: $this->palette->color('fff'); + $pixel = $this->getColor($background); + + $this->gmagick->rotateimage($pixel, $angle); + + unset($pixel); + } catch (\GmagickException $e) { + throw new RuntimeException('Rotate operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * Internal + * + * Applies options before save or output + * + * @param \Gmagick $image + * @param array $options + * @param string $path + * + * @throws InvalidArgumentException + */ + private function applyImageOptions(\Gmagick $image, array $options, $path) + { + if (isset($options['format'])) { + $format = $options['format']; + } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) { + $format = $extension; + } else { + $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION); + } + + $format = strtolower($format); + + $options = $this->updateSaveOptions($options); + + if (isset($options['jpeg_quality']) && in_array($format, array('jpeg', 'jpg', 'pjpeg'))) { + $image->setCompressionQuality($options['jpeg_quality']); + } + + if ((isset($options['png_compression_level']) || isset($options['png_compression_filter'])) && $format === 'png') { + // first digit: compression level (default: 7) + if (isset($options['png_compression_level'])) { + if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) { + throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9'); + } + $compression = $options['png_compression_level'] * 10; + } else { + $compression = 70; + } + + // second digit: compression filter (default: 5) + if (isset($options['png_compression_filter'])) { + if ($options['png_compression_filter'] < 0 || $options['png_compression_filter'] > 9) { + throw new InvalidArgumentException('png_compression_filter option should be an integer from 0 to 9'); + } + $compression += $options['png_compression_filter']; + } else { + $compression += 5; + } + + $image->setCompressionQuality($compression); + } + + if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) { + if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) { + $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERCENTIMETER); + } elseif ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERINCH) { + $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERINCH); + } else { + throw new InvalidArgumentException('Unsupported image unit format'); + } + + $image->setimageresolution($options['resolution-x'], $options['resolution-y']); + } + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function save($path = null, array $options = array()) + { + $path = null === $path ? $this->gmagick->getImageFilename() : $path; + + if ('' === trim($path)) { + throw new RuntimeException('You can omit save path only if image has been open from a file'); + } + + try { + $this->prepareOutput($options, $path); + $allFrames = !isset($options['animated']) || false === $options['animated']; + $this->gmagick->writeimage($path, $allFrames); + } catch (\GmagickException $e) { + throw new RuntimeException('Save operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function show($format, array $options = array()) + { + header('Content-type: '.$this->getMimeType($format)); + echo $this->get($format, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($format, array $options = array()) + { + try { + $options['format'] = $format; + $this->prepareOutput($options); + } catch (\GmagickException $e) { + throw new RuntimeException('Get operation failed', $e->getCode(), $e); + } + + return $this->gmagick->getimagesblob(); + } + + /** + * @param array $options + * @param string $path + */ + private function prepareOutput(array $options, $path = null) + { + if (isset($options['format'])) { + $this->gmagick->setimageformat($options['format']); + } + + if (isset($options['animated']) && true === $options['animated']) { + $format = isset($options['format']) ? $options['format'] : 'gif'; + $delay = isset($options['animated.delay']) ? $options['animated.delay'] : null; + $loops = isset($options['animated.loops']) ? $options['animated.loops'] : 0; + + $options['flatten'] = false; + + $this->layers->animate($format, $delay, $loops); + } else { + $this->layers->merge(); + } + $this->applyImageOptions($this->gmagick, $options, $path); + + // flatten only if image has multiple layers + if ((!isset($options['flatten']) || $options['flatten'] === true) && count($this->layers) > 1) { + $this->flatten(); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->get('png'); + } + + /** + * {@inheritdoc} + */ + public function draw() + { + return new Drawer($this->gmagick); + } + + /** + * {@inheritdoc} + */ + public function effects() + { + return new Effects($this->gmagick); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + try { + $i = $this->gmagick->getimageindex(); + $this->gmagick->setimageindex(0); //rewind + $width = $this->gmagick->getimagewidth(); + $height = $this->gmagick->getimageheight(); + $this->gmagick->setimageindex($i); + } catch (\GmagickException $e) { + throw new RuntimeException('Get size operation failed', $e->getCode(), $e); + } + + return new Box($width, $height); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function applyMask(ImageInterface $mask) + { + if (!$mask instanceof self) { + throw new InvalidArgumentException('Can only apply instances of Symfony\Component\Image\Gmagick\Image as masks'); + } + + $size = $this->getSize(); + $maskSize = $mask->getSize(); + + if ($size != $maskSize) { + throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size)); + } + + try { + $mask = $mask->copy(); + $this->gmagick->compositeimage($mask->gmagick, \Gmagick::COMPOSITE_DEFAULT, 0, 0); + } catch (\GmagickException $e) { + throw new RuntimeException('Apply mask operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function mask() + { + $mask = $this->copy(); + + try { + $mask->gmagick->modulateimage(100, 0, 100); + } catch (\GmagickException $e) { + throw new RuntimeException('Mask operation failed', $e->getCode(), $e); + } + + return $mask; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function fill(FillInterface $fill) + { + try { + $draw = new \GmagickDraw(); + $size = $this->getSize(); + + $w = $size->getWidth(); + $h = $size->getHeight(); + + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $pixel = $this->getColor($fill->getColor(new Point($x, $y))); + + $draw->setfillcolor($pixel); + $draw->point($x, $y); + + $pixel = null; + } + } + + $this->gmagick->drawimage($draw); + + $draw = null; + } catch (\GmagickException $e) { + throw new RuntimeException('Fill operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function histogram() + { + try { + $pixels = $this->gmagick->getimagehistogram(); + } catch (\GmagickException $e) { + throw new RuntimeException('Error while fetching histogram', $e->getCode(), $e); + } + + $image = $this; + + return array_map(function (\GmagickPixel $pixel) use ($image) { + return $image->pixelToColor($pixel); + }, $pixels); + } + + /** + * {@inheritdoc} + */ + public function getColorAt(PointInterface $point) + { + if (!$point->in($this->getSize())) { + throw new InvalidArgumentException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight())); + } + + try { + $cropped = clone $this->gmagick; + $histogram = $cropped + ->cropImage(1, 1, $point->getX(), $point->getY()) + ->getImageHistogram(); + } catch (\GmagickException $e) { + throw new RuntimeException('Unable to get the pixel', $e->getCode(), $e); + } + + $pixel = array_shift($histogram); + + unset($histogram, $cropped); + + return $this->pixelToColor($pixel); + } + + /** + * Returns a color given a pixel, depending the Palette context + * + * Note : this method is public for PHP 5.3 compatibility + * + * @param \GmagickPixel $pixel + * + * @return ColorInterface + * + * @throws InvalidArgumentException In case a unknown color is requested + */ + public function pixelToColor(\GmagickPixel $pixel) + { + static $colorMapping = array( + ColorInterface::COLOR_RED => \Gmagick::COLOR_RED, + ColorInterface::COLOR_GREEN => \Gmagick::COLOR_GREEN, + ColorInterface::COLOR_BLUE => \Gmagick::COLOR_BLUE, + ColorInterface::COLOR_CYAN => \Gmagick::COLOR_CYAN, + ColorInterface::COLOR_MAGENTA => \Gmagick::COLOR_MAGENTA, + ColorInterface::COLOR_YELLOW => \Gmagick::COLOR_YELLOW, + ColorInterface::COLOR_KEYLINE => \Gmagick::COLOR_BLACK, + // There is no gray component in \Gmagick, let's use one of the RGB comp + ColorInterface::COLOR_GRAY => \Gmagick::COLOR_RED, + ); + + if ($this->palette->supportsAlpha()) { + try { + $alpha = (int) round($pixel->getcolorvalue(\Gmagick::COLOR_ALPHA) * 100); + } catch (\GmagickPixelException $e) { + $alpha = null; + } + } else { + $alpha = null; + } + + $palette = $this->palette(); + + return $this->palette->color(array_map(function ($color) use ($palette, $pixel, $colorMapping) { + if (!isset($colorMapping[$color])) { + throw new InvalidArgumentException(sprintf('Color %s is not mapped in Gmagick', $color)); + } + $multiplier = 255; + if ($palette->name() === PaletteInterface::PALETTE_CMYK) { + $multiplier = 100; + } + + return $pixel->getcolorvalue($colorMapping[$color]) * $multiplier; + }, $this->palette->pixelDefinition()), $alpha); + } + + /** + * {@inheritdoc} + */ + public function layers() + { + return $this->layers; + } + + /** + * {@inheritdoc} + */ + public function interlace($scheme) + { + static $supportedInterlaceSchemes = array( + ImageInterface::INTERLACE_NONE => \Gmagick::INTERLACE_NO, + ImageInterface::INTERLACE_LINE => \Gmagick::INTERLACE_LINE, + ImageInterface::INTERLACE_PLANE => \Gmagick::INTERLACE_PLANE, + ImageInterface::INTERLACE_PARTITION => \Gmagick::INTERLACE_PARTITION, + ); + + if (!array_key_exists($scheme, $supportedInterlaceSchemes)) { + throw new InvalidArgumentException('Unsupported interlace type'); + } + + $this->gmagick->setInterlaceScheme($supportedInterlaceSchemes[$scheme]); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function usePalette(PaletteInterface $palette) + { + if (!isset(static::$colorspaceMapping[$palette->name()])) { + throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver',$palette->name())); + } + + if ($this->palette->name() === $palette->name()) { + return $this; + } + + try { + try { + $hasICCProfile = (Boolean) $this->gmagick->getimageprofile('ICM'); + } catch (\GmagickException $e) { + $hasICCProfile = false; + } + + if (!$hasICCProfile) { + $this->profile($this->palette->profile()); + } + + $this->profile($palette->profile()); + + $this->setColorspace($palette); + $this->palette = $palette; + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to set colorspace', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function palette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function profile(ProfileInterface $profile) + { + try { + $this->gmagick->profileimage('ICM', $profile->data()); + } catch (\GmagickException $e) { + if (false !== strpos($e->getMessage(), 'LCMS encoding not enabled')) { + throw new RuntimeException(sprintf('Unable to add profile %s to image, be sue to compile graphicsmagick with `--with-lcms2` option', $profile->name()), $e->getCode(), $e); + } + + throw new RuntimeException(sprintf('Unable to add profile %s to image', $profile->name()), $e->getCode(), $e); + } + + return $this; + } + + /** + * Internal + * + * Flatten the image. + */ + private function flatten() + { + /** + * @see http://pecl.php.net/bugs/bug.php?id=22435 + */ + if (method_exists($this->gmagick, 'flattenImages')) { + try { + $this->gmagick = $this->gmagick->flattenImages(); + } catch (\GmagickException $e) { + throw new RuntimeException('Flatten operation failed', $e->getCode(), $e); + } + } + } + + /** + * Gets specifically formatted color string from Color instance + * + * @param ColorInterface $color + * + * @return \GmagickPixel + * + * @throws InvalidArgumentException + */ + private function getColor(ColorInterface $color) + { + if (!$color->isOpaque()) { + throw new InvalidArgumentException('Gmagick doesn\'t support transparency'); + } + + return new \GmagickPixel((string) $color); + } + + /** + * Internal + * + * Get the mime type based on format. + * + * @param string $format + * + * @return string mime-type + * + * @throws InvalidArgumentException + */ + private function getMimeType($format) + { + static $mimeTypes = array( + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'wbmp' => 'image/vnd.wap.wbmp', + 'xbm' => 'image/xbm', + ); + + if (!isset($mimeTypes[$format])) { + throw new InvalidArgumentException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(", ", array_keys($mimeTypes)), $format)); + } + + return $mimeTypes[$format]; + } + + /** + * Sets colorspace and image type, assigns the palette. + * + * @param PaletteInterface $palette + * + * @throws InvalidArgumentException + */ + private function setColorspace(PaletteInterface $palette) + { + if (!isset(static::$colorspaceMapping[$palette->name()])) { + throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver', $palette->name())); + } + + $this->gmagick->setimagecolorspace(static::$colorspaceMapping[$palette->name()]); + $this->palette = $palette; + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Layers.php b/src/Symfony/Component/Image/Gmagick/Layers.php new file mode 100644 index 0000000000000..448f213f975f3 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Layers.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Image\AbstractLayers; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\NotSupportedException; +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\PaletteInterface; + +class Layers extends AbstractLayers +{ + /** + * @var Image + */ + private $image; + + /** + * @var \Gmagick + */ + private $resource; + + /** + * @var integer + */ + private $offset = 0; + + /** + * @var array + */ + private $layers = array(); + + /** + * @var PaletteInterface + */ + private $palette; + + public function __construct(Image $image, PaletteInterface $palette, \Gmagick $resource) + { + $this->image = $image; + $this->resource = $resource; + $this->palette = $palette; + } + + /** + * {@inheritdoc} + */ + public function merge() + { + foreach ($this->layers as $offset => $image) { + try { + $this->resource->setimageindex($offset); + $this->resource->setimage($image->getGmagick()); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to substitute layer', $e->getCode(), $e); + } + } + } + + /** + * {@inheritdoc} + */ + public function coalesce() + { + throw new NotSupportedException('Gmagick does not support coalescing'); + } + + /** + * {@inheritdoc} + */ + public function animate($format, $delay, $loops) + { + if ('gif' !== strtolower($format)) { + throw new NotSupportedException('Animated picture is currently only supported on gif'); + } + + if (!is_int($loops) || $loops < 0) { + throw new InvalidArgumentException('Loops must be a positive integer.'); + } + + if (null !== $delay && (!is_int($delay) || $delay < 0)) { + throw new InvalidArgumentException('Delay must be either null or a positive integer.'); + } + + try { + foreach ($this as $offset => $layer) { + $this->resource->setimageindex($offset); + $this->resource->setimageformat($format); + + if (null !== $delay) { + $this->resource->setimagedelay($delay / 10); + } + + $this->resource->setimageiterations($loops); + } + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to animate layers', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function current() + { + return $this->extractAt($this->offset); + } + + /** + * Tries to extract layer at given offset + * + * @param integer $offset + * @return Image + * @throws RuntimeException + */ + private function extractAt($offset) + { + if (!isset($this->layers[$offset])) { + try { + $this->resource->setimageindex($offset); + $this->layers[$offset] = new Image($this->resource->getimage(), $this->palette, new MetadataBag()); + } catch (\GmagickException $e) { + throw new RuntimeException(sprintf('Failed to extract layer %d', $offset), $e->getCode(), $e); + } + } + + return $this->layers[$offset]; + } + + /** + * {@inheritdoc} + */ + public function key() + { + return $this->offset; + } + + /** + * {@inheritdoc} + */ + public function next() + { + ++$this->offset; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->offset = 0; + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return $this->offset < count($this); + } + + /** + * {@inheritdoc} + */ + public function count() + { + try { + return $this->resource->getnumberimages(); + } catch (\GmagickException $e) { + throw new RuntimeException('Failed to count the number of layers', $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return is_int($offset) && $offset >= 0 && $offset < count($this); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->extractAt($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $image) + { + if (!$image instanceof Image) { + throw new InvalidArgumentException('Only a Gmagick Image can be used as layer'); + } + + if (null === $offset) { + $offset = count($this) - 1; + } else { + if (!is_int($offset)) { + throw new InvalidArgumentException('Invalid offset for layer, it must be an integer'); + } + + if (count($this) < $offset || 0 > $offset) { + throw new OutOfBoundsException(sprintf('Invalid offset for layer, it must be a value between 0 and %d, %d given', count($this), $offset)); + } + + if (isset($this[$offset])) { + unset($this[$offset]); + $offset = $offset - 1; + } + } + + $frame = $image->getGmagick(); + + try { + if (count($this) > 0) { + $this->resource->setimageindex($offset); + $this->resource->nextimage(); + } + $this->resource->addimage($frame); + + /** + * ugly hack to bypass issue https://bugs.php.net/bug.php?id=64623 + */ + if (count($this) == 2) { + $this->resource->setimageindex($offset+1); + $this->resource->nextimage(); + $this->resource->addimage($frame); + unset($this[0]); + } + } catch (\GmagickException $e) { + throw new RuntimeException('Unable to set the layer', $e->getCode(), $e); + } + + $this->layers = array(); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + try { + $this->extractAt($offset); + } catch (RuntimeException $e) { + return; + } + + try { + $this->resource->setimageindex($offset); + $this->resource->removeimage(); + } catch (\GmagickException $e) { + throw new RuntimeException('Unable to remove layer', $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/Image/Gmagick/Loader.php b/src/Symfony/Component/Image/Gmagick/Loader.php new file mode 100644 index 0000000000000..d12b09e827051 --- /dev/null +++ b/src/Symfony/Component/Image/Gmagick/Loader.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Gmagick; + +use Symfony\Component\Image\Image\AbstractLoader; +use Symfony\Component\Image\Exception\NotSupportedException; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\Grayscale; +use Symfony\Component\Image\Image\Palette\CMYK; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; + +/** + * Loader implementation using the Gmagick PHP extension + */ +class Loader extends AbstractLoader +{ + /** + * @throws RuntimeException + */ + public function __construct() + { + if (!class_exists('Gmagick')) { + throw new RuntimeException('Gmagick not installed'); + } + } + + /** + * {@inheritdoc} + */ + public function open($path) + { + $path = $this->checkPath($path); + + try { + $gmagick = new \Gmagick($path); + $image = new Image($gmagick, $this->createPalette($gmagick), $this->getMetadataReader()->readFile($path)); + } catch (\GmagickException $e) { + throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e); + } + + return $image; + } + + /** + * {@inheritdoc} + */ + public function create(BoxInterface $size, ColorInterface $color = null) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + $palette = null !== $color ? $color->getPalette() : new RGB(); + $color = null !== $color ? $color : $palette->color('fff'); + + try { + $gmagick = new \Gmagick(); + // Gmagick does not support creation of CMYK GmagickPixel + // see https://bugs.php.net/bug.php?id=64466 + if ($color instanceof CMYKColor) { + $switchPalette = $palette; + $palette = new RGB(); + $pixel = new \GmagickPixel($palette->color((string) $color)); + } else { + $switchPalette = null; + $pixel = new \GmagickPixel((string) $color); + } + + if ($color->getPalette()->supportsAlpha() && $color->getAlpha() < 100) { + throw new NotSupportedException('alpha transparency is not supported'); + } + + $gmagick->newimage($width, $height, $pixel->getcolor(false)); + $gmagick->setimagecolorspace(\Gmagick::COLORSPACE_TRANSPARENT); + $gmagick->setimagebackgroundcolor($pixel); + + $image = new Image($gmagick, $palette, new MetadataBag()); + + if ($switchPalette) { + $image->usePalette($switchPalette); + } + + return $image; + } catch (\GmagickException $e) { + throw new RuntimeException('Could not create empty image', $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function load($string) + { + return $this->doLoad($string, $this->getMetadataReader()->readData($string)); + } + + /** + * {@inheritdoc} + */ + public function read($resource) + { + if (!is_resource($resource)) { + throw new InvalidArgumentException('Variable does not contain a stream resource'); + } + + $content = stream_get_contents($resource); + + if (false === $content) { + throw new InvalidArgumentException('Couldn\'t read given resource'); + } + + return $this->doLoad($content, $this->getMetadataReader()->readData($content, $resource)); + } + + /** + * {@inheritdoc} + */ + public function font($file, $size, ColorInterface $color) + { + $gmagick = new \Gmagick(); + $gmagick->newimage(1, 1, 'transparent'); + + return new Font($gmagick, $file, $size, $color); + } + + private function createPalette(\Gmagick $gmagick) + { + switch ($gmagick->getimagecolorspace()) { + case \Gmagick::COLORSPACE_SRGB: + case \Gmagick::COLORSPACE_RGB: + return new RGB(); + case \Gmagick::COLORSPACE_CMYK: + return new CMYK(); + case \Gmagick::COLORSPACE_GRAY: + return new Grayscale(); + default: + throw new NotSupportedException('Only RGB and CMYK colorspace are currently supported'); + } + } + + private function doLoad($content, MetadataBag $metadata) + { + try { + $gmagick = new \Gmagick(); + $gmagick->readimageblob($content); + } catch (\GmagickException $e) { + throw new RuntimeException('Could not load image from string', $e->getCode(), $e); + } + + return new Image($gmagick, $this->createPalette($gmagick), $metadata); + } +} diff --git a/src/Symfony/Component/Image/Image/AbstractFont.php b/src/Symfony/Component/Image/Image/AbstractFont.php new file mode 100644 index 0000000000000..d5c057652a4d2 --- /dev/null +++ b/src/Symfony/Component/Image/Image/AbstractFont.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +/** + * Abstract font base class + */ +abstract class AbstractFont implements FontInterface +{ + /** + * @var string + */ + protected $file; + + /** + * @var integer + */ + protected $size; + + /** + * @var ColorInterface + */ + protected $color; + + /** + * Constructs a font with specified $file, $size and $color + * + * The font size is to be specified in points (e.g. 10pt means 10) + * + * @param string $file + * @param integer $size + * @param ColorInterface $color + */ + public function __construct($file, $size, ColorInterface $color) + { + $this->file = $file; + $this->size = $size; + $this->color = $color; + } + + /** + * {@inheritdoc} + */ + final public function getFile() + { + return $this->file; + } + + /** + * {@inheritdoc} + */ + final public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + */ + final public function getColor() + { + return $this->color; + } +} diff --git a/src/Symfony/Component/Image/Image/AbstractImage.php b/src/Symfony/Component/Image/Image/AbstractImage.php new file mode 100644 index 0000000000000..d01bd679a4575 --- /dev/null +++ b/src/Symfony/Component/Image/Image/AbstractImage.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Image\Metadata\MetadataBag; + +abstract class AbstractImage implements ImageInterface +{ + /** + * @var MetadataBag + */ + protected $metadata; + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED) + { + if ($mode !== ImageInterface::THUMBNAIL_INSET && + $mode !== ImageInterface::THUMBNAIL_OUTBOUND) { + throw new InvalidArgumentException('Invalid mode specified'); + } + + $imageSize = $this->getSize(); + $ratios = array( + $size->getWidth() / $imageSize->getWidth(), + $size->getHeight() / $imageSize->getHeight() + ); + + $thumbnail = $this->copy(); + + $thumbnail->usePalette($this->palette()); + $thumbnail->strip(); + // if target width is larger than image width + // AND target height is longer than image height + if ($size->contains($imageSize)) { + return $thumbnail; + } + + if ($mode === ImageInterface::THUMBNAIL_INSET) { + $ratio = min($ratios); + } else { + $ratio = max($ratios); + } + + if ($mode === ImageInterface::THUMBNAIL_OUTBOUND) { + if (!$imageSize->contains($size)) { + $size = new Box( + min($imageSize->getWidth(), $size->getWidth()), + min($imageSize->getHeight(), $size->getHeight()) + ); + } else { + $imageSize = $thumbnail->getSize()->scale($ratio); + $thumbnail->resize($imageSize, $filter); + } + $thumbnail->crop(new Point( + max(0, round(($imageSize->getWidth() - $size->getWidth()) / 2)), + max(0, round(($imageSize->getHeight() - $size->getHeight()) / 2)) + ), $size); + } else { + if (!$imageSize->contains($size)) { + $imageSize = $imageSize->scale($ratio); + $thumbnail->resize($imageSize, $filter); + } else { + $imageSize = $thumbnail->getSize()->scale($ratio); + $thumbnail->resize($imageSize, $filter); + } + } + + return $thumbnail; + } + + /** + * Updates a given array of save options for backward compatibility with legacy names + * + * @param array $options + * + * @return array + */ + protected function updateSaveOptions(array $options) + { + // Preserve BC until version 1.0 + if (isset($options['quality']) && !isset($options['jpeg_quality'])) { + $options['jpeg_quality'] = $options['quality']; + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function metadata() + { + return $this->metadata; + } + + /** + * Assures the metadata instance will be cloned, too + */ + public function __clone() + { + if ($this->metadata !== null) { + $this->metadata = clone $this->metadata; + } + } + +} diff --git a/src/Symfony/Component/Image/Image/AbstractLayers.php b/src/Symfony/Component/Image/Image/AbstractLayers.php new file mode 100644 index 0000000000000..936e7421cf1c3 --- /dev/null +++ b/src/Symfony/Component/Image/Image/AbstractLayers.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +abstract class AbstractLayers implements LayersInterface +{ + /** + * {@inheritdoc} + */ + public function add(ImageInterface $image) + { + $this[] = $image; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function set($offset, ImageInterface $image) + { + $this[$offset] = $image; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function remove($offset) + { + unset($this[$offset]); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($offset) + { + return $this[$offset]; + } + + /** + * {@inheritdoc} + */ + public function has($offset) + { + return isset($this[$offset]); + } +} diff --git a/src/Symfony/Component/Image/Image/AbstractLoader.php b/src/Symfony/Component/Image/Image/AbstractLoader.php new file mode 100644 index 0000000000000..4006aff865d53 --- /dev/null +++ b/src/Symfony/Component/Image/Image/AbstractLoader.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Image\Metadata\DefaultMetadataReader; +use Symfony\Component\Image\Image\Metadata\ExifMetadataReader; +use Symfony\Component\Image\Image\Metadata\MetadataReaderInterface; +use Symfony\Component\Image\Exception\InvalidArgumentException; + +abstract class AbstractLoader implements LoaderInterface +{ + /** @var MetadataReaderInterface */ + private $metadataReader; + + /** + * @param MetadataReaderInterface $metadataReader + * + * @return LoaderInterface + */ + public function setMetadataReader(MetadataReaderInterface $metadataReader) + { + $this->metadataReader = $metadataReader; + + return $this; + } + + /** + * @return MetadataReaderInterface + */ + public function getMetadataReader() + { + if (null === $this->metadataReader) { + if (ExifMetadataReader::isSupported()) { + $this->metadataReader = new ExifMetadataReader(); + } else { + $this->metadataReader = new DefaultMetadataReader(); + } + } + + return $this->metadataReader; + } + + /** + * Checks a path that could be used with LoaderInterface::open and returns + * a proper string. + * + * @param string|object $path + * + * @return string + * + * @throws InvalidArgumentException In case the given path is invalid. + */ + protected function checkPath($path) + { + // provide compatibility with objects such as \SplFileInfo + if (is_object($path) && method_exists($path, '__toString')) { + $path = (string) $path; + } + + $handle = @fopen($path, 'r'); + + if (false === $handle) { + throw new InvalidArgumentException(sprintf('File %s does not exist.', $path)); + } + + fclose($handle); + + return $path; + } +} diff --git a/src/Symfony/Component/Image/Image/Box.php b/src/Symfony/Component/Image/Image/Box.php new file mode 100644 index 0000000000000..2872e48a6f670 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Box.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +/** + * A box implementation + */ +final class Box implements BoxInterface +{ + /** + * @var integer + */ + private $width; + + /** + * @var integer + */ + private $height; + + /** + * Constructs the Size with given width and height + * + * @param integer $width + * @param integer $height + * + * @throws InvalidArgumentException + */ + public function __construct($width, $height) + { + if ($height < 1 || $width < 1) { + throw new InvalidArgumentException(sprintf('Length of either side cannot be 0 or negative, current size is %sx%s', $width, $height)); + } + + $this->width = (int) $width; + $this->height = (int) $height; + } + + /** + * {@inheritdoc} + */ + public function getWidth() + { + return $this->width; + } + + /** + * {@inheritdoc} + */ + public function getHeight() + { + return $this->height; + } + + /** + * {@inheritdoc} + */ + public function scale($ratio) + { + return new Box(round($ratio * $this->width), round($ratio * $this->height)); + } + + /** + * {@inheritdoc} + */ + public function increase($size) + { + return new Box((int) $size + $this->width, (int) $size + $this->height); + } + + /** + * {@inheritdoc} + */ + public function contains(BoxInterface $box, PointInterface $start = null) + { + $start = $start ? $start : new Point(0, 0); + + return $start->in($this) && $this->width >= $box->getWidth() + $start->getX() && $this->height >= $box->getHeight() + $start->getY(); + } + + /** + * {@inheritdoc} + */ + public function square() + { + return $this->width * $this->height; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return sprintf('%dx%d px', $this->width, $this->height); + } + + /** + * {@inheritdoc} + */ + public function widen($width) + { + return $this->scale($width / $this->width); + } + + /** + * {@inheritdoc} + */ + public function heighten($height) + { + return $this->scale($height / $this->height); + } +} diff --git a/src/Symfony/Component/Image/Image/BoxInterface.php b/src/Symfony/Component/Image/Image/BoxInterface.php new file mode 100644 index 0000000000000..0fde68689da20 --- /dev/null +++ b/src/Symfony/Component/Image/Image/BoxInterface.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +/** + * Interface for a box + */ +interface BoxInterface +{ + /** + * Gets current image height + * + * @return integer + */ + public function getHeight(); + + /** + * Gets current image width + * + * @return integer + */ + public function getWidth(); + + /** + * Creates new BoxInterface instance with ratios applied to both sides + * + * @param float $ratio + * + * @return BoxInterface + */ + public function scale($ratio); + + /** + * Creates new BoxInterface, adding given size to both sides + * + * @param integer $size + * + * @return BoxInterface + */ + public function increase($size); + + /** + * Checks whether current box can fit given box at a given start position, + * start position defaults to top left corner xy(0,0) + * + * @param BoxInterface $box + * @param PointInterface $start + * + * @return Boolean + */ + public function contains(BoxInterface $box, PointInterface $start = null); + + /** + * Gets current box square, useful for getting total number of pixels in a + * given box + * + * @return integer + */ + public function square(); + + /** + * Returns a string representation of the current box + * + * @return string + */ + public function __toString(); + + /** + * Resizes box to given width, constraining proportions and returns the new box + * + * @param integer $width + * + * @return BoxInterface + */ + public function widen($width); + + /** + * Resizes box to given height, constraining proportions and returns the new box + * + * @param integer $height + * + * @return BoxInterface + */ + public function heighten($height); +} diff --git a/src/Symfony/Component/Image/Image/Fill/FillInterface.php b/src/Symfony/Component/Image/Image/Fill/FillInterface.php new file mode 100644 index 0000000000000..2ec2d32c73df6 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Fill/FillInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Fill; + +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Interface for the fill + */ +interface FillInterface +{ + /** + * Gets color of the fill for the given position + * + * @param PointInterface $position + * + * @return ColorInterface + */ + public function getColor(PointInterface $position); +} diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php new file mode 100644 index 0000000000000..68bad65ff5921 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\PointInterface; + +/** + * Horizontal gradient fill + */ +final class Horizontal extends Linear +{ + /** + * {@inheritdoc} + */ + public function getDistance(PointInterface $position) + { + return $position->getX(); + } +} diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php new file mode 100644 index 0000000000000..5335a16bc674e --- /dev/null +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Linear gradient fill + */ +abstract class Linear implements FillInterface +{ + /** + * @var integer + */ + private $length; + + /** + * @var ColorInterface + */ + private $start; + + /** + * @var ColorInterface + */ + private $end; + + /** + * Constructs a linear gradient with overall gradient length, and start and + * end shades, which default to 0 and 255 accordingly + * + * @param integer $length + * @param ColorInterface $start + * @param ColorInterface $end + */ + final public function __construct($length, ColorInterface $start, ColorInterface $end) + { + $this->length = $length; + $this->start = $start; + $this->end = $end; + } + + /** + * {@inheritdoc} + */ + final public function getColor(PointInterface $position) + { + $l = $this->getDistance($position); + + if ($l >= $this->length) { + return $this->end; + } + + if ($l < 0) { + return $this->start; + } + + return $this->start->getPalette()->blend($this->start, $this->end, $l / $this->length); + } + + /** + * @return ColorInterface + */ + final public function getStart() + { + return $this->start; + } + + /** + * @return ColorInterface + */ + final public function getEnd() + { + return $this->end; + } + + /** + * Get the distance of the position relative to the beginning of the gradient + * + * @param PointInterface $position + * + * @return integer + */ + abstract protected function getDistance(PointInterface $position); +} diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php new file mode 100644 index 0000000000000..cd422d04b3026 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\PointInterface; + +/** + * Vertical gradient fill + */ +final class Vertical extends Linear +{ + /** + * {@inheritdoc} + */ + public function getDistance(PointInterface $position) + { + return $position->getY(); + } +} diff --git a/src/Symfony/Component/Image/Image/FontInterface.php b/src/Symfony/Component/Image/Image/FontInterface.php new file mode 100644 index 0000000000000..3bb4ab41e2303 --- /dev/null +++ b/src/Symfony/Component/Image/Image/FontInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +/** + * The font interface + */ +interface FontInterface +{ + /** + * Gets the fontfile for current font + * + * @return string + */ + public function getFile(); + + /** + * Gets font's integer point size + * + * @return integer + */ + public function getSize(); + + /** + * Gets font's color + * + * @return ColorInterface + */ + public function getColor(); + + /** + * Gets BoxInterface of font size on the image based on string and angle + * + * @param string $string + * @param integer $angle + * + * @return BoxInterface + */ + public function box($string, $angle = 0); +} diff --git a/src/Symfony/Component/Image/Image/Histogram/Bucket.php b/src/Symfony/Component/Image/Image/Histogram/Bucket.php new file mode 100644 index 0000000000000..c5b63cb8fa657 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Histogram/Bucket.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Histogram; + +/** + * Bucket histogram + */ +final class Bucket implements \Countable +{ + /** + * @var Range + */ + private $range; + + /** + * @var integer + */ + private $count; + + /** + * @param Range $range + * @param integer $count + */ + public function __construct(Range $range, $count = 0) + { + $this->range = $range; + $this->count = $count; + } + + /** + * @param integer $value + */ + public function add($value) + { + if ($this->range->contains($value)) { + $this->count++; + } + } + + /** + * @return integer The number of elements in the bucket. + */ + public function count() + { + return $this->count; + } +} diff --git a/src/Symfony/Component/Image/Image/Histogram/Range.php b/src/Symfony/Component/Image/Image/Histogram/Range.php new file mode 100644 index 0000000000000..ff28ce9786902 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Histogram/Range.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Histogram; + +use Symfony\Component\Image\Exception\OutOfBoundsException; + +/** + * Range histogram + */ +final class Range +{ + /** + * @var integer + */ + private $start; + + /** + * @var integer + */ + private $end; + + /** + * @param integer $start + * @param integer $end + * + * @throws OutOfBoundsException + */ + public function __construct($start, $end) + { + if ($end <= $start) { + throw new OutOfBoundsException(sprintf('Range end cannot be bigger than start, %d %d given accordingly', $this->start, $this->end)); + } + + $this->start = $start; + $this->end = $end; + } + + /** + * @param integer $value + * + * @return Boolean + */ + public function contains($value) + { + return $value >= $this->start && $value < $this->end; + } +} diff --git a/src/Symfony/Component/Image/Image/ImageInterface.php b/src/Symfony/Component/Image/Image/ImageInterface.php new file mode 100644 index 0000000000000..562d47cdbd8b1 --- /dev/null +++ b/src/Symfony/Component/Image/Image/ImageInterface.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Draw\DrawerInterface; +use Symfony\Component\Image\Effects\EffectsInterface; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\OutOfBoundsException; + +/** + * The image interface + */ +interface ImageInterface extends ManipulatorInterface +{ + const RESOLUTION_PIXELSPERINCH = 'ppi'; + const RESOLUTION_PIXELSPERCENTIMETER = 'ppc'; + + const INTERLACE_NONE = 'none'; + const INTERLACE_LINE = 'line'; + const INTERLACE_PLANE = 'plane'; + const INTERLACE_PARTITION = 'partition'; + + const FILTER_UNDEFINED = 'undefined'; + const FILTER_POINT = 'point'; + const FILTER_BOX = 'box'; + const FILTER_TRIANGLE = 'triangle'; + const FILTER_HERMITE = 'hermite'; + const FILTER_HANNING = 'hanning'; + const FILTER_HAMMING = 'hamming'; + const FILTER_BLACKMAN = 'blackman'; + const FILTER_GAUSSIAN = 'gaussian'; + const FILTER_QUADRATIC = 'quadratic'; + const FILTER_CUBIC = 'cubic'; + const FILTER_CATROM = 'catrom'; + const FILTER_MITCHELL = 'mitchell'; + const FILTER_LANCZOS = 'lanczos'; + const FILTER_BESSEL = 'bessel'; + const FILTER_SINC = 'sinc'; + + /** + * Returns the image content as a binary string + * + * @param string $format + * @param array $options + * + * @throws RuntimeException + * + * @return string binary + */ + public function get($format, array $options = array()); + + /** + * Returns the image content as a PNG binary string + * + * @throws RuntimeException + * + * @return string binary + */ + public function __toString(); + + /** + * Instantiates and returns a DrawerInterface instance for image drawing + * + * @return DrawerInterface + */ + public function draw(); + + /** + * @return EffectsInterface + */ + public function effects(); + + /** + * Returns current image size + * + * @return BoxInterface + */ + public function getSize(); + + /** + * Transforms creates a grayscale mask from current image, returns a new + * image, while keeping the existing image unmodified + * + * @return ImageInterface + */ + public function mask(); + + /** + * Returns array of image colors as Symfony\Component\Image\Image\Palette\Color\ColorInterface instances + * + * @return array + */ + public function histogram(); + + /** + * Returns color at specified positions of current image + * + * @param PointInterface $point + * + * @throws RuntimeException + * + * @return ColorInterface + */ + public function getColorAt(PointInterface $point); + + /** + * Returns the image layers when applicable. + * + * @throws RuntimeException In case the layer can not be returned + * @throws OutOfBoundsException In case the index is not a valid value + * + * @return LayersInterface + */ + public function layers(); + + /** + * Enables or disables interlacing + * + * @param string $scheme + * + * @throws InvalidArgumentException When an unsupported Interface type is supplied + * + * @return ImageInterface + */ + public function interlace($scheme); + + /** + * Return the current color palette + * + * @return PaletteInterface + */ + public function palette(); + + /** + * Set a palette for the image. Useful to change colorspace. + * + * @param PaletteInterface $palette + * + * @return ImageInterface + * + * @throws RuntimeException + */ + public function usePalette(PaletteInterface $palette); + + /** + * Applies a color profile on the Image + * + * @param ProfileInterface $profile + * + * @return ImageInterface + * + * @throws RuntimeException + */ + public function profile(ProfileInterface $profile); + + /** + * Returns the Image's meta data + * + * @return Metadata\MetadataBag + */ + public function metadata(); +} diff --git a/src/Symfony/Component/Image/Image/LayersInterface.php b/src/Symfony/Component/Image/Image/LayersInterface.php new file mode 100644 index 0000000000000..991d86adbe631 --- /dev/null +++ b/src/Symfony/Component/Image/Image/LayersInterface.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\OutOfBoundsException; + +/** + * The layers interface + */ +interface LayersInterface extends \Iterator, \Countable, \ArrayAccess +{ + /** + * Merge layers into the original objects + * + * @throws RuntimeException + */ + public function merge(); + + /** + * Animates layers + * + * @param string $format The output output format + * @param integer $delay The delay in milliseconds between two frames + * @param integer $loops The number of loops, 0 means infinite + * + * @return LayersInterface + * + * @throws InvalidArgumentException In case an invalid argument is provided + * @throws RuntimeException In case the driver fails to animate + */ + public function animate($format, $delay, $loops); + + /** + * Coalesce layers. Each layer in the sequence is the same size as the first and composited with the next layer in + * the sequence. + */ + public function coalesce(); + + /** + * Adds an image at the end of the layers stack + * + * @param ImageInterface $image + * + * @return LayersInterface + * + * @throws RuntimeException + */ + public function add(ImageInterface $image); + + /** + * Set an image at offset + * + * @param integer $offset + * @param ImageInterface $image + * + * @return LayersInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + * @throws OutOfBoundsException + */ + public function set($offset, ImageInterface $image); + + /** + * Removes the image at offset + * + * @param integer $offset + * + * @return LayersInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function remove($offset); + + /** + * Returns the image at offset + * + * @param integer $offset + * + * @return ImageInterface + * + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function get($offset); + + /** + * Returns true if a layer at offset is preset + * + * @param integer $offset + * + * @return Boolean + */ + public function has($offset); +} diff --git a/src/Symfony/Component/Image/Image/LoaderInterface.php b/src/Symfony/Component/Image/Image/LoaderInterface.php new file mode 100644 index 0000000000000..34791f29f8992 --- /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 integer $size + * @param ColorInterface $color + * + * @return FontInterface + */ + public function font($file, $size, ColorInterface $color); +} diff --git a/src/Symfony/Component/Image/Image/ManipulatorInterface.php b/src/Symfony/Component/Image/Image/ManipulatorInterface.php new file mode 100644 index 0000000000000..2510222877222 --- /dev/null +++ b/src/Symfony/Component/Image/Image/ManipulatorInterface.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; + +/** + * The manipulator interface + */ +interface ManipulatorInterface +{ + const THUMBNAIL_INSET = 'inset'; + const THUMBNAIL_OUTBOUND = 'outbound'; + + /** + * Copies current source image into a new ImageInterface instance + * + * @throws RuntimeException + * + * @return static + */ + public function copy(); + + /** + * Crops a specified box out of the source image (modifies the source image) + * Returns cropped self + * + * @param PointInterface $start + * @param BoxInterface $size + * + * @throws OutOfBoundsException + * @throws RuntimeException + * + * @return static + */ + public function crop(PointInterface $start, BoxInterface $size); + + /** + * Resizes current image and returns self + * + * @param BoxInterface $size + * @param string $filter + * + * @throws RuntimeException + * + * @return static + */ + public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED); + + /** + * Rotates an image at the given angle. + * Optional $background can be used to specify the fill color of the empty + * area of rotated image. + * + * @param integer $angle + * @param ColorInterface $background + * + * @throws RuntimeException + * + * @return static + */ + public function rotate($angle, ColorInterface $background = null); + + /** + * Pastes an image into a parent image + * Throws exceptions if image exceeds parent image borders or if paste + * operation fails + * + * Returns source image + * + * @param ImageInterface $image + * @param PointInterface $start + * + * @throws InvalidArgumentException + * @throws OutOfBoundsException + * @throws RuntimeException + * + * @return static + */ + public function paste(ImageInterface $image, PointInterface $start); + + /** + * Saves the image at a specified path, the target file extension is used + * to determine file format, only jpg, jpeg, gif, png, wbmp and xbm are + * supported + * + * @param string $path + * @param array $options + * + * @throws RuntimeException + * + * @return static + */ + public function save($path = null, array $options = array()); + + /** + * Outputs the image content + * + * @param string $format + * @param array $options + * + * @throws RuntimeException + * + * @return static + */ + public function show($format, array $options = array()); + + /** + * Flips current image using horizontal axis + * + * @throws RuntimeException + * + * @return static + */ + public function flipHorizontally(); + + /** + * Flips current image using vertical axis + * + * @throws RuntimeException + * + * @return static + */ + public function flipVertically(); + + /** + * Remove all profiles and comments + * + * @throws RuntimeException + * + * @return static + */ + public function strip(); + + /** + * Generates a thumbnail from a current image + * Returns it as a new image, doesn't modify the current image + * + * @param BoxInterface $size + * @param string $mode + * @param string $filter The filter to use for resizing, one of ImageInterface::FILTER_* + * + * @throws RuntimeException + * + * @return static + */ + public function thumbnail(BoxInterface $size, $mode = self::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED); + + /** + * Applies a given mask to current image's alpha channel + * + * @param ImageInterface $mask + * + * @return static + */ + public function applyMask(ImageInterface $mask); + + /** + * Fills image with provided filling, by replacing each pixel's color in + * the current image with corresponding color from FillInterface, and + * returns modified image + * + * @param FillInterface $fill + * + * @return static + */ + public function fill(FillInterface $fill); +} diff --git a/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php new file mode 100644 index 0000000000000..0c35323a48983 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Metadata; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +abstract class AbstractMetadataReader implements MetadataReaderInterface +{ + /** + * {@inheritdoc} + */ + public function readFile($file) + { + if (stream_is_local($file)) { + if (!is_file($file)) { + throw new InvalidArgumentException(sprintf('File %s does not exist.', $file)); + } + + return new MetadataBag(array_merge(array('filepath' => realpath($file), 'uri' => $file), $this->extractFromFile($file))); + } + + return new MetadataBag(array_merge(array('uri' => $file), $this->extractFromFile($file))); + } + + /** + * {@inheritdoc} + */ + public function readData($data, $originalResource = null) + { + if (null !== $originalResource) { + return new MetadataBag(array_merge($this->getStreamMetadata($originalResource), $this->extractFromData($data))); + } + + return new MetadataBag($this->extractFromData($data)); + } + + /** + * {@inheritdoc} + */ + public function readStream($resource) + { + if (!is_resource($resource)) { + throw new InvalidArgumentException('Invalid resource provided.'); + } + + return new MetadataBag(array_merge($this->getStreamMetadata($resource), $this->extractFromStream($resource))); + } + + /** + * Gets the URI from a stream resource + * + * @param resource $resource + * + * @return string|null The URI f ava + */ + private function getStreamMetadata($resource) + { + $metadata = array(); + + if (false !== $data = @stream_get_meta_data($resource)) { + $metadata['uri'] = $data['uri']; + if (stream_is_local($resource)) { + $metadata['filepath'] = realpath($data['uri']); + } + } + + return $metadata; + } + + /** + * Extracts metadata from a file + * + * @param $file + * + * @return array An associative array of metadata + */ + abstract protected function extractFromFile($file); + + /** + * Extracts metadata from raw data + * + * @param $data + * + * @return array An associative array of metadata + */ + abstract protected function extractFromData($data); + + /** + * Extracts metadata from a stream + * + * @param $resource + * + * @return array An associative array of metadata + */ + abstract protected function extractFromStream($resource); +} diff --git a/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php new file mode 100644 index 0000000000000..c8f23833e8f10 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Metadata; + +/** + * Default metadata reader + */ +class DefaultMetadataReader extends AbstractMetadataReader +{ + /** + * {@inheritdoc} + */ + protected function extractFromFile($file) + { + return array(); + } + + /** + * {@inheritdoc} + */ + protected function extractFromData($data) + { + return array(); + } + + /** + * {@inheritdoc} + */ + protected function extractFromStream($resource) + { + return array(); + } +} diff --git a/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php new file mode 100644 index 0000000000000..7dc1881d5b6e4 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Metadata; + +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\NotSupportedException; + +/** + * Metadata driven by Exif information + */ +class ExifMetadataReader extends AbstractMetadataReader +{ + public function __construct() + { + if (!self::isSupported()) { + throw new NotSupportedException('PHP exif extension is required to use the ExifMetadataReader'); + } + } + + public static function isSupported() + { + return function_exists('exif_read_data'); + } + + /** + * {@inheritdoc} + */ + protected function extractFromFile($file) + { + if (stream_is_local($file)) { + if (false === is_readable($file)) { + throw new InvalidArgumentException(sprintf('File %s is not readable.', $file)); + } + + return $this->extract($file); + } + + if (false === $data = @file_get_contents($file)) { + throw new InvalidArgumentException(sprintf('File %s is not readable.', $file)); + } + + return $this->doReadData($data); + } + + /** + * {@inheritdoc} + */ + protected function extractFromData($data) + { + return $this->doReadData($data); + } + + /** + * {@inheritdoc} + */ + protected function extractFromStream($resource) + { + return $this->doReadData(stream_get_contents($resource)); + } + + /** + * Extracts metadata from raw data, merges with existing metadata + * + * @param string $data + * + * @return MetadataBag + */ + private function doReadData($data) + { + if (substr($data, 0, 2) === 'II') { + $mime = 'image/tiff'; + } else { + $mime = 'image/jpeg'; + } + + return $this->extract('data://' . $mime . ';base64,' . base64_encode($data)); + } + + /** + * Performs the exif data extraction given a path or data-URI representation. + * + * @param string $path The path to the file or the data-URI representation. + * + * @return MetadataBag + */ + private function extract($path) + { + if (false === $exifData = @exif_read_data($path, null, true, false)) { + return array(); + } + + $metadata = array(); + $sources = array('EXIF' => 'exif', 'IFD0' => 'ifd0'); + + foreach ($sources as $name => $prefix) { + if (!isset($exifData[$name])) { + continue; + } + foreach ($exifData[$name] as $prop => $value) { + $metadata[$prefix.'.'.$prop] = $value; + } + } + + return $metadata; + } +} diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php new file mode 100644 index 0000000000000..91087a03249ab --- /dev/null +++ b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Metadata; + +/** + * An interface for Image Metadata + */ +class MetadataBag implements \ArrayAccess, \IteratorAggregate, \Countable +{ + /** @var array */ + private $data; + + public function __construct(array $data = array()) + { + $this->data = $data; + } + + /** + * Returns the metadata key, default value if it does not exist + * + * @param string $key + * @param mixed|null $default + * + * @return mixed + */ + public function get($key, $default = null) + { + return array_key_exists($key, $this->data) ? $this->data[$key] : $default; + } + + /** + * {@inheritdoc} + */ + public function count() + { + return count($this->data); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new \ArrayIterator($this->data); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->data); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + $this->data[$offset] = $value; + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + unset($this->data[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Returns metadata as an array + * + * @return array An associative array + */ + public function toArray() + { + return $this->data; + } +} diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php new file mode 100644 index 0000000000000..110b809e06ebf --- /dev/null +++ b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Metadata; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +interface MetadataReaderInterface +{ + /** + * Reads metadata from a file. + * + * @param $file The path to the file where to read metadata. + * + * @throws InvalidArgumentException In case the file does not exist. + * + * @return MetadataBag + */ + public function readFile($file); + + /** + * Reads metadata from a binary string. + * + * @param $data The binary string to read. + * @param $originalResource An optional resource to gather stream metadata. + * + * @return MetadataBag + */ + public function readData($data, $originalResource = null); + + /** + * Reads metadata from a stream. + * + * @param $resource The stream to read. + * + * @throws InvalidArgumentException In case the resource is not valid. + * + * @return MetadataBag + */ + public function readStream($resource); +} diff --git a/src/Symfony/Component/Image/Image/Palette/CMYK.php b/src/Symfony/Component/Image/Image/Palette/CMYK.php new file mode 100644 index 0000000000000..da49bb0a45ad4 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/CMYK.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette; + +use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Image\Profile; +use Symfony\Component\Image\Image\ProfileInterface; + +class CMYK implements PaletteInterface +{ + private $parser; + private $profile; + private static $colors = array(); + + public function __construct() + { + $this->parser = new ColorParser(); + } + + /** + * {@inheritdoc} + */ + public function name() + { + return PaletteInterface::PALETTE_CMYK; + } + + /** + * {@inheritdoc} + */ + public function pixelDefinition() + { + return array( + ColorInterface::COLOR_CYAN, + ColorInterface::COLOR_MAGENTA, + ColorInterface::COLOR_YELLOW, + ColorInterface::COLOR_KEYLINE, + ); + } + + /** + * {@inheritdoc} + */ + public function supportsAlpha() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function color($color, $alpha = null) + { + if (null !== $alpha) { + throw new InvalidArgumentException('CMYK palette does not support alpha'); + } + + $color = $this->parser->parseToCMYK($color); + $index = sprintf('cmyk(%d, %d, %d, %d)', $color[0], $color[1], $color[2], $color[3]); + + if (false === array_key_exists($index, self::$colors)) { + self::$colors[$index] = new CMYKColor($this, $color); + } + + return self::$colors[$index]; + } + + /** + * {@inheritdoc} + */ + public function blend(ColorInterface $color1, ColorInterface $color2, $amount) + { + if (!$color1 instanceof CMYKColor || ! $color2 instanceof CMYKColor) { + throw new RuntimeException('CMYK palette can only blend CMYK colors'); + } + + return $this->color(array( + min(100, $color1->getCyan() + $color2->getCyan() * $amount), + min(100, $color1->getMagenta() + $color2->getMagenta() * $amount), + min(100, $color1->getYellow() + $color2->getYellow() * $amount), + min(100, $color1->getKeyline() + $color2->getKeyline() * $amount), + )); + } + + /** + * {@inheritdoc} + */ + public function useProfile(ProfileInterface $profile) + { + $this->profile = $profile; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function profile() + { + if (!$this->profile) { + $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/Adobe/CMYK/USWebUncoated.icc'); + } + + return $this->profile; + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php new file mode 100644 index 0000000000000..366cb46f66803 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\CMYK as CMYKPalette; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\InvalidArgumentException; + +final class CMYK implements ColorInterface +{ + /** + * @var integer + */ + private $c; + + /** + * @var integer + */ + private $m; + + /** + * @var integer + */ + private $y; + + /** + * @var integer + */ + private $k; + + /** + * + * @var CMYK + */ + private $palette; + + public function __construct(CMYKPalette $palette, array $color) + { + $this->palette = $palette; + $this->setColor($color); + } + + /** + * {@inheritdoc} + */ + public function getValue($component) + { + switch ($component) { + case ColorInterface::COLOR_CYAN: + return $this->getCyan(); + case ColorInterface::COLOR_MAGENTA: + return $this->getMagenta(); + case ColorInterface::COLOR_YELLOW: + return $this->getYellow(); + case ColorInterface::COLOR_KEYLINE: + return $this->getKeyline(); + default: + throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component)); + } + } + + /** + * Returns Cyan value of the color + * + * @return integer + */ + public function getCyan() + { + return $this->c; + } + + /** + * Returns Magenta value of the color + * + * @return integer + */ + public function getMagenta() + { + return $this->m; + } + + /** + * Returns Yellow value of the color + * + * @return integer + */ + public function getYellow() + { + return $this->y; + } + + /** + * Returns Key value of the color + * + * @return integer + */ + public function getKeyline() + { + return $this->k; + } + + /** + * {@inheritdoc} + */ + public function getPalette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function getAlpha() + { + return null; + } + + /** + * {@inheritdoc} + */ + public function dissolve($alpha) + { + throw new RuntimeException('CMYK does not support dissolution'); + } + + /** + * {@inheritdoc} + */ + public function lighten($shade) + { + return $this->palette->color( + array( + $this->c, + $this->m, + $this->y, + max(0, $this->k - $shade), + ) + ); + } + + /** + * {@inheritdoc} + */ + public function darken($shade) + { + return $this->palette->color( + array( + $this->c, + $this->m, + $this->y, + min(100, $this->k + $shade), + ) + ); + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + $color = array( + $this->c * (1 - $this->k / 100) + $this->k, + $this->m * (1 - $this->k / 100) + $this->k, + $this->y * (1 - $this->k / 100) + $this->k, + ); + + $gray = min(100, round(0.299 * $color[0] + 0.587 * $color[1] + 0.114 * $color[2])); + + return $this->palette->color(array($gray, $gray, $gray, $this->k)); + } + + /** + * {@inheritdoc} + */ + public function isOpaque() + { + return true; + } + + /** + * Returns hex representation of the color + * + * @return string + */ + public function __toString() + { + return sprintf('cmyk(%d%%, %d%%, %d%%, %d%%)', $this->c, $this->m, $this->y, $this->k); + } + + /** + * Internal, Performs checks for color validity (an of array(C, M, Y, K)) + * + * @param array $color + * + * @throws InvalidArgumentException + */ + private function setColor(array $color) + { + if (count($color) !== 4) { + throw new InvalidArgumentException('Color argument must look like array(C, M, Y, K), where C, M, Y, K are the integer values between 0 and 255 for cyan, magenta, yellow and black color indexes accordingly'); + } + + $colors = array_values($color); + array_walk($colors, function ($color) { + return max(0, min(100, $color)); + }); + + list($this->c, $this->m, $this->y, $this->k) = $colors; + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php new file mode 100644 index 0000000000000..47c128984432e --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\PaletteInterface; + +interface ColorInterface +{ + const COLOR_RED = 'red'; + const COLOR_GREEN = 'green'; + const COLOR_BLUE = 'blue'; + + const COLOR_CYAN = 'cyan'; + const COLOR_MAGENTA = 'magenta'; + const COLOR_YELLOW = 'yellow'; + const COLOR_KEYLINE = 'keyline'; + + const COLOR_GRAY = 'gray'; + + /** + * Return the value of one of the component. + * + * @param string $component One of the ColorInterface::COLOR_* component + * + * @return Integer + */ + public function getValue($component); + + /** + * Returns percentage of transparency of the color + * + * @return integer + */ + public function getAlpha(); + + /** + * Returns the palette attached to the current color + * + * @return PaletteInterface + */ + public function getPalette(); + + /** + * Returns a copy of current color, incrementing the alpha channel by the + * given amount + * + * @param integer $alpha + * + * @return ColorInterface + */ + public function dissolve($alpha); + + /** + * Returns a copy of the current color, lightened by the specified number + * of shades + * + * @param integer $shade + * + * @return ColorInterface + */ + public function lighten($shade); + + /** + * Returns a copy of the current color, darkened by the specified number of + * shades + * + * @param integer $shade + * + * @return ColorInterface + */ + public function darken($shade); + + /** + * Returns a gray related to the current color + * + * @return ColorInterface + */ + public function grayscale(); + + /** + * Checks if the current color is opaque + * + * @return Boolean + */ + public function isOpaque(); +} diff --git a/src/Symfony/Component/Image/Image/Palette/Color/Gray.php b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php new file mode 100644 index 0000000000000..c0be1212c5d2a --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\Grayscale; +use Symfony\Component\Image\Exception\InvalidArgumentException; + +final class Gray implements ColorInterface +{ + /** + * @var integer + */ + private $gray; + + /** + * @var integer + */ + private $alpha; + + /** + * + * @var Grayscale + */ + private $palette; + + public function __construct(Grayscale $palette, array $color, $alpha) + { + $this->palette = $palette; + $this->setColor($color); + $this->setAlpha($alpha); + } + + /** + * {@inheritdoc} + */ + public function getValue($component) + { + switch ($component) { + case ColorInterface::COLOR_GRAY: + return $this->getGray(); + default: + throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component)); + } + } + + /** + * Returns Gray value of the color + * + * @return integer + */ + public function getGray() + { + return $this->gray; + } + + /** + * {@inheritdoc} + */ + public function getPalette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function getAlpha() + { + return $this->alpha; + } + + /** + * {@inheritdoc} + */ + public function dissolve($alpha) + { + return $this->palette->color( + array($this->gray), $this->alpha + $alpha + ); + } + + /** + * {@inheritdoc} + */ + public function lighten($shade) + { + return $this->palette->color(array(min(255, $this->gray + $shade)), $this->alpha); + } + + /** + * {@inheritdoc} + */ + public function darken($shade) + { + return $this->palette->color(array(max(0, $this->gray - $shade)), $this->alpha); + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + return $this; + } + + /** + * {@inheritdoc} + */ + public function isOpaque() + { + return 100 === $this->alpha; + } + + /** + * Returns hex representation of the color + * + * @return string + */ + public function __toString() + { + return sprintf('#%02x%02x%02x', $this->gray, $this->gray, $this->gray); + } + + /** + * Performs checks for validity of given alpha value and sets it + * + * @param integer $alpha + * + * @throws InvalidArgumentException + */ + private function setAlpha($alpha) + { + if (!is_int($alpha) || $alpha < 0 || $alpha > 100) { + throw new InvalidArgumentException(sprintf('Alpha must be an integer between 0 and 100, %s given', $alpha)); + } + + $this->alpha = $alpha; + } + + /** + * Performs checks for color validity (array of array(gray)) + * + * @param array $color + * + * @throws InvalidArgumentException + */ + private function setColor(array $color) + { + if (count($color) !== 1) { + throw new InvalidArgumentException('Color argument must look like array(gray), where gray is the integer value between 0 and 255 for the grayscale'); + } + + list($this->gray) = array_values($color); + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/Color/RGB.php b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php new file mode 100644 index 0000000000000..7cf5e16569826 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php @@ -0,0 +1,214 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\RGB as RGBPalette; +use Symfony\Component\Image\Exception\InvalidArgumentException; + +final class RGB implements ColorInterface +{ + /** + * @var integer + */ + private $r; + + /** + * @var integer + */ + private $g; + + /** + * @var integer + */ + private $b; + + /** + * @var integer + */ + private $alpha; + + /** + * + * @var RGBPalette + */ + private $palette; + + public function __construct(RGBPalette $palette, array $color, $alpha) + { + $this->palette = $palette; + $this->setColor($color); + $this->setAlpha($alpha); + } + + /** + * {@inheritdoc} + */ + public function getValue($component) + { + switch ($component) { + case ColorInterface::COLOR_RED: + return $this->getRed(); + case ColorInterface::COLOR_GREEN: + return $this->getGreen(); + case ColorInterface::COLOR_BLUE: + return $this->getBlue(); + default: + throw new InvalidArgumentException(sprintf('Color component %s is not valid', $component)); + } + } + + /** + * Returns RED value of the color + * + * @return integer + */ + public function getRed() + { + return $this->r; + } + + /** + * Returns GREEN value of the color + * + * @return integer + */ + public function getGreen() + { + return $this->g; + } + + /** + * Returns BLUE value of the color + * + * @return integer + */ + public function getBlue() + { + return $this->b; + } + + /** + * {@inheritdoc} + */ + public function getPalette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function getAlpha() + { + return $this->alpha; + } + + /** + * {@inheritdoc} + */ + public function dissolve($alpha) + { + return $this->palette->color(array($this->r, $this->g, $this->b), $this->alpha + $alpha); + } + + /** + * {@inheritdoc} + */ + public function lighten($shade) + { + return $this->palette->color( + array( + min(255, $this->r + $shade), + min(255, $this->g + $shade), + min(255, $this->b + $shade), + ), $this->alpha + ); + } + + /** + * {@inheritdoc} + */ + public function darken($shade) + { + return $this->palette->color( + array( + max(0, $this->r - $shade), + max(0, $this->g - $shade), + max(0, $this->b - $shade), + ), $this->alpha + ); + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + $gray = min(255, round(0.299 * $this->getRed() + 0.114 * $this->getBlue() + 0.587 * $this->getGreen())); + + return $this->palette->color(array($gray, $gray, $gray), $this->alpha); + } + + /** + * {@inheritdoc} + */ + public function isOpaque() + { + return 100 === $this->alpha; + } + + /** + * Returns hex representation of the color + * + * @return string + */ + public function __toString() + { + return sprintf('#%02x%02x%02x', $this->r, $this->g, $this->b); + } + + /** + * Internal + * + * Performs checks for validity of given alpha value and sets it + * + * @param integer $alpha + * + * @throws InvalidArgumentException + */ + private function setAlpha($alpha) + { + if (!is_int($alpha) || $alpha < 0 || $alpha > 100) { + throw new InvalidArgumentException(sprintf('Alpha must be an integer between 0 and 100, %s given', $alpha)); + } + + $this->alpha = $alpha; + } + + /** + * Internal + * + * Performs checks for color validity (array of array(R, G, B)) + * + * @param array $color + * + * @throws InvalidArgumentException + */ + private function setColor(array $color) + { + if (count($color) !== 3) { + throw new InvalidArgumentException('Color argument must look like array(R, G, B), where R, G, B are the integer values between 0 and 255 for red, green and blue color indexes accordingly'); + } + + list($this->r, $this->g, $this->b) = array_values($color); + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/ColorParser.php b/src/Symfony/Component/Image/Image/Palette/ColorParser.php new file mode 100644 index 0000000000000..e63ad588e4c97 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/ColorParser.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +class ColorParser +{ + /** + * Parses a color to a RGB tuple + * + * @param string|array|integer $color + * + * @return array + * + * @throws InvalidArgumentException + */ + public function parseToRGB($color) + { + $color = $this->parse($color); + + if (4 === count($color)) { + $color = array( + 255 * (1 - $color[0] / 100) * (1 - $color[3] / 100), + 255 * (1 - $color[1] / 100) * (1 - $color[3] / 100), + 255 * (1 - $color[2] / 100) * (1 - $color[3] / 100), + ); + } + + return $color; + } + + /** + * Parses a color to a CMYK tuple + * + * @param string|array|integer $color + * + * @return array + * + * @throws InvalidArgumentException + */ + public function parseToCMYK($color) + { + $color = $this->parse($color); + + if (3 === count($color)) { + $r = $color[0] / 255; + $g = $color[1] / 255; + $b = $color[2] / 255; + + $k = 1 - max($r, $g, $b); + + $color = array( + 1 === $k ? 0 : round((1 - $r - $k) / (1- $k) * 100), + 1 === $k ? 0 : round((1 - $g - $k) / (1- $k) * 100), + 1 === $k ? 0 : round((1 - $b - $k) / (1- $k) * 100), + round($k * 100) + ); + } + + return $color; + } + + /** + * Parses a color to a grayscale value + * + * @param string|array|integer $color + * + * @return array + * + * @throws InvalidArgumentException + */ + public function parseToGrayscale($color) + { + if (is_array($color) && 1 === count($color)) { + return array_values($color); + } + + $color = array_unique($this->parse($color)); + + if (1 !== count($color)) { + throw new InvalidArgumentException('The provided color has different values of red, green and blue components. Grayscale colors must have the same values for these.'); + } + + return $color; + } + + /** + * Parses a color + * + * @param string|array|integer $color + * + * @return array + * + * @throws InvalidArgumentException + */ + private function parse($color) + { + if (!is_string($color) && !is_array($color) && !is_int($color)) { + throw new InvalidArgumentException(sprintf('Color must be specified as a hexadecimal string, array or integer, %s given', gettype($color))); + } + + if (is_array($color)) { + if (3 === count($color) || 4 === count($color)) { + return array_values($color); + } + throw new InvalidArgumentException('Color argument if array, must look like array(R, G, B), or array(C, M, Y, K) where R, G, B are the integer values between 0 and 255 for red, green and blue or cyan, magenta, yellow and black color indexes accordingly'); + } + + if (is_string($color)) { + if (0 === strpos($color, 'cmyk(')) { + $substrColor = substr($color, 5, strlen($color) - 6); + + $components = array_map(function ($component) { + return round(trim($component, ' %')); + }, explode(',', $substrColor)); + + if (count($components) !== 4) { + throw new InvalidArgumentException(sprintf('Unable to parse color %s', $color)); + } + + return $components; + } else { + $color = ltrim($color, '#'); + + if (strlen($color) !== 3 && strlen($color) !== 6) { + throw new InvalidArgumentException(sprintf('Color must be a hex value in regular (6 characters) or short (3 characters) notation, "%s" given', $color)); + } + + if (strlen($color) === 3) { + $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2]; + } + + $color = array_map('hexdec', str_split($color, 2)); + } + } + + if (is_int($color)) { + $color = array(255 & ($color >> 16), 255 & ($color >> 8), 255 & $color); + } + + return $color; + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/Grayscale.php b/src/Symfony/Component/Image/Image/Palette/Grayscale.php new file mode 100644 index 0000000000000..01f34eaba02e0 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/Grayscale.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette; + +use Symfony\Component\Image\Image\Palette\Color\Gray as GrayColor; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Image\Profile; +use Symfony\Component\Image\Exception\RuntimeException; + +class Grayscale implements PaletteInterface +{ + /** + * @var ColorParser + */ + private $parser; + + /** + * @var ProfileInterface + */ + private $profile; + + /** + * @var array + */ + protected static $colors = array(); + + public function __construct() + { + $this->parser = new ColorParser(); + } + + /** + * {@inheritdoc} + */ + public function name() + { + return PaletteInterface::PALETTE_GRAYSCALE; + } + + /** + * {@inheritdoc} + */ + public function pixelDefinition() + { + return array(ColorInterface::COLOR_GRAY); + } + + /** + * {@inheritdoc} + */ + public function supportsAlpha() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function useProfile(ProfileInterface $profile) + { + $this->profile = $profile; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function profile() + { + if (!$this->profile) { + $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC'); + } + + return $this->profile; + } + + /** + * {@inheritdoc} + */ + public function color($color, $alpha = null) + { + if (null === $alpha) { + $alpha = 0; + } + + $color = $this->parser->parseToGrayscale($color); + $index = sprintf('#%02x%02x%02x-%d', $color[0], $color[0], $color[0], $alpha); + + if (false === array_key_exists($index, static::$colors)) { + static::$colors[$index] = new GrayColor($this, $color, $alpha); + } + + return static::$colors[$index]; + } + + /** + * {@inheritdoc} + */ + public function blend(ColorInterface $color1, ColorInterface $color2, $amount) + { + if (!$color1 instanceof GrayColor || ! $color2 instanceof GrayColor) { + throw new RuntimeException('Grayscale palette can only blend Grayscale colors'); + } + + return $this->color( + array( + (int) min(255, min($color1->getGray(), $color2->getGray()) + round(abs($color2->getGray() - $color1->getGray()) * $amount)), + ), + (int) min(100, min($color1->getAlpha(), $color2->getAlpha()) + round(abs($color2->getAlpha() - $color1->getAlpha()) * $amount)) + ); + } +} diff --git a/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php new file mode 100644 index 0000000000000..04d8aae3f8ba1 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette; + +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +interface PaletteInterface +{ + const PALETTE_GRAYSCALE = 'gray'; + const PALETTE_RGB = 'rgb'; + const PALETTE_CMYK = 'cmyk'; + + /** + * Returns a color given some values + * + * @param string|array|integer $color A color + * @param integer|null $alpha Set alpha to null to disable it + * + * @return ColorInterface + * + * @throws InvalidArgumentException In case you pass an alpha value to a + * Palette that does not support alpha + */ + public function color($color, $alpha = null); + + /** + * Blend two colors given an amount + * + * @param ColorInterface $color1 + * @param ColorInterface $color2 + * @param float $amount The amount of color2 in color1 + * + * @return ColorInterface + */ + public function blend(ColorInterface $color1, ColorInterface $color2, $amount); + + /** + * Attachs an ICC profile to this Palette. + * + * (A default profile is provided by default) + * + * @param ProfileInterface $profile + * + * @return PaletteInterface + */ + public function useProfile(ProfileInterface $profile); + + /** + * Returns the ICC profile attached to this Palette. + * + * @return ProfileInterface + */ + public function profile(); + + /** + * Returns the name of this Palette, one of PaletteInterface::PALETTE_* + * constants + * + * @return String + */ + public function name(); + + /** + * Returns an array containing ColorInterface::COLOR_* constants that + * define the structure of colors for a pixel. + * + * @return array + */ + public function pixelDefinition(); + + /** + * Tells if alpha channel is supported in this palette + * + * @return Boolean + */ + public function supportsAlpha(); +} diff --git a/src/Symfony/Component/Image/Image/Palette/RGB.php b/src/Symfony/Component/Image/Image/Palette/RGB.php new file mode 100644 index 0000000000000..639aaa27d74c4 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Palette/RGB.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Palette; + +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Image\Profile; +use Symfony\Component\Image\Exception\RuntimeException; + +class RGB implements PaletteInterface +{ + /** + * @var ColorParser + */ + private $parser; + + /** + * @var ProfileInterface + */ + private $profile; + + /** + * @var array + */ + protected static $colors = array(); + + public function __construct() + { + $this->parser = new ColorParser(); + } + + /** + * {@inheritdoc} + */ + public function name() + { + return PaletteInterface::PALETTE_RGB; + } + + /** + * {@inheritdoc} + */ + public function pixelDefinition() + { + return array( + ColorInterface::COLOR_RED, + ColorInterface::COLOR_GREEN, + ColorInterface::COLOR_BLUE, + ); + } + + /** + * {@inheritdoc} + */ + public function supportsAlpha() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function useProfile(ProfileInterface $profile) + { + $this->profile = $profile; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function profile() + { + if (!$this->profile) { + $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc'); + } + + return $this->profile; + } + + /** + * {@inheritdoc} + */ + public function color($color, $alpha = null) + { + if (null === $alpha) { + $alpha = 100; + } + + $color = $this->parser->parseToRGB($color); + $index = sprintf('#%02x%02x%02x-%d', $color[0], $color[1], $color[2], $alpha); + + if (false === array_key_exists($index, static::$colors)) { + static::$colors[$index] = new RGBColor($this, $color, $alpha); + } + + return static::$colors[$index]; + } + + /** + * {@inheritdoc} + */ + public function blend(ColorInterface $color1, ColorInterface $color2, $amount) + { + if (!$color1 instanceof RGBColor || ! $color2 instanceof RGBColor) { + throw new RuntimeException('RGB palette can only blend RGB colors'); + } + + return $this->color( + array( + (int) min(255, min($color1->getRed(), $color2->getRed()) + round(abs($color2->getRed() - $color1->getRed()) * $amount)), + (int) min(255, min($color1->getGreen(), $color2->getGreen()) + round(abs($color2->getGreen() - $color1->getGreen()) * $amount)), + (int) min(255, min($color1->getBlue(), $color2->getBlue()) + round(abs($color2->getBlue() - $color1->getBlue()) * $amount)), + ), + (int) min(100, min($color1->getAlpha(), $color2->getAlpha()) + round(abs($color2->getAlpha() - $color1->getAlpha()) * $amount)) + ); + } +} diff --git a/src/Symfony/Component/Image/Image/Point.php b/src/Symfony/Component/Image/Image/Point.php new file mode 100644 index 0000000000000..a33f3eada01f5 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Point.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +/** + * The point class + */ +final class Point implements PointInterface +{ + /** + * @var integer + */ + private $x; + + /** + * @var integer + */ + private $y; + + /** + * Constructs a point of coordinates + * + * @param integer $x + * @param integer $y + * + * @throws InvalidArgumentException + */ + public function __construct($x, $y) + { + if ($x < 0 || $y < 0) { + throw new InvalidArgumentException(sprintf('A coordinate cannot be positioned outside of a bounding box (x: %s, y: %s given)', $x, $y)); + } + + $this->x = $x; + $this->y = $y; + } + + /** + * {@inheritdoc} + */ + public function getX() + { + return $this->x; + } + + /** + * {@inheritdoc} + */ + public function getY() + { + return $this->y; + } + + /** + * {@inheritdoc} + */ + public function in(BoxInterface $box) + { + return $this->x < $box->getWidth() && $this->y < $box->getHeight(); + } + + /** + * {@inheritdoc} + */ + public function move($amount) + { + return new Point($this->x + $amount, $this->y + $amount); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return sprintf('(%d, %d)', $this->x, $this->y); + } +} diff --git a/src/Symfony/Component/Image/Image/Point/Center.php b/src/Symfony/Component/Image/Image/Point/Center.php new file mode 100644 index 0000000000000..9a28b70828f71 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Point/Center.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image\Point; + +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Point as OriginalPoint; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Point center + */ +final class Center implements PointInterface +{ + /** + * @var BoxInterface + */ + private $box; + + /** + * Constructs coordinate with size instance, it needs to be relative to + * + * @param BoxInterface $box + */ + public function __construct(BoxInterface $box) + { + $this->box = $box; + } + + /** + * {@inheritdoc} + */ + public function getX() + { + return ceil($this->box->getWidth() / 2); + } + + /** + * {@inheritdoc} + */ + public function getY() + { + return ceil($this->box->getHeight() / 2); + } + + /** + * {@inheritdoc} + */ + public function in(BoxInterface $box) + { + return $this->getX() < $box->getWidth() && $this->getY() < $box->getHeight(); + } + + /** + * {@inheritdoc} + */ + public function move($amount) + { + return new OriginalPoint($this->getX() + $amount, $this->getY() + $amount); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return sprintf('(%d, %d)', $this->getX(), $this->getY()); + } +} diff --git a/src/Symfony/Component/Image/Image/PointInterface.php b/src/Symfony/Component/Image/Image/PointInterface.php new file mode 100644 index 0000000000000..f42217e27c19d --- /dev/null +++ b/src/Symfony/Component/Image/Image/PointInterface.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +/** + * The point interface + */ +interface PointInterface +{ + /** + * Gets points x coordinate + * + * @return integer + */ + public function getX(); + + /** + * Gets points y coordinate + * + * @return integer + */ + public function getY(); + + /** + * Checks if current coordinate is inside a given box + * + * @param BoxInterface $box + * + * @return Boolean + */ + public function in(BoxInterface $box); + + /** + * Returns another point, moved by a given amount from current coordinates + * + * @param integer $amount + * @return ImageInterface + */ + public function move($amount); + + /** + * Gets a string representation for the current point + * + * @return string + */ + public function __toString(); +} diff --git a/src/Symfony/Component/Image/Image/Profile.php b/src/Symfony/Component/Image/Image/Profile.php new file mode 100644 index 0000000000000..7374e6f525d36 --- /dev/null +++ b/src/Symfony/Component/Image/Image/Profile.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +use Symfony\Component\Image\Exception\InvalidArgumentException; + +class Profile implements ProfileInterface +{ + private $data; + private $name; + + public function __construct($name, $data) + { + $this->name = $name; + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function name() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function data() + { + return $this->data; + } + + /** + * Creates a profile from a path to a file + * + * @param String $path + * + * @return Profile + * + * @throws InvalidArgumentException In case the provided path is not valid + */ + public static function fromPath($path) + { + if (!file_exists($path) || !is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('Path %s is an invalid profile file or is not readable', $path)); + } + + return new static(basename($path), file_get_contents($path)); + } +} diff --git a/src/Symfony/Component/Image/Image/ProfileInterface.php b/src/Symfony/Component/Image/Image/ProfileInterface.php new file mode 100644 index 0000000000000..3e09656c75ea7 --- /dev/null +++ b/src/Symfony/Component/Image/Image/ProfileInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Image; + +interface ProfileInterface +{ + /** + * Returns the name of the profile + * + * @return String + */ + public function name(); + + /** + * Returns the profile data + * + * @return String + */ + public function data(); +} diff --git a/src/Symfony/Component/Image/Imagick/Drawer.php b/src/Symfony/Component/Image/Imagick/Drawer.php new file mode 100644 index 0000000000000..f91d68f942c0e --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Drawer.php @@ -0,0 +1,404 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Draw\DrawerInterface; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; + +/** + * Drawer implementation using the Imagick PHP extension + */ +final class Drawer implements DrawerInterface +{ + /** + * @var \Imagick + */ + private $imagick; + + /** + * @param \Imagick $imagick + */ + public function __construct(\Imagick $imagick) + { + $this->imagick = $imagick; + } + + /** + * {@inheritdoc} + */ + public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) + { + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $arc = new \ImagickDraw(); + + $arc->setStrokeColor($pixel); + $arc->setStrokeWidth(max(1, (int) $thickness)); + $arc->setFillColor('transparent'); + $arc->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end); + + $this->imagick->drawImage($arc); + + $pixel->clear(); + $pixel->destroy(); + + $arc->clear(); + $arc->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $chord = new \ImagickDraw(); + + $chord->setStrokeColor($pixel); + $chord->setStrokeWidth(max(1, (int) $thickness)); + + if ($fill) { + $chord->setFillColor($pixel); + } else { + $this->line( + new Point(round($x + $width / 2 * cos(deg2rad($start))), round($y + $height / 2 * sin(deg2rad($start)))), + new Point(round($x + $width / 2 * cos(deg2rad($end))), round($y + $height / 2 * sin(deg2rad($end)))), + $color + ); + + $chord->setFillColor('transparent'); + } + + $chord->arc( + $x - $width / 2, + $y - $height / 2, + $x + $width / 2, + $y + $height / 2, + $start, + $end + ); + + $this->imagick->drawImage($chord); + + $pixel->clear(); + $pixel->destroy(); + + $chord->clear(); + $chord->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + try { + $pixel = $this->getColor($color); + $ellipse = new \ImagickDraw(); + + $ellipse->setStrokeColor($pixel); + $ellipse->setStrokeWidth(max(1, (int) $thickness)); + + if ($fill) { + $ellipse->setFillColor($pixel); + } else { + $ellipse->setFillColor('transparent'); + } + + $ellipse->ellipse( + $center->getX(), + $center->getY(), + $width / 2, + $height / 2, + 0, 360 + ); + + if (false === $this->imagick->drawImage($ellipse)) { + throw new RuntimeException('Ellipse operation failed'); + } + + $pixel->clear(); + $pixel->destroy(); + + $ellipse->clear(); + $ellipse->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1) + { + try { + $pixel = $this->getColor($color); + $line = new \ImagickDraw(); + + $line->setStrokeColor($pixel); + $line->setStrokeWidth(max(1, (int) $thickness)); + $line->setFillColor($pixel); + $line->line( + $start->getX(), + $start->getY(), + $end->getX(), + $end->getY() + ); + + $this->imagick->drawImage($line); + + $pixel->clear(); + $pixel->destroy(); + + $line->clear(); + $line->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw line operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start))); + $y1 = round($center->getY() + $height / 2 * sin(deg2rad($start))); + $x2 = round($center->getX() + $width / 2 * cos(deg2rad($end))); + $y2 = round($center->getY() + $height / 2 * sin(deg2rad($end))); + + if ($fill) { + $this->chord($center, $size, $start, $end, $color, true, $thickness); + $this->polygon( + array( + $center, + new Point($x1, $y1), + new Point($x2, $y2), + ), + $color, + true, + $thickness + ); + } else { + $this->arc($center, $size, $start, $end, $color, $thickness); + $this->line($center, new Point($x1, $y1), $color, $thickness); + $this->line($center, new Point($x2, $y2), $color, $thickness); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function dot(PointInterface $position, ColorInterface $color) + { + $x = $position->getX(); + $y = $position->getY(); + + try { + $pixel = $this->getColor($color); + $point = new \ImagickDraw(); + + $point->setFillColor($pixel); + $point->point($x, $y); + + $this->imagick->drawimage($point); + + $pixel->clear(); + $pixel->destroy(); + + $point->clear(); + $point->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw point operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1) + { + if (count($coordinates) < 3) { + throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates))); + } + + $points = array_map(function (PointInterface $p) { + return array('x' => $p->getX(), 'y' => $p->getY()); + }, $coordinates); + + try { + $pixel = $this->getColor($color); + $polygon = new \ImagickDraw(); + + $polygon->setStrokeColor($pixel); + $polygon->setStrokeWidth(max(1, (int) $thickness)); + + if ($fill) { + $polygon->setFillColor($pixel); + } else { + $polygon->setFillColor('transparent'); + } + + $polygon->polygon($points); + $this->imagick->drawImage($polygon); + + $pixel->clear(); + $pixel->destroy(); + + $polygon->clear(); + $polygon->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null) + { + try { + $pixel = $this->getColor($font->getColor()); + $text = new \ImagickDraw(); + + $text->setFont($font->getFile()); + /** + * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027 + * + * ensure font resolution is the same as GD's hard-coded 96 + */ + if (version_compare(phpversion("imagick"), "3.0.2", ">=")) { + $text->setResolution(96, 96); + $text->setFontSize($font->getSize()); + } else { + $text->setFontSize((int) ($font->getSize() * (96 / 72))); + } + $text->setFillColor($pixel); + $text->setTextAntialias(true); + + $info = $this->imagick->queryFontMetrics($text, $string); + $rad = deg2rad($angle); + $cos = cos($rad); + $sin = sin($rad); + + // round(0 * $cos - 0 * $sin) + $x1 = 0; + $x2 = round($info['characterWidth'] * $cos - $info['characterHeight'] * $sin); + // round(0 * $sin + 0 * $cos) + $y1 = 0; + $y2 = round($info['characterWidth'] * $sin + $info['characterHeight'] * $cos); + + $xdiff = 0 - min($x1, $x2); + $ydiff = 0 - min($y1, $y2); + + if ($width !== null) { + $string = $this->wrapText($string, $text, $angle, $width); + } + + $this->imagick->annotateImage( + $text, $position->getX() + $x1 + $xdiff, + $position->getY() + $y2 + $ydiff, $angle, $string + ); + + $pixel->clear(); + $pixel->destroy(); + + $text->clear(); + $text->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Draw text operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * Gets specifically formatted color string from ColorInterface instance + * + * @param ColorInterface $color + * + * @return string + */ + private function getColor(ColorInterface $color) + { + $pixel = new \ImagickPixel((string) $color); + $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100); + + return $pixel; + } + + /** + * Internal + * + * Fits a string into box with given width + */ + private function wrapText($string, $text, $angle, $width) + { + $result = ''; + $words = explode(' ', $string); + foreach ($words as $word) { + $teststring = $result . ' ' . $word; + $testbox = $this->imagick->queryFontMetrics($text, $teststring, true); + if ($testbox['textWidth'] > $width) { + $result .= ($result == '' ? '' : "\n") . $word; + } else { + $result .= ($result == '' ? '' : ' ') . $word; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/Image/Imagick/Effects.php b/src/Symfony/Component/Image/Imagick/Effects.php new file mode 100644 index 0000000000000..0c484b713a45f --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Effects.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Effects\EffectsInterface; +use Symfony\Component\Image\Exception\NotSupportedException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\Color\RGB; + +/** + * Effects implementation using the Imagick PHP extension + */ +class Effects implements EffectsInterface +{ + private $imagick; + + public function __construct(\Imagick $imagick) + { + $this->imagick = $imagick; + } + + /** + * {@inheritdoc} + */ + public function gamma($correction) + { + try { + $this->imagick->gammaImage($correction, \Imagick::CHANNEL_ALL); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function negative() + { + try { + $this->imagick->negateImage(false, \Imagick::CHANNEL_ALL); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to negate the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function grayscale() + { + try { + $this->imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function colorize(ColorInterface $color) + { + if (!$color instanceof RGB) { + throw new NotSupportedException('Colorize with non-rgb color is not supported'); + } + + try { + $this->imagick->colorizeImage((string) $color, new \ImagickPixel(sprintf('rgba(%d, %d, %d, 1)', $color->getRed(), $color->getGreen(), $color->getBlue()))); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to colorize the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function sharpen() + { + try { + $this->imagick->sharpenImage(2, 1); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to sharpen the image', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function blur($sigma = 1) + { + try { + $this->imagick->gaussianBlurImage(0, $sigma); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to blur the image', $e->getCode(), $e); + } + + return $this; + } +} diff --git a/src/Symfony/Component/Image/Imagick/Font.php b/src/Symfony/Component/Image/Imagick/Font.php new file mode 100644 index 0000000000000..d6093cbd9b5cf --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Font.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Image\AbstractFont; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +/** + * Font implementation using the Imagick PHP extension + */ +final class Font extends AbstractFont +{ + /** + * @var \Imagick + */ + private $imagick; + + /** + * @param \Imagick $imagick + * @param string $file + * @param integer $size + * @param ColorInterface $color + */ + public function __construct(\Imagick $imagick, $file, $size, ColorInterface $color) + { + $this->imagick = $imagick; + + parent::__construct($file, $size, $color); + } + + /** + * {@inheritdoc} + */ + public function box($string, $angle = 0) + { + $text = new \ImagickDraw(); + + $text->setFont($this->file); + + /** + * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027 + * + * ensure font resolution is the same as GD's hard-coded 96 + */ + if (version_compare(phpversion("imagick"), "3.0.2", ">=")) { + $text->setResolution(96, 96); + $text->setFontSize($this->size); + } else { + $text->setFontSize((int) ($this->size * (96 / 72))); + } + + $info = $this->imagick->queryFontMetrics($text, $string); + + $box = new Box($info['textWidth'], $info['textHeight']); + + return $box; + } +} diff --git a/src/Symfony/Component/Image/Imagick/Image.php b/src/Symfony/Component/Image/Imagick/Image.php new file mode 100644 index 0000000000000..afb17ca4ebdec --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Image.php @@ -0,0 +1,900 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\AbstractImage; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Fill\FillInterface; +use Symfony\Component\Image\Image\Fill\Gradient\Horizontal; +use Symfony\Component\Image\Image\Fill\Gradient\Linear; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Palette\PaletteInterface; + +/** + * Image implementation using the Imagick PHP extension + */ +final class Image extends AbstractImage +{ + /** + * @var \Imagick + */ + private $imagick; + /** + * @var Layers + */ + private $layers; + /** + * @var PaletteInterface + */ + private $palette; + + /** + * @var Boolean + */ + private static $supportsColorspaceConversion; + + private static $colorspaceMapping = array( + PaletteInterface::PALETTE_CMYK => \Imagick::COLORSPACE_CMYK, + PaletteInterface::PALETTE_RGB => \Imagick::COLORSPACE_RGB, + PaletteInterface::PALETTE_GRAYSCALE => \Imagick::COLORSPACE_GRAY, + ); + + /** + * Constructs a new Image instance + * + * @param \Imagick $imagick + * @param PaletteInterface $palette + * @param MetadataBag $metadata + */ + public function __construct(\Imagick $imagick, PaletteInterface $palette, MetadataBag $metadata) + { + $this->metadata = $metadata; + $this->detectColorspaceConversionSupport(); + $this->imagick = $imagick; + if (static::$supportsColorspaceConversion) { + $this->setColorspace($palette); + } + $this->palette = $palette; + $this->layers = new Layers($this, $this->palette, $this->imagick); + } + + /** + * Destroys allocated imagick resources + */ + public function __destruct() + { + if ($this->imagick instanceof \Imagick) { + $this->imagick->clear(); + $this->imagick->destroy(); + } + } + + /** + * Returns the underlying \Imagick instance + * + * @return \Imagick + */ + public function getImagick() + { + return $this->imagick; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function copy() + { + try { + if (version_compare(phpversion("imagick"), "3.1.0b1", ">=") || defined("HHVM_VERSION")) { + $clone = clone $this->imagick; + } else { + $clone = $this->imagick->clone(); + } + } catch (\ImagickException $e) { + throw new RuntimeException('Copy operation failed', $e->getCode(), $e); + } + + return new self($clone, $this->palette, clone $this->metadata); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function crop(PointInterface $start, BoxInterface $size) + { + if (!$start->in($this->getSize())) { + throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders'); + } + try { + if ($this->layers()->count() > 1) { + // Crop each layer separately + $this->imagick = $this->imagick->coalesceImages(); + foreach ($this->imagick as $frame) { + $frame->cropImage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY()); + // Reset canvas for gif format + $frame->setImagePage(0, 0, 0, 0); + } + $this->imagick = $this->imagick->deconstructImages(); + } else { + $this->imagick->cropImage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY()); + // Reset canvas for gif format + $this->imagick->setImagePage(0, 0, 0, 0); + } + } catch (\ImagickException $e) { + throw new RuntimeException('Crop operation failed', $e->getCode(), $e); + } + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function flipHorizontally() + { + try { + $this->imagick->flopImage(); + } catch (\ImagickException $e) { + throw new RuntimeException('Horizontal Flip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function flipVertically() + { + try { + $this->imagick->flipImage(); + } catch (\ImagickException $e) { + throw new RuntimeException('Vertical flip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function strip() + { + try { + try { + $this->profile($this->palette->profile()); + } catch (\Exception $e) { + // here we discard setting the profile as the previous incorporated profile + // is corrupted, let's now strip the image + } + $this->imagick->stripImage(); + } catch (\ImagickException $e) { + throw new RuntimeException('Strip operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function paste(ImageInterface $image, PointInterface $start) + { + if (!$image instanceof self) { + throw new InvalidArgumentException(sprintf('Imagick\Image can only paste() Imagick\Image instances, %s given', get_class($image))); + } + + if (!$this->getSize()->contains($image->getSize(), $start)) { + throw new OutOfBoundsException('Cannot paste image of the given size at the specified position, as it moves outside of the current image\'s box'); + } + + try { + $this->imagick->compositeImage($image->imagick, \Imagick::COMPOSITE_DEFAULT, $start->getX(), $start->getY()); + } catch (\ImagickException $e) { + throw new RuntimeException('Paste operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDEFINED) + { + try { + if ($this->layers->count() > 1) { + $this->imagick = $this->imagick->coalesceImages(); + foreach ($this->imagick as $frame) { + $frame->resizeImage($size->getWidth(), $size->getHeight(), $this->getFilter($filter), 1); + } + $this->imagick = $this->imagick->deconstructImages(); + } else { + $this->imagick->resizeImage($size->getWidth(), $size->getHeight(), $this->getFilter($filter), 1); + } + } catch (\ImagickException $e) { + throw new RuntimeException('Resize operation failed', $e->getCode(), $e); + } + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function rotate($angle, ColorInterface $background = null) + { + $color = $background ? $background : $this->palette->color('fff'); + + try { + $pixel = $this->getColor($color); + + $this->imagick->rotateimage($pixel, $angle); + + $pixel->clear(); + $pixel->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Rotate operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function save($path = null, array $options = array()) + { + $path = null === $path ? $this->imagick->getImageFilename() : $path; + if (null === $path) { + throw new RuntimeException('You can omit save path only if image has been open from a file'); + } + + try { + $this->prepareOutput($options, $path); + $this->imagick->writeImages($path, true); + } catch (\ImagickException $e) { + throw new RuntimeException('Save operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function show($format, array $options = array()) + { + header('Content-type: '.$this->getMimeType($format)); + echo $this->get($format, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($format, array $options = array()) + { + try { + $options['format'] = $format; + $this->prepareOutput($options); + } catch (\ImagickException $e) { + throw new RuntimeException('Get operation failed', $e->getCode(), $e); + } + + return $this->imagick->getImagesBlob(); + } + + /** + * {@inheritdoc} + */ + public function interlace($scheme) + { + static $supportedInterlaceSchemes = array( + ImageInterface::INTERLACE_NONE => \Imagick::INTERLACE_NO, + ImageInterface::INTERLACE_LINE => \Imagick::INTERLACE_LINE, + ImageInterface::INTERLACE_PLANE => \Imagick::INTERLACE_PLANE, + ImageInterface::INTERLACE_PARTITION => \Imagick::INTERLACE_PARTITION, + ); + + if (!array_key_exists($scheme, $supportedInterlaceSchemes)) { + throw new InvalidArgumentException('Unsupported interlace type'); + } + + $this->imagick->setInterlaceScheme($supportedInterlaceSchemes[$scheme]); + + return $this; + } + + /** + * @param array $options + * @param string $path + */ + private function prepareOutput(array $options, $path = null) + { + if (isset($options['format'])) { + $this->imagick->setImageFormat($options['format']); + } + + if (isset($options['animated']) && true === $options['animated']) { + $format = isset($options['format']) ? $options['format'] : 'gif'; + $delay = isset($options['animated.delay']) ? $options['animated.delay'] : null; + $loops = isset($options['animated.loops']) ? $options['animated.loops'] : 0; + + $options['flatten'] = false; + + $this->layers->animate($format, $delay, $loops); + } else { + $this->layers->merge(); + } + $this->applyImageOptions($this->imagick, $options, $path); + + // flatten only if image has multiple layers + if ((!isset($options['flatten']) || $options['flatten'] === true) && count($this->layers) > 1) { + $this->flatten(); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->get('png'); + } + + /** + * {@inheritdoc} + */ + public function draw() + { + return new Drawer($this->imagick); + } + + /** + * {@inheritdoc} + */ + public function effects() + { + return new Effects($this->imagick); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + try { + $i = $this->imagick->getIteratorIndex(); + $this->imagick->rewind(); + $width = $this->imagick->getImageWidth(); + $height = $this->imagick->getImageHeight(); + $this->imagick->setIteratorIndex($i); + } catch (\ImagickException $e) { + throw new RuntimeException('Could not get size', $e->getCode(), $e); + } + + return new Box($width, $height); + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function applyMask(ImageInterface $mask) + { + if (!$mask instanceof self) { + throw new InvalidArgumentException('Can only apply instances of Symfony\Component\Image\Imagick\Image as masks'); + } + + $size = $this->getSize(); + $maskSize = $mask->getSize(); + + if ($size != $maskSize) { + throw new InvalidArgumentException(sprintf('The given mask doesn\'t match current image\'s size, Current mask\'s dimensions are %s, while image\'s dimensions are %s', $maskSize, $size)); + } + + $mask = $mask->mask(); + $mask->imagick->negateImage(true); + + try { + // remove transparent areas of the original from the mask + $mask->imagick->compositeImage($this->imagick, \Imagick::COMPOSITE_DSTIN, 0, 0); + $this->imagick->compositeImage($mask->imagick, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); + + $mask->imagick->clear(); + $mask->imagick->destroy(); + } catch (\ImagickException $e) { + throw new RuntimeException('Apply mask operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function mask() + { + $mask = $this->copy(); + + try { + $mask->imagick->modulateImage(100, 0, 100); + $mask->imagick->setImageMatte(false); + } catch (\ImagickException $e) { + throw new RuntimeException('Mask operation failed', $e->getCode(), $e); + } + + return $mask; + } + + /** + * {@inheritdoc} + * + * @return ImageInterface + */ + public function fill(FillInterface $fill) + { + try { + if ($this->isLinearOpaque($fill)) { + $this->applyFastLinear($fill); + } else { + $iterator = $this->imagick->getPixelIterator(); + + foreach ($iterator as $y => $pixels) { + foreach ($pixels as $x => $pixel) { + $color = $fill->getColor(new Point($x, $y)); + + $pixel->setColor((string) $color); + $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100); + } + + $iterator->syncIterator(); + } + } + } catch (\ImagickException $e) { + throw new RuntimeException('Fill operation failed', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function histogram() + { + try { + $pixels = $this->imagick->getImageHistogram(); + } catch (\ImagickException $e) { + throw new RuntimeException('Error while fetching histogram', $e->getCode(), $e); + } + + $image = $this; + + return array_map(function (\ImagickPixel $pixel) use ($image) { + return $image->pixelToColor($pixel); + },$pixels); + } + + /** + * {@inheritdoc} + */ + public function getColorAt(PointInterface $point) + { + if (!$point->in($this->getSize())) { + throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight())); + } + + try { + $pixel = $this->imagick->getImagePixelColor($point->getX(), $point->getY()); + } catch (\ImagickException $e) { + throw new RuntimeException('Error while getting image pixel color', $e->getCode(), $e); + } + + return $this->pixelToColor($pixel); + } + + /** + * Returns a color given a pixel, depending the Palette context + * + * Note : this method is public for PHP 5.3 compatibility + * + * @param \ImagickPixel $pixel + * + * @return ColorInterface + * + * @throws InvalidArgumentException In case a unknown color is requested + */ + public function pixelToColor(\ImagickPixel $pixel) + { + static $colorMapping = array( + ColorInterface::COLOR_RED => \Imagick::COLOR_RED, + ColorInterface::COLOR_GREEN => \Imagick::COLOR_GREEN, + ColorInterface::COLOR_BLUE => \Imagick::COLOR_BLUE, + ColorInterface::COLOR_CYAN => \Imagick::COLOR_CYAN, + ColorInterface::COLOR_MAGENTA => \Imagick::COLOR_MAGENTA, + ColorInterface::COLOR_YELLOW => \Imagick::COLOR_YELLOW, + ColorInterface::COLOR_KEYLINE => \Imagick::COLOR_BLACK, + // There is no gray component in \Imagick, let's use one of the RGB comp + ColorInterface::COLOR_GRAY => \Imagick::COLOR_RED, + ); + + $alpha = $this->palette->supportsAlpha() ? (int) round($pixel->getColorValue(\Imagick::COLOR_ALPHA) * 100) : null; + $palette = $this->palette(); + + return $this->palette->color(array_map(function ($color) use ($palette, $pixel, $colorMapping) { + if (!isset($colorMapping[$color])) { + throw new InvalidArgumentException(sprintf('Color %s is not mapped in Imagick', $color)); + } + $multiplier = 255; + if ($palette->name() === PaletteInterface::PALETTE_CMYK) { + $multiplier = 100; + } + + return $pixel->getColorValue($colorMapping[$color]) * $multiplier; + }, $this->palette->pixelDefinition()), $alpha); + } + + /** + * {@inheritdoc} + */ + public function layers() + { + return $this->layers; + } + + /** + * {@inheritdoc} + */ + public function usePalette(PaletteInterface $palette) + { + if (!isset(static::$colorspaceMapping[$palette->name()])) { + throw new InvalidArgumentException(sprintf('The palette %s is not supported by Imagick driver', $palette->name())); + } + + if ($this->palette->name() === $palette->name()) { + return $this; + } + + if (!static::$supportsColorspaceConversion) { + throw new RuntimeException('Your version of Imagick does not support colorspace conversions.'); + } + + try { + try { + $hasICCProfile = (Boolean) $this->imagick->getImageProfile('icc'); + } catch (\ImagickException $e) { + $hasICCProfile = false; + } + + if (!$hasICCProfile) { + $this->profile($this->palette->profile()); + } + + $this->profile($palette->profile()); + $this->setColorspace($palette); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to set colorspace', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function palette() + { + return $this->palette; + } + + /** + * {@inheritdoc} + */ + public function profile(ProfileInterface $profile) + { + try { + $this->imagick->profileImage('icc', $profile->data()); + } catch (\ImagickException $e) { + throw new RuntimeException(sprintf('Unable to add profile %s to image', $profile->name()), $e->getCode(), $e); + } + + return $this; + } + + /** + * Internal + * + * Flatten the image. + */ + private function flatten() + { + /** + * @see https://github.com/mkoppanen/imagick/issues/45 + */ + try { + if (method_exists($this->imagick, 'mergeImageLayers') && defined('Imagick::LAYERMETHOD_UNDEFINED')) { + $this->imagick = $this->imagick->mergeImageLayers(\Imagick::LAYERMETHOD_UNDEFINED); + } elseif (method_exists($this->imagick, 'flattenImages')) { + $this->imagick = $this->imagick->flattenImages(); + } + } catch (\ImagickException $e) { + throw new RuntimeException('Flatten operation failed', $e->getCode(), $e); + } + } + + /** + * Internal + * + * Applies options before save or output + * + * @param \Imagick $image + * @param array $options + * @param string $path + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + private function applyImageOptions(\Imagick $image, array $options, $path) + { + if (isset($options['format'])) { + $format = $options['format']; + } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) { + $format = $extension; + } else { + $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION); + } + + $format = strtolower($format); + + $options = $this->updateSaveOptions($options); + + if (isset($options['jpeg_quality']) && in_array($format, array('jpeg', 'jpg', 'pjpeg'))) { + $image->setImageCompressionQuality($options['jpeg_quality']); + } + + if ((isset($options['png_compression_level']) || isset($options['png_compression_filter'])) && $format === 'png') { + // first digit: compression level (default: 7) + if (isset($options['png_compression_level'])) { + if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) { + throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9'); + } + $compression = $options['png_compression_level'] * 10; + } else { + $compression = 70; + } + + // second digit: compression filter (default: 5) + if (isset($options['png_compression_filter'])) { + if ($options['png_compression_filter'] < 0 || $options['png_compression_filter'] > 9) { + throw new InvalidArgumentException('png_compression_filter option should be an integer from 0 to 9'); + } + $compression += $options['png_compression_filter']; + } else { + $compression += 5; + } + + $image->setImageCompressionQuality($compression); + } + + if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) { + if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) { + $image->setImageUnits(\Imagick::RESOLUTION_PIXELSPERCENTIMETER); + } elseif ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERINCH) { + $image->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH); + } else { + throw new RuntimeException('Unsupported image unit format'); + } + + $filter = ImageInterface::FILTER_UNDEFINED; + if (!empty($options['resampling-filter'])) { + $filter = $options['resampling-filter']; + } + + $image->setImageResolution($options['resolution-x'], $options['resolution-y']); + $image->resampleImage($options['resolution-x'], $options['resolution-y'], $this->getFilter($filter), 0); + } + } + + /** + * Gets specifically formatted color string from Color instance + * + * @param ColorInterface $color + * + * @return \ImagickPixel + */ + private function getColor(ColorInterface $color) + { + $pixel = new \ImagickPixel((string) $color); + $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100); + + return $pixel; + } + + /** + * Checks whether given $fill is linear and opaque + * + * @param FillInterface $fill + * + * @return Boolean + */ + private function isLinearOpaque(FillInterface $fill) + { + return $fill instanceof Linear && $fill->getStart()->isOpaque() && $fill->getEnd()->isOpaque(); + } + + /** + * Performs optimized gradient fill for non-opaque linear gradients + * + * @param Linear $fill + */ + private function applyFastLinear(Linear $fill) + { + $gradient = new \Imagick(); + $size = $this->getSize(); + $color = sprintf('gradient:%s-%s', (string) $fill->getStart(), (string) $fill->getEnd()); + + if ($fill instanceof Horizontal) { + $gradient->newPseudoImage($size->getHeight(), $size->getWidth(), $color); + $gradient->rotateImage(new \ImagickPixel(), 90); + } else { + $gradient->newPseudoImage($size->getWidth(), $size->getHeight(), $color); + } + + $this->imagick->compositeImage($gradient, \Imagick::COMPOSITE_OVER, 0, 0); + $gradient->clear(); + $gradient->destroy(); + } + + /** + * Internal + * + * Get the mime type based on format. + * + * @param string $format + * + * @return string mime-type + * + * @throws RuntimeException + */ + private function getMimeType($format) + { + static $mimeTypes = array( + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'wbmp' => 'image/vnd.wap.wbmp', + 'xbm' => 'image/xbm', + ); + + if (!isset($mimeTypes[$format])) { + throw new RuntimeException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(", ", array_keys($mimeTypes)), $format)); + } + + return $mimeTypes[$format]; + } + + /** + * Sets colorspace and image type, assigns the palette. + * + * @param PaletteInterface $palette + * + * @throws InvalidArgumentException + */ + private function setColorspace(PaletteInterface $palette) + { + $typeMapping = array( + // We use Matte variants to preserve alpha + // + // (the constants \Imagick::IMGTYPE_TRUECOLORMATTE and \Imagick::IMGTYPE_GRAYSCALEMATTE do not exist anymore in Imagick 7, + // to fix this the former values are hard coded here, the documentation under http://php.net/manual/en/imagick.settype.php + // doesn't tell us which constants to use and the alternative constants listed under + // https://pecl.php.net/package/imagick/3.4.3RC1 do not exist either, so we found no other way to fix it as to hard code + // the values here) + PaletteInterface::PALETTE_CMYK => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7, + PaletteInterface::PALETTE_RGB => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7, + PaletteInterface::PALETTE_GRAYSCALE => defined('\Imagick::IMGTYPE_GRAYSCALEMATTE') ? \Imagick::IMGTYPE_GRAYSCALEMATTE : 3, + ); + + if (!isset(static::$colorspaceMapping[$palette->name()])) { + throw new InvalidArgumentException(sprintf('The palette %s is not supported by Imagick driver', $palette->name())); + } + + $this->imagick->setType($typeMapping[$palette->name()]); + $this->imagick->setColorspace(static::$colorspaceMapping[$palette->name()]); + $this->palette = $palette; + } + + /** + * Older imagemagick versions does not support colorspace conversions. + * Let's detect if it is supported. + * + * @return Boolean + */ + private function detectColorspaceConversionSupport() + { + if (null !== static::$supportsColorspaceConversion) { + return static::$supportsColorspaceConversion; + } + + return static::$supportsColorspaceConversion = method_exists('Imagick', 'setColorspace'); + } + + /** + * Returns the filter if it's supported. + * + * @param string $filter + * + * @return string + * + * @throws InvalidArgumentException If the filter is unsupported. + */ + private function getFilter($filter = ImageInterface::FILTER_UNDEFINED) + { + static $supportedFilters = array( + ImageInterface::FILTER_UNDEFINED => \Imagick::FILTER_UNDEFINED, + ImageInterface::FILTER_BESSEL => \Imagick::FILTER_BESSEL, + ImageInterface::FILTER_BLACKMAN => \Imagick::FILTER_BLACKMAN, + ImageInterface::FILTER_BOX => \Imagick::FILTER_BOX, + ImageInterface::FILTER_CATROM => \Imagick::FILTER_CATROM, + ImageInterface::FILTER_CUBIC => \Imagick::FILTER_CUBIC, + ImageInterface::FILTER_GAUSSIAN => \Imagick::FILTER_GAUSSIAN, + ImageInterface::FILTER_HANNING => \Imagick::FILTER_HANNING, + ImageInterface::FILTER_HAMMING => \Imagick::FILTER_HAMMING, + ImageInterface::FILTER_HERMITE => \Imagick::FILTER_HERMITE, + ImageInterface::FILTER_LANCZOS => \Imagick::FILTER_LANCZOS, + ImageInterface::FILTER_MITCHELL => \Imagick::FILTER_MITCHELL, + ImageInterface::FILTER_POINT => \Imagick::FILTER_POINT, + ImageInterface::FILTER_QUADRATIC => \Imagick::FILTER_QUADRATIC, + ImageInterface::FILTER_SINC => \Imagick::FILTER_SINC, + ImageInterface::FILTER_TRIANGLE => \Imagick::FILTER_TRIANGLE + ); + + if (!array_key_exists($filter, $supportedFilters)) { + throw new InvalidArgumentException(sprintf( + 'The resampling filter "%s" is not supported by Imagick driver.', + $filter + )); + } + + return $supportedFilters[$filter]; + } +} diff --git a/src/Symfony/Component/Image/Imagick/Layers.php b/src/Symfony/Component/Image/Imagick/Layers.php new file mode 100644 index 0000000000000..860ee8f6f5331 --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Layers.php @@ -0,0 +1,271 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Image\AbstractLayers; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Exception\OutOfBoundsException; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Image\Palette\PaletteInterface; + +class Layers extends AbstractLayers +{ + /** + * @var Image + */ + private $image; + /** + * @var \Imagick + */ + private $resource; + /** + * @var integer + */ + private $offset = 0; + /** + * @var array + */ + private $layers = array(); + + private $palette; + + public function __construct(Image $image, PaletteInterface $palette, \Imagick $resource) + { + $this->image = $image; + $this->resource = $resource; + $this->palette = $palette; + } + + /** + * {@inheritdoc} + */ + public function merge() + { + foreach ($this->layers as $offset => $image) { + try { + $this->resource->setIteratorIndex($offset); + $this->resource->setImage($image->getImagick()); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to substitute layer', $e->getCode(), $e); + } + } + } + + /** + * {@inheritdoc} + */ + public function animate($format, $delay, $loops) + { + if ('gif' !== strtolower($format)) { + throw new InvalidArgumentException('Animated picture is currently only supported on gif'); + } + + if (!is_int($loops) || $loops < 0) { + throw new InvalidArgumentException('Loops must be a positive integer.'); + } + + if (null !== $delay && (!is_int($delay) || $delay < 0)) { + throw new InvalidArgumentException('Delay must be either null or a positive integer.'); + } + + try { + foreach ($this as $offset => $layer) { + $this->resource->setIteratorIndex($offset); + $this->resource->setFormat($format); + + if (null !== $delay) { + $layer->getImagick()->setImageDelay($delay / 10); + $layer->getImagick()->setImageTicksPerSecond(100); + } + $layer->getImagick()->setImageIterations($loops); + + $this->resource->setImage($layer->getImagick()); + } + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to animate layers', $e->getCode(), $e); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function coalesce() + { + try { + $coalescedResource = $this->resource->coalesceImages(); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to coalesce layers', $e->getCode(), $e); + } + + $count = $coalescedResource->getNumberImages(); + for ($offset = 0; $offset < $count; $offset++) { + try { + $coalescedResource->setIteratorIndex($offset); + $this->layers[$offset] = new Image($coalescedResource->getImage(), $this->palette, new MetadataBag()); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to retrieve layer', $e->getCode(), $e); + } + } + } + + /** + * {@inheritdoc} + */ + public function current() + { + return $this->extractAt($this->offset); + } + + /** + * Tries to extract layer at given offset + * + * @param integer $offset + * + * @return Image + * @throws RuntimeException + */ + private function extractAt($offset) + { + if (!isset($this->layers[$offset])) { + try { + $this->resource->setIteratorIndex($offset); + $this->layers[$offset] = new Image($this->resource->getImage(), $this->palette, new MetadataBag()); + } catch (\ImagickException $e) { + throw new RuntimeException(sprintf('Failed to extract layer %d', $offset), $e->getCode(), $e); + } + } + + return $this->layers[$offset]; + } + + /** + * {@inheritdoc} + */ + public function key() + { + return $this->offset; + } + + /** + * {@inheritdoc} + */ + public function next() + { + ++$this->offset; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->offset = 0; + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return $this->offset < count($this); + } + + /** + * {@inheritdoc} + */ + public function count() + { + try { + return $this->resource->getNumberImages(); + } catch (\ImagickException $e) { + throw new RuntimeException('Failed to count the number of layers', $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return is_int($offset) && $offset >= 0 && $offset < count($this); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->extractAt($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $image) + { + if (!$image instanceof Image) { + throw new InvalidArgumentException('Only an Imagick Image can be used as layer'); + } + + if (null === $offset) { + $offset = count($this) - 1; + } else { + if (!is_int($offset)) { + throw new InvalidArgumentException('Invalid offset for layer, it must be an integer'); + } + + if (count($this) < $offset || 0 > $offset) { + throw new OutOfBoundsException(sprintf('Invalid offset for layer, it must be a value between 0 and %d, %d given', count($this), $offset)); + } + + if (isset($this[$offset])) { + unset($this[$offset]); + $offset = $offset - 1; + } + } + + $frame = $image->getImagick(); + + try { + if (count($this) > 0) { + $this->resource->setIteratorIndex($offset); + } + $this->resource->addImage($frame); + } catch (\ImagickException $e) { + throw new RuntimeException('Unable to set the layer', $e->getCode(), $e); + } + + $this->layers = array(); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + try { + $this->extractAt($offset); + } catch (RuntimeException $e) { + return; + } + + try { + $this->resource->setIteratorIndex($offset); + $this->resource->removeImage(); + } catch (\ImagickException $e) { + throw new RuntimeException('Unable to remove layer', $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/Image/Imagick/Loader.php b/src/Symfony/Component/Image/Imagick/Loader.php new file mode 100644 index 0000000000000..46991c797473a --- /dev/null +++ b/src/Symfony/Component/Image/Imagick/Loader.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Imagick; + +use Symfony\Component\Image\Exception\NotSupportedException; +use Symfony\Component\Image\Image\AbstractLoader; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Exception\InvalidArgumentException; +use Symfony\Component\Image\Exception\RuntimeException; +use Symfony\Component\Image\Image\Palette\CMYK; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Image\Palette\Grayscale; + +/** + * Loader implementation using the Imagick PHP extension + */ +final class Loader extends AbstractLoader +{ + /** + * @throws RuntimeException + */ + public function __construct() + { + if (!class_exists('Imagick')) { + throw new RuntimeException('Imagick not installed'); + } + + $version = $this->getVersion(new \Imagick()); + + if (version_compare('6.2.9', $version) > 0) { + throw new RuntimeException(sprintf('ImageMagick version 6.2.9 or higher is required, %s provided', $version)); + } + } + + /** + * {@inheritdoc} + */ + public function open($path) + { + $path = $this->checkPath($path); + + try { + $imagick = new \Imagick($path); + $image = new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readFile($path)); + } catch (\Exception $e) { + throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e); + } + + return $image; + } + + /** + * {@inheritdoc} + */ + public function create(BoxInterface $size, ColorInterface $color = null) + { + $width = $size->getWidth(); + $height = $size->getHeight(); + + $palette = null !== $color ? $color->getPalette() : new RGB(); + $color = null !== $color ? $color : $palette->color('fff'); + + try { + $pixel = new \ImagickPixel((string) $color); + $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100); + + $imagick = new \Imagick(); + $imagick->newImage($width, $height, $pixel); + $imagick->setImageMatte(true); + $imagick->setImageBackgroundColor($pixel); + + if (version_compare('6.3.1', $this->getVersion($imagick)) < 0) { + if (method_exists($imagick, 'setImageAlpha')) { + $imagick->setImageAlpha($pixel->getColorValue(\Imagick::COLOR_ALPHA)); + } else { + $imagick->setImageOpacity($pixel->getColorValue(\Imagick::COLOR_ALPHA)); + } + } + + $pixel->clear(); + $pixel->destroy(); + + return new Image($imagick, $palette, new MetadataBag()); + } catch (\ImagickException $e) { + throw new RuntimeException('Could not create empty image', $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function load($string) + { + try { + $imagick = new \Imagick(); + + $imagick->readImageBlob($string); + $imagick->setImageMatte(true); + + return new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readData($string)); + } catch (\ImagickException $e) { + throw new RuntimeException('Could not load image from string', $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function read($resource) + { + if (!is_resource($resource)) { + throw new InvalidArgumentException('Variable does not contain a stream resource'); + } + + $content = stream_get_contents($resource); + + try { + $imagick = new \Imagick(); + $imagick->readImageBlob($content); + } catch (\ImagickException $e) { + throw new RuntimeException('Could not read image from resource', $e->getCode(), $e); + } + + return new Image($imagick, $this->createPalette($imagick), $this->getMetadataReader()->readData($content, $resource)); + } + + /** + * {@inheritdoc} + */ + public function font($file, $size, ColorInterface $color) + { + return new Font(new \Imagick(), $file, $size, $color); + } + + /** + * Returns the palette corresponding to an \Imagick resource colorspace + * + * @param \Imagick $imagick + * + * @return CMYK|Grayscale|RGB + * + * @throws NotSupportedException + */ + private function createPalette(\Imagick $imagick) + { + switch ($imagick->getImageColorspace()) { + case \Imagick::COLORSPACE_RGB: + case \Imagick::COLORSPACE_SRGB: + return new RGB(); + case \Imagick::COLORSPACE_CMYK: + return new CMYK(); + case \Imagick::COLORSPACE_GRAY: + return new Grayscale(); + default: + throw new NotSupportedException('Only RGB and CMYK colorspace are currently supported'); + } + } + + /** + * Returns ImageMagick version + * + * @param \Imagick $imagick + * + * @return string + */ + private function getVersion(\Imagick $imagick) + { + $v = $imagick->getVersion(); + list($version) = sscanf($v['versionString'], 'ImageMagick %s %04d-%02d-%02d %s %s'); + + return $version; + } +} diff --git a/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc b/src/Symfony/Component/Image/Resources/Adobe/CMYK/USWebUncoated.icc new file mode 100644 index 0000000000000000000000000000000000000000..75efcb259a48c2c0bc3b74ada6ff8cff090754ea GIT binary patch literal 557164 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 -qoEF-{>7w1-LE6ckpzYjQ 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 KW1s^E=xqQe&Y6|~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71e33830223c4c05c61002462e13df02bb30ae02 GIT binary patch literal 3048 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 K(|-m3{{sMwfHSB7 literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php new file mode 100644 index 0000000000000..75fbd1f2809f9 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Histogram\Bucket; +use Symfony\Component\Image\Image\Histogram\Range; + +if (class_exists(\PHPUnit_Framework_Constraint::class)) { + abstract class PHPUnitConstraint extends \PHPUnit_Framework_Constraint{} +} else { + abstract class PHPUnitConstraint extends Constraint{} +} + +class IsImageEqual extends PHPUnitConstraint +{ + /** + * @var \Symfony\Component\Image\Image\ImageInterface + */ + private $value; + + /** + * @var float + */ + private $delta; + + /** + * @var integer + */ + private $buckets; + + /** + * @param \Symfony\Component\Image\Image\ImageInterface $value + * @param float $delta + * @param integer $buckets + * + * @throws InvalidArgumentException + */ + public function __construct($value, $delta = 0.1, $buckets = 4) + { + if (!$value instanceof ImageInterface) { + throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class); + } + + if (!is_numeric($delta)) { + throw \PHPUnit_Util_InvalidArgumentHelper::factory(2, 'numeric'); + } + + if (!is_integer($buckets) || $buckets <= 0) { + throw \PHPUnit_Util_InvalidArgumentHelper::factory(3, 'integer'); + } + + $this->value = $value; + $this->delta = $delta; + $this->buckets = $buckets; + } + + /** + * {@inheritdoc} + */ + public function evaluate($other, $description = '', $returnResult = false) + { + if (!$other instanceof ImageInterface) { + throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class); + } + + list($currentRed, $currentGreen, $currentBlue, $currentAlpha) = $this->normalize($this->value); + list($otherRed, $otherGreen, $otherBlue, $otherAlpha) = $this->normalize($other); + + $total = 0; + + foreach ($currentRed as $bucket => $count) { + $total += abs($count - $otherRed[$bucket]); + } + + foreach ($currentGreen as $bucket => $count) { + $total += abs($count - $otherGreen[$bucket]); + } + + foreach ($currentBlue as $bucket => $count) { + $total += abs($count - $otherBlue[$bucket]); + } + + foreach ($currentAlpha as $bucket => $count) { + $total += abs($count - $otherAlpha[$bucket]); + } + + return $total <= $this->delta; + } + + /** + * {@inheritdoc} + */ + public function toString() + { + return sprintf('contains color histogram identical to expected %s', \PHPUnit_Util_Type::toString($this->value)); + } + + /** + * @param \Symfony\Component\Image\Image\ImageInterface $image + * + * @return array + */ + private function normalize(ImageInterface $image) + { + $step = (int) round(255 / $this->buckets); + + $red = + $green = + $blue = + $alpha = array(); + + for ($i = 1; $i <= $this->buckets; $i++) { + $range = new Range(($i - 1) * $step, $i * $step); + $red[] = new Bucket($range); + $green[] = new Bucket($range); + $blue[] = new Bucket($range); + $alpha[] = new Bucket($range); + } + + foreach ($image->histogram() as $color) { + foreach ($red as $bucket) { + $bucket->add($color->getRed()); + } + + foreach ($green as $bucket) { + $bucket->add($color->getGreen()); + } + + foreach ($blue as $bucket) { + $bucket->add($color->getBlue()); + } + + foreach ($alpha as $bucket) { + $bucket->add($color->getAlpha()); + } + } + + $total = $image->getSize()->square(); + + $callback = function (Bucket $bucket) use ($total) { + return count($bucket) / $total; + }; + + return array( + array_map($callback, $red), + array_map($callback, $green), + array_map($callback, $blue), + array_map($callback, $alpha), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php new file mode 100644 index 0000000000000..cf45706f4a137 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php @@ -0,0 +1,253 @@ + + * + * 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(__DIR__.'/../results/smiley.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/smiley.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/ellipse.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/ellipse.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/pie.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/pie.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/chord.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/chord.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/lines.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/lines.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/polygon.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/polygon.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/dot.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/dot.png')); + + unlink(__DIR__.'/../results/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(__DIR__.'/../results/arc.png'); + + $this->assertTrue(file_exists(__DIR__.'/../results/arc.png')); + + unlink(__DIR__.'/../results/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 = __DIR__.'/../results/bulat36.png'; + $file24 = __DIR__.'/../results/bulat24.png'; + $file18 = __DIR__.'/../results/bulat18.png'; + $file12 = __DIR__.'/../results/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->assertTrue(file_exists($file36)); + + unlink($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->assertTrue(file_exists($file24)); + + unlink($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->assertTrue(file_exists($file18)); + + unlink($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->assertTrue(file_exists($file12)); + + unlink($file12); + } + + private function getColor($color) + { + static $palette; + + if (!$palette) { + $palette = new RGB(); + } + + return $palette->color($color); + } + + /** + * @return LoaderInterface + */ + abstract protected function getLoader(); + + abstract protected function isFontTestSupported(); +} diff --git a/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php new file mode 100644 index 0000000000000..301a776211cb7 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Effects; + +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\LoaderInterface; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Tests\TestCase; + +abstract class AbstractEffectsTest extends TestCase +{ + + public function testNegate() + { + $palette = new RGB(); + $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..18b82364f9053 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Advanced; + +use Symfony\Component\Image\Filter\Advanced\Border; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class BorderTest extends FilterTestCase +{ + public function testBorderImage() + { + $color = $this->getMockBuilder(ColorInterface::class)->getMock(); + $width = 2; + $height = 4; + $image = $this->getImage(); + + $size = $this->getMockBuilder(BoxInterface::class)->getMock(); + $size->expects($this->once()) + ->method('getWidth') + ->will($this->returnValue($width)); + + $size->expects($this->once()) + ->method('getHeight') + ->will($this->returnValue($height)); + + $draw = $this->getDrawer(); + $draw->expects($this->exactly(4)) + ->method('line') + ->will($this->returnValue($draw)); + + $image->expects($this->once()) + ->method('getSize') + ->will($this->returnValue($size)); + + $image->expects($this->once()) + ->method('draw') + ->will($this->returnValue($draw)); + + $filter = new Border($color, $width, $height); + + $this->assertSame($image, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php new file mode 100644 index 0000000000000..7e72bc4c2784d --- /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..0d3f457dafd62 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Advanced; + +use Symfony\Component\Image\Filter\Advanced\Grayscale; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +class GrayscaleTest extends FilterTestCase +{ + /** + * @covers \Symfony\Component\Image\Filter\Advanced\Grayscale::apply + * + * @dataProvider getDataSet + * + * @param BoxInterface $size + * @param ColorInterface $color + * @param ColorInterface $filteredColor + */ + public function testGrayscaling(BoxInterface $size, ColorInterface $color, ColorInterface $filteredColor) + { + $image = $this->getImage(); + $imageWidth = $size->getWidth(); + $imageHeight = $size->getHeight(); + + $size = $this->getMockBuilder(BoxInterface::class)->getMock(); + $size->expects($this->once()) + ->method('getWidth') + ->will($this->returnValue($imageWidth)); + + $size->expects($this->once()) + ->method('getHeight') + ->will($this->returnValue($imageHeight)); + + $image->expects($this->any()) + ->method('getSize') + ->will($this->returnValue($size)); + + $image->expects($this->exactly($imageWidth*$imageHeight)) + ->method('getColorAt') + ->will($this->returnValue($color)); + + $color->expects($this->exactly($imageWidth*$imageHeight)) + ->method('grayscale') + ->will($this->returnValue($filteredColor)); + + $draw = $this->getDrawer(); + $draw->expects($this->exactly($imageWidth*$imageHeight)) + ->method('dot') + ->with($this->isInstanceOf(Point::class), $this->equalTo($filteredColor)); + + $image->expects($this->exactly($imageWidth*$imageHeight)) + ->method('draw') + ->will($this->returnValue($draw)); + + $filter = new Grayscale(); + $this->assertSame($image, $filter->apply($image)); + } + + /** + * Data provider for testShouldCanvasImageAndReturnResult + * + * @return array + */ + public function getDataSet() + { + return array( + array(new Box(20, 10), $this->getColor(), $this->getColor()), + array(new Box(10, 15), $this->getColor(), $this->getColor()), + array(new Box(12, 23), $this->getColor(), $this->getColor()), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php new file mode 100644 index 0000000000000..b13fd4793fcfb --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +USE Symfony\Component\Image\Filter\Basic\Autorotate; +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class AutorotateTest extends FilterTestCase +{ + /** + * @dataProvider provideMetadataAndRotations + */ + public function testApply($expectedRotation, $hFlipExpected, MetadataBag $metadata) + { + $image = $this->getImage(); + $image->expects($this->any()) + ->method('metadata') + ->will($this->returnValue($metadata)); + + if (null === $expectedRotation) { + $image->expects($this->never()) + ->method('rotate'); + } else { + $image->expects($this->once()) + ->method('rotate') + ->with($expectedRotation); + } + + $image->expects($hFlipExpected ? $this->once() : $this->never()) + ->method('flipHorizontally'); + + $filter = new Autorotate($this->getColor()); + $filter->apply($image); + } + + public function provideMetadataAndRotations() + { + return array( + array(null, false, new MetadataBag(array())), + array(null, false, new MetadataBag(array('ifd0.Orientation' => null))), + array(null, false, new MetadataBag(array('ifd0.Orientation' => 0))), + array(null, false, new MetadataBag(array('ifd0.Orientation' => 1))), + array(null, true, new MetadataBag(array('ifd0.Orientation' => 2))), + array(180, false, new MetadataBag(array('ifd0.Orientation' => 3))), + array(180, true, new MetadataBag(array('ifd0.Orientation' => 4))), + array(-90, true, new MetadataBag(array('ifd0.Orientation' => 5))), + array(90, false, new MetadataBag(array('ifd0.Orientation' => 6))), + array(90, true, new MetadataBag(array('ifd0.Orientation' => 7))), + array(-90, false, new MetadataBag(array('ifd0.Orientation' => 8))), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php new file mode 100644 index 0000000000000..76fb138006761 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Tests\Filter\FilterTestCase; +use Symfony\Component\Image\Filter\Basic\Copy; + +class CopyTest extends FilterTestCase +{ + public function testShouldCopyAndReturnResultingImage() + { + $command = new Copy(); + $image = $this->getImage(); + $clone = $this->getImage(); + + $image->expects($this->once()) + ->method('copy') + ->will($this->returnValue($clone)); + + $this->assertSame($clone, $command->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php new file mode 100644 index 0000000000000..4ce0acc61f426 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Filter\Basic\Crop; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class CropTest extends FilterTestCase +{ + /** + * @covers \Symfony\Component\Image\Filter\Basic\Crop::apply + * + * @dataProvider getDataSet + * + * @param PointInterface $start + * @param BoxInterface $size + */ + public function testShouldApplyCropAndReturnResult(PointInterface $start, BoxInterface $size) + { + $image = $this->getImage(); + + $command = new Crop($start, $size); + + $image->expects($this->once()) + ->method('crop') + ->with($start, $size) + ->will($this->returnValue($image)); + + $this->assertSame($image, $command->apply($image)); + } + + /** + * Provides coordinates and sizes for testShouldApplyCropAndReturnResult + * + * @return array + */ + public function getDataSet() + { + return array( + array(new Point(0, 0), new Box(40, 50)), + array(new Point(0, 15), new Box(50, 32)) + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php new file mode 100644 index 0000000000000..6f8236f9863c2 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\FlipHorizontally; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class FlipHorizontallyTest extends FilterTestCase +{ + public function testShouldFlipImage() + { + $image = $this->getImage(); + $filter = new FlipHorizontally(); + + $image->expects($this->once()) + ->method('flipHorizontally') + ->will($this->returnValue($image)); + + $this->assertSame($image, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php new file mode 100644 index 0000000000000..c1014c27f6cd3 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\FlipVertically; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class FlipVerticallyTest extends FilterTestCase +{ + public function testShouldFlipImage() + { + $image = $this->getImage(); + $filter = new FlipVertically(); + + $image->expects($this->once()) + ->method('flipVertically') + ->will($this->returnValue($image)); + + $this->assertSame($image, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php new file mode 100644 index 0000000000000..096d9147b4da7 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Filter\Basic\Paste; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class PasteTest extends FilterTestCase +{ + public function testShouldFlipImage() + { + $start = new Point(0, 0); + $image = $this->getImage(); + $toPaste = $this->getImage(); + $filter = new Paste($toPaste, $start); + + $image->expects($this->once()) + ->method('paste') + ->with($toPaste, $start) + ->will($this->returnValue($image)); + + $this->assertSame($image, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php new file mode 100644 index 0000000000000..2c0569298331c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Resize; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class ResizeTest extends FilterTestCase +{ + /** + * @covers \Symfony\Component\Image\Filter\Basic\Resize::apply + * + * @dataProvider getDataSet + * + * @param BoxInterface $size + */ + public function testShouldResizeImageAndReturnResult(BoxInterface $size) + { + $image = $this->getImage(); + + $image->expects($this->once()) + ->method('resize') + ->with($size) + ->will($this->returnValue($image)); + + $command = new Resize($size); + + $this->assertSame($image, $command->apply($image)); + } + + /** + * Data provider for testShouldResizeImageAndReturnResult + * + * @return array + */ + public function getDataSet() + { + return array( + array(new Box(50, 15)), + array(new Box(300, 25)), + array(new Box(123, 23)), + array(new Box(45, 23)) + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php new file mode 100644 index 0000000000000..e9ee8e0572ceb --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Rotate; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class RotateTest extends FilterTestCase +{ + public function testShouldRotateImageAndReturnResult() + { + $image = $this->getImage(); + $angle = 90; + $command = new Rotate($angle); + + $image->expects($this->once()) + ->method('rotate') + ->with($angle) + ->will($this->returnValue($image)); + + $this->assertSame($image, $command->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php new file mode 100644 index 0000000000000..b80987a5791f3 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Save; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class SaveTest extends FilterTestCase +{ + public function testShouldSaveImageAndReturnResult() + { + $image = $this->getImage(); + $path = '/path/to/image.jpg'; + $command = new Save($path); + + $image->expects($this->once()) + ->method('save') + ->with($path) + ->will($this->returnValue($image)); + + $this->assertSame($image, $command->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php new file mode 100644 index 0000000000000..38e5aebf541ed --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Show; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class ShowTest extends FilterTestCase +{ + public function testShouldShowImageAndReturnResult() + { + $image = $this->getImage(); + $format = 'jpg'; + $command = new Show($format); + + $image->expects($this->once()) + ->method('show') + ->with($format) + ->will($this->returnValue($image)); + + $this->assertSame($image, $command->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php new file mode 100644 index 0000000000000..208ee36b2ddc6 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Strip; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class StripTest extends FilterTestCase +{ + public function testShouldStripImage() + { + $image = $this->getImage(); + $filter = new Strip(); + + $image->expects($this->once()) + ->method('strip') + ->will($this->returnValue($image)); + + $this->assertSame($image, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php new file mode 100644 index 0000000000000..b24242c2e6b2c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\Thumbnail; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\ManipulatorInterface; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class ThumbnailTest extends FilterTestCase +{ + public function testShouldMakeAThumbnail() + { + $image = $this->getImage(); + $thumbnail = $this->getImage(); + $size = new Box(50, 50); + $filter = new Thumbnail($size); + + $image->expects($this->once()) + ->method('thumbnail') + ->with($size, ManipulatorInterface::THUMBNAIL_INSET) + ->will($this->returnValue($thumbnail)); + + $this->assertSame($thumbnail, $filter->apply($image)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php new file mode 100644 index 0000000000000..6be7368926ccc --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter\Basic; + +use Symfony\Component\Image\Filter\Basic\WebOptimization; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Tests\Filter\FilterTestCase; + +class WebOptimizationTest extends FilterTestCase +{ + public function testShouldNotSave() + { + $image = $this->getImage(); + $filter = new WebOptimization(); + + $image->expects($this->once()) + ->method('usePalette') + ->with($this->isInstanceOf(RGB::class)) + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('strip') + ->will($this->returnValue($image)); + + $image->expects($this->never()) + ->method('save'); + + $this->assertSame($image, $filter->apply($image)); + } + + public function testShouldSaveWithCallbackAndCustomOption() + { + $image = $this->getImage(); + $result = '/path/to/ploum'; + $path = function (ImageInterface $image) use ($result) { return $result; }; + $filter = new WebOptimization($path, array( + 'custom-option' => 'custom-value', + 'resolution-y' => 100, + )); + $capturedOptions = null; + + $image->expects($this->once()) + ->method('usePalette') + ->with($this->isInstanceOf(RGB::class)) + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('strip') + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('save') + ->with($this->equalTo($result), $this->isType('array')) + ->will($this->returnCallback(function ($path, $options) use (&$capturedOptions, $image) { + $capturedOptions = $options; + + return $image; + })); + + $this->assertSame($image, $filter->apply($image)); + + $this->assertCount(4, $capturedOptions); + $this->assertEquals('custom-value', $capturedOptions['custom-option']); + $this->assertEquals(ImageInterface::RESOLUTION_PIXELSPERINCH, $capturedOptions['resolution-units']); + $this->assertEquals(72, $capturedOptions['resolution-x']); + $this->assertEquals(100, $capturedOptions['resolution-y']); + } + + public function testShouldSaveWithPathAndCustomOption() + { + $image = $this->getImage(); + $path = '/path/to/dest'; + $filter = new WebOptimization($path, array( + 'custom-option' => 'custom-value', + 'resolution-y' => 100, + )); + $capturedOptions = null; + + $image->expects($this->once()) + ->method('usePalette') + ->with($this->isInstanceOf(RGB::class)) + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('strip') + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('save') + ->with($this->equalTo($path), $this->isType('array')) + ->will($this->returnCallback(function ($path, $options) use (&$capturedOptions, $image) { + $capturedOptions = $options; + + return $image; + })); + + $this->assertSame($image, $filter->apply($image)); + + $this->assertCount(4, $capturedOptions); + $this->assertEquals('custom-value', $capturedOptions['custom-option']); + $this->assertEquals(ImageInterface::RESOLUTION_PIXELSPERINCH, $capturedOptions['resolution-units']); + $this->assertEquals(72, $capturedOptions['resolution-x']); + $this->assertEquals(100, $capturedOptions['resolution-y']); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php new file mode 100644 index 0000000000000..aadc75159864f --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php @@ -0,0 +1,24 @@ +getLoader()->create(new Box(200, 200)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php b/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php new file mode 100644 index 0000000000000..cdaeb3475696e --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/FilterTestCase.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter; + +use Symfony\Component\Image\Draw\DrawerInterface; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\LoaderInterface; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Tests\TestCase; + +abstract class FilterTestCase extends TestCase +{ + protected function getImage() + { + return $this->getMockBuilder(ImageInterface::class)->getMock(); + } + + protected function getLoader() + { + return $this->getMockBuilder(LoaderInterface::class)->getMock(); + } + + protected function getDrawer() + { + return $this->getMockBuilder(DrawerInterface::class)->getMock(); + } + + protected function getPalette() + { + return $this->getMockBuilder(PaletteInterface::class)->getMock(); + } + + protected function getColor() + { + return $this->getMockBuilder(ColorInterface::class)->getMock(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php b/src/Symfony/Component/Image/Tests/Filter/LoaderAwareTest.php new file mode 100644 index 0000000000000..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..956ce09b93623 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Filter; + +use Symfony\Component\Image\Filter\FilterInterface; +use Symfony\Component\Image\Filter\Transformation; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\ManipulatorInterface; + +class TransformationTest extends FilterTestCase +{ + public function testSimpleStack() + { + $image = $this->getImage(); + $size = new Box(50, 50); + $path = sys_get_temp_dir(); + + $image->expects($this->once()) + ->method('resize') + ->with($size) + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('save') + ->with($path) + ->will($this->returnValue($image)); + + $transformation = new Transformation(); + $this->assertSame($image, $transformation->resize($size) + ->save($path) + ->apply($image) + ); + } + + public function testComplexFlow() + { + $image = $this->getImage(); + $clone = $this->getImage(); + $thumbnail = $this->getImage(); + $path = sys_get_temp_dir(); + $size = new Box(50, 50); + $resize = new Box(200, 200); + $angle = 90; + $background = $this->getPalette()->color('fff'); + + $image->expects($this->once()) + ->method('resize') + ->with($resize) + ->will($this->returnValue($image)); + + $image->expects($this->once()) + ->method('copy') + ->will($this->returnValue($clone)); + + $clone->expects($this->once()) + ->method('rotate') + ->with($angle, $background) + ->will($this->returnValue($clone)); + + $clone->expects($this->once()) + ->method('thumbnail') + ->with($size, ManipulatorInterface::THUMBNAIL_INSET) + ->will($this->returnValue($thumbnail)); + + $thumbnail->expects($this->once()) + ->method('save') + ->with($path) + ->will($this->returnValue($thumbnail)); + + $transformation = new Transformation(); + + $transformation->resize($resize) + ->copy() + ->rotate($angle, $background) + ->thumbnail($size, ManipulatorInterface::THUMBNAIL_INSET) + ->save($path); + + $this->assertSame($thumbnail, $transformation->apply($image)); + } + + public function testCropFlipPasteShow() + { + $img1 = $this->getImage(); + $img2 = $this->getImage(); + $start = new Point(0, 0); + $size = new Box(50, 50); + + $img1->expects($this->once()) + ->method('paste') + ->with($img2, $start) + ->will($this->returnValue($img1)); + + $img1->expects($this->once()) + ->method('show') + ->with('png') + ->will($this->returnValue($img1)); + + $img2->expects($this->once()) + ->method('flipHorizontally') + ->will($this->returnValue($img2)); + + $img2->expects($this->once()) + ->method('flipVertically') + ->will($this->returnValue($img2)); + + $img2->expects($this->once()) + ->method('crop') + ->with($start, $size) + ->will($this->returnValue($img2)); + + $transformation2 = new Transformation(); + $transformation2->flipHorizontally() + ->flipVertically() + ->crop($start, $size); + + $transformation1 = new Transformation(); + $transformation1->paste($transformation2->apply($img2), $start) + ->show('png') + ->apply($img1); + } + + public function testFilterSorting() + { + $filter1 = new TestFilter(); + $filter2 = new TestFilter(); + $filter3 = new TestFilter(); + + $transformation1 = new Transformation(); + $transformation1 + ->add($filter1, 5) + ->add($filter2, -3) + ->add($filter3); + + $expected1 = array( + $filter2, + $filter3, + $filter1, + ); + + $transformation2 = new Transformation(); + $transformation2 + ->add($filter1) + ->add($filter2) + ->add($filter3); + + $expected2 = array( + $filter1, + $filter2, + $filter3, + ); + + $this->assertSame($expected1, $transformation1->getFilters()); + $this->assertSame($expected2, $transformation2->getFilters()); + } + + public function testGetEmptyFilters() + { + $transformation = new Transformation(); + $this->assertSame(array(), $transformation->getFilters()); + } +} + +class TestFilter implements FilterInterface +{ + /** + * {@inheritdoc} + */ + public function apply(ImageInterface $image) + { + } +} diff --git a/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php new file mode 100644 index 0000000000000..e0de826fb3a2b --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.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\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()); + + unlink($new); + } +} diff --git a/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php b/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php new file mode 100644 index 0000000000000..63feeaa14c051 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Gd/DrawerTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Gd; + +use Symfony\Component\Image\Gd\Loader; +use Symfony\Component\Image\Tests\Draw\AbstractDrawerTest; + +class DrawerTest extends AbstractDrawerTest +{ + protected function setUp() + { + parent::setUp(); + + if (!function_exists('gd_info')) { + $this->markTestSkipped('Gd not installed'); + } + } + + protected function getLoader() + { + return new Loader(); + } + + protected function isFontTestSupported() + { + $infos = gd_info(); + + return isset($infos['FreeType Support']) ? $infos['FreeType Support'] : false; + } +} diff --git a/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php b/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php new file mode 100644 index 0000000000000..ef00329915a43 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Gd/EffectsTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Gd; + +use Symfony\Component\Image\Gd\Loader; +use Symfony\Component\Image\Tests\Effects\AbstractEffectsTest; + +class EffectsTest extends AbstractEffectsTest +{ + protected function setUp() + { + parent::setUp(); + + if (!function_exists('gd_info')) { + $this->markTestSkipped('Gd not installed'); + } + } + + protected function getLoader() + { + return new Loader(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Gd/ImageTest.php b/src/Symfony/Component/Image/Tests/Gd/ImageTest.php new file mode 100644 index 0000000000000..e00387f88ac34 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Gd/ImageTest.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Gd; + +use Symfony\Component\Image\Gd\Loader; +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Tests\Image\AbstractImageTest; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Exception\RuntimeException; + +class ImageTest extends AbstractImageTest +{ + protected function setUp() + { + parent::setUp(); + + if (!function_exists('gd_info')) { + $this->markTestSkipped('Gd not installed'); + } + } + + public function testImageResolutionChange() + { + $this->markTestSkipped('GD driver does not support resolution options'); + } + + public function provideFilters() + { + return array( + array(ImageInterface::FILTER_UNDEFINED), + ); + } + + public function providePalettes() + { + return array( + array(RGB::class, array(255, 0, 0)), + ); + } + + public function provideFromAndToPalettes() + { + return array( + array( + RGB::class, + RGB::class, + array(10, 10, 10), + ), + ); + } + + public function testProfile() + { + try { + parent::testProfile(); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + $this->assertSame('GD driver does not support color profiles', $e->getMessage()); + } + } + + public function testPaletteIsGrayIfGrayImage() + { + $this->markTestSkipped('Gd does not support Gray colorspace'); + } + + public function testPaletteIsCMYKIfCMYKImage() + { + $this->markTestSkipped('GD driver does not recognize CMYK images properly'); + } + + public function testGetColorAtCMYK() + { + $this->markTestSkipped('GD driver does not recognize CMYK images properly'); + } + + public function testChangeColorSpaceAndStripImage() + { + $this->markTestSkipped('GD driver does not support ICC profiles'); + } + + public function testStripImageWithInvalidProfile() + { + $this->markTestSkipped('GD driver does not support ICC profiles'); + } + + public function testStripGBRImageHasGoodColors() + { + $this->markTestSkipped('GD driver does not support ICC profiles'); + } + + protected function getLoader() + { + return new Loader(); + } + + protected function supportMultipleLayers() + { + return false; + } + + public function testRotateWithNoBackgroundColor() + { + if (version_compare(PHP_VERSION, '5.5', '>=')) { + // see https://bugs.php.net/bug.php?id=65148 + $this->markTestSkipped('Disabling test while bug #65148 is open'); + } + + parent::testRotateWithNoBackgroundColor(); + } + + /** + * @dataProvider provideVariousSources + */ + public function testResolutionOnSave($source) + { + $this->markTestSkipped('Gd only supports 72 dpi resolution'); + } + + protected function getImageResolution(ImageInterface $image) + { + } +} diff --git a/src/Symfony/Component/Image/Tests/Gd/LayersTest.php b/src/Symfony/Component/Image/Tests/Gd/LayersTest.php new file mode 100644 index 0000000000000..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..a4e914855b842 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.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\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..0d913aa91d807 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php @@ -0,0 +1,817 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image; + +use Symfony\Component\Image\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(__DIR__ . '/tmp.jpg'); + + $image = $this->getLoader()->open(__DIR__ . '/tmp.jpg'); + + $this->assertInstanceOf($to, $image->palette()); + unlink(__DIR__ . '/tmp.jpg'); + } + + public function testSaveWithoutFormatShouldSaveInOriginalFormat() + { + if (!extension_loaded('exif')) { + $this->markTestSkipped('The EXIF extension is required for this test'); + } + + $tmpFile = __DIR__ . '/tmpfile'; + + $this + ->getLoader() + ->open(FixturesLoader::getFixture('large.jpg')) + ->save($tmpFile); + + $data = exif_read_data($tmpFile); + $this->assertEquals('image/jpeg', $data['MimeType']); + unlink($tmpFile); + } + + public function testSaveWithoutPathFileFromImageLoadShouldBeOkay() + { + $source = FixturesLoader::getFixture('google.png'); + $tmpFile = __DIR__ . '/../results/google.tmp.png'; + + if (file_exists($tmpFile)) { + unlink($tmpFile); + } + + copy($source, $tmpFile); + + $this->assertEquals(md5_file($source), md5_file($tmpFile)); + + $this + ->getLoader() + ->open($tmpFile) + ->resize(new Box(20, 20)) + ->save(); + + $this->assertNotEquals(md5_file($source), md5_file($tmpFile)); + unlink($tmpFile); + } + + public function testSaveWithoutPathFileFromImageCreationShouldFail() + { + $image = $this->getLoader()->create(new Box(20, 20)); + $this->setExpectedException(RuntimeException::class); + $image->save(); + } + + public function provideFromAndToPalettes() + { + $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(__DIR__.'/../results/mask.png'); + + $size = $factory->open(__DIR__.'/../results/mask.png') + ->getSize(); + + $this->assertEquals(364, $size->getWidth()); + $this->assertEquals(126, $size->getHeight()); + + unlink(__DIR__.'/../results/mask.png'); + } + + 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 = __DIR__.'/../results/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']); + } + + unlink($outfile); + } + + public function testInOutResult() + { + $this->processInOut("trans", "png","png"); + $this->processInOut("trans", "png","gif"); + $this->processInOut("trans", "png","jpg"); + $this->processInOut("anima", "gif","png"); + $this->processInOut("anima", "gif","gif"); + $this->processInOut("anima", "gif","jpg"); + $this->processInOut("trans", "gif","png"); + $this->processInOut("trans", "gif","gif"); + $this->processInOut("trans", "gif","jpg"); + $this->processInOut("large", "jpg","png"); + $this->processInOut("large", "jpg","gif"); + $this->processInOut("large", "jpg","jpg"); + } + + public function testLayerReturnsALayerInterface() + { + $factory = $this->getLoader(); + + $image = $factory->open(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(__DIR__.'/../results/anima-half-size.gif', array('animated' => true)); + @unlink(__DIR__.'/../results/anima-half-size.gif'); + + $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 = __DIR__.'/../results/anima2-half-size.gif'; + $image->save($target, array('animated' => true)); + + $this->assertFileExists($target); + + @unlink($target); + } + + public function testMetadataReturnsMetadataInstance() + { + $this->assertInstanceOf(MetadataBag::class, $this->getMonoLayeredImage()->metadata()); + } + + public function testCloningImageResultsInNewMetadataInstance() + { + $image = $this->getMonoLayeredImage(); + $originalMetadata = $image->metadata(); + $clone = clone $image; + $this->assertNotSame($originalMetadata, $clone->metadata(), 'The image\'s metadata is the same after cloning the image, but must be a new instance.'); + } + + public function testImageSizeOnAnimatedGif() + { + $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)); + unlink($file); + } + + 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); + if (!is_dir(__DIR__.'/../results/in_out')) { + mkdir(__DIR__.'/../results/in_out', 0777, true); + } + $target = __DIR__."/../results/in_out/{$class}_{$file}_from_{$in}_to.{$out}"; + $thumb->save($target); + + $this->assertFileExists($target); + unlink($target); + } + + /** + * @return \Symfony\Component\Image\Image\LoaderInterface + */ + abstract protected function getLoader(); + + /** + * @return boolean + */ + abstract protected function supportMultipleLayers(); +} diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php new file mode 100644 index 0000000000000..9461a0b34508b --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php @@ -0,0 +1,283 @@ + + * + * 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 = __DIR__ . '/../results/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + )); + + $this->assertFileExists($target); + + @unlink($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 = __DIR__ . '/../results/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + 'animated.delay' => $delay, + 'animated.loops' => $loops, + )); + + $this->assertFileExists($target); + + @unlink($target); + } + + public function provideAnimationParameters() + { + return array( + array(0, 0), + array(500, 0), + array(0, 10), + array(5000, 10), + ); + } + + /** + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + * @dataProvider provideWrongAnimationParameters + */ + public function testAnimateWithWrongParameters($delay, $loops) + { + $image = $this->getImage(FixturesLoader::getFixture('pink.gif')); + $layers = $image->layers(); + + $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); + $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); + + $target = __DIR__ . '/../results/temporary-gif.gif'; + + $image->save($target, array( + 'animated' => true, + 'animated.delay' => $delay, + 'animated.loops' => $loops, + )); + + @unlink($target); + } + + public function provideWrongAnimationParameters() + { + return array( + array(-1, 0), + array(500, -1), + array(-1, 10), + array(0, -1), + ); + } + + public function provideInvalidArguments() + { + return array( + array('lambda'), + array('0'), + array('1'), + array(1.0), + ); + } + + public function provideOutOfBoundsArguments() + { + return array( + array(-1), + array(2), + ); + } + + abstract protected function getImage($path = null); + + /** + * @return LoaderInterface + */ + abstract protected function getLoader(); + abstract protected function assertLayersEquals($expected, $actual); +} diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php new file mode 100644 index 0000000000000..87c0222274b26 --- /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..29c1c1406f2ec --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/BoxTest.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image; + +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Tests\TestCase; + +class BoxTest extends TestCase +{ + /** + * @covers \Symfony\Component\Image\Image\Box::getWidth + * @covers \Symfony\Component\Image\Image\Box::getHeight + * + * @dataProvider getSizes + * + * @param integer $width + * @param integer $height + */ + public function testShouldAssignWidthAndHeight($width, $height) + { + $size = new Box($width, $height); + + $this->assertEquals($width, $size->getWidth()); + $this->assertEquals($height, $size->getHeight()); + } + + /** + * Data provider for testShouldAssignWidthAndHeight + * + * @return array + */ + public function getSizes() + { + return array( + array(1, 1), + array(10, 10), + array(15, 36) + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Box::__construct + * + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + * + * @dataProvider getInvalidSizes + * + * @param integer $width + * @param integer $height + */ + public function testShouldThrowExceptionOnInvalidSize($width, $height) + { + new Box($width, $height); + } + + /** + * Data provider for testShouldThrowExceptionOnInvalidSize + * + * @return array + */ + public function getInvalidSizes() + { + return array( + array(0, 0), + array(15, 0), + array(0, 25), + array(-1, 4) + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Box::contains + * + * @dataProvider getSizeBoxStartAndExpected + * + * @param BoxInterface $size + * @param BoxInterface $box + * @param PointInterface $start + * @param Boolean $expected + */ + public function testShouldDetermineIfASizeContainsABoxAtAStartPosition( + BoxInterface $size, + BoxInterface $box, + PointInterface $start, + $expected + ) { + $this->assertEquals($expected, $size->contains($box, $start)); + } + + /** + * Data provider for testShouldDetermineIfASizeContainsABoxAtAStartPosition + * + * @return array + */ + public function getSizeBoxStartAndExpected() + { + return array( + array(new Box(50, 50), new Box(30, 30), new Point(0, 0), true), + array(new Box(50, 50), new Box(30, 30), new Point(20, 20), true), + array(new Box(50, 50), new Box(30, 30), new Point(21, 21), false), + array(new Box(50, 50), new Box(30, 30), new Point(21, 20), false), + array(new Box(50, 50), new Box(30, 30), new Point(20, 22), false), + ); + } + + /** + * @cover Symfony\Component\Image\Image\Box::__toString + */ + public function testToString() + { + $this->assertEquals('100x100 px', (string) new Box(100, 100)); + } + + public function testShouldScaleBox() + { + $box = new Box(10, 20); + + $this->assertEquals(new Box(100, 200), $box->scale(10)); + } + + public function testShouldIncreaseBox() + { + $box = new Box(10, 20); + + $this->assertEquals(new Box(15, 25), $box->increase(5)); + } + + /** + * @dataProvider getSizesAndSquares + * + * @param integer $width + * @param integer $height + * @param integer $square + */ + public function testShouldCalculateSquare($width, $height, $square) + { + $box = new Box($width, $height); + + $this->assertEquals($square, $box->square()); + } + + public function getSizesAndSquares() + { + return array( + array(10, 15, 150), + array(2, 2, 4), + array(9, 8, 72), + ); + } + + /** + * @dataProvider getDimensionsAndTargets + * + * @param integer $width + * @param integer $height + * @param integer $targetWidth + * @param integer $targetHeight + */ + public function testShouldResizeToTargetWidthAndHeight($width, $height, $targetWidth, $targetHeight) + { + $box = new Box($width, $height); + $expected = new Box($targetWidth, $targetHeight); + + $this->assertEquals($expected, $box->widen($targetWidth)); + $this->assertEquals($expected, $box->heighten($targetHeight)); + } + + public function getDimensionsAndTargets() + { + return array( + array(10, 50, 50, 250), + array(25, 40, 50, 80), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php new file mode 100644 index 0000000000000..65669f9faa624 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Symfony\Component\Image\Tests\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\Fill\Gradient\Horizontal; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Point; + +class HorizontalTest extends LinearTest +{ + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd() + */ + protected function getEnd() + { + return $this->getColor('fff'); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart() + */ + protected function getStart() + { + return $this->getColor('000'); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask() + */ + protected function getFill(ColorInterface $start, ColorInterface $end) + { + return new Horizontal(100, $start, $end); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades() + */ + public function getPointsAndColors() + { + return array( + array($this->getColor('fff'), new Point(100, 5)), + array($this->getColor('000'), new Point(0, 15)), + array($this->getColor(array(128, 128, 128)), new Point(50, 25)) + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php new file mode 100644 index 0000000000000..cf85e74ac1490 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Tests\TestCase; + +abstract class LinearTest extends TestCase +{ + /** + * @var \Symfony\Component\Image\Image\Fill\FillInterface + */ + private $fill; + + /** + * @var ColorInterface + */ + private $start; + + /** + * @var ColorInterface + */ + private $end; + protected $palette; + + protected function setUp() + { + $this->start = $this->getStart(); + $this->end = $this->getEnd(); + $this->fill = $this->getFill($this->start, $this->end); + } + + /** + * @dataProvider getPointsAndColors + * + * @param integer $shade + * @param \Symfony\Component\Image\Image\PointInterface $position + */ + public function testShouldProvideCorrectColorsValues(ColorInterface $color, PointInterface $position) + { + $this->assertEquals($color, $this->fill->getColor($position)); + } + + /** + * @covers \Symfony\Component\Image\Image\Fill\Gradient\Linear::getStart + * @covers \Symfony\Component\Image\Image\Fill\Gradient\Linear::getEnd + */ + public function testShouldReturnCorrectStartAndEnd() + { + $this->assertSame($this->start, $this->fill->getStart()); + $this->assertSame($this->end, $this->fill->getEnd()); + } + + protected function getColor($color) + { + static $palette; + + if (!$palette) { + $palette = new RGB(); + } + + return $palette->color($color); + } + + /** + * @param ColorInterface $start + * @param ColorInterface $end + * + * @return Symfony\Component\Image\Image\Fill\FillInterface + */ + abstract protected function getFill(ColorInterface $start, ColorInterface $end); + + /** + * @return ColorInterface + */ + abstract protected function getStart(); + + /** + * @return ColorInterface + */ + abstract protected function getEnd(); + + /** + * @return array + */ + abstract public function getPointsAndColors(); +} diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php new file mode 100644 index 0000000000000..fcb7fc1fb4f02 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Symfony\Component\Image\Tests\Image\Fill\Gradient; + +use Symfony\Component\Image\Image\Fill\Gradient\Vertical; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Point; + +class VerticalTest extends LinearTest +{ + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd() + */ + protected function getEnd() + { + return $this->getColor('fff'); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart() + */ + protected function getStart() + { + return $this->getColor('000'); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask() + */ + protected function getFill(ColorInterface $start, ColorInterface $end) + { + return new Vertical(100, $start, $end); + } + + /** + * (non-PHPdoc) + * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades() + */ + public function getPointsAndColors() + { + return array( + array($this->getColor('fff'), new Point(5, 100)), + array($this->getColor('000'), new Point(15, 0)), + array($this->getColor(array(128, 128, 128)), new Point(25, 50)) + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php b/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php new file mode 100644 index 0000000000000..4c2b73b44a19c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Histogram; + +use Symfony\Component\Image\Image\Histogram\Bucket; +use Symfony\Component\Image\Image\Histogram\Range; +use Symfony\Component\Image\Tests\TestCase; + +class BucketTest extends TestCase +{ + private $bucket; + + protected function setUp() + { + $this->bucket = new Bucket(new Range(0, 63)); + $this->assertInstanceOf('Countable', $this->bucket); + } + + /** + * @dataProvider getCountAndValues + * + * @param integer $count + * @param array $values + */ + public function testShouldOnlyRegisterValuesInRange($count, array $values) + { + foreach ($values as $value) { + $this->bucket->add($value); + } + + $this->assertEquals($count, $this->bucket->count()); + } + + public function getCountAndValues() + { + return array( + array(3, array(12, 123, 232, 142, 152, 172, 93, 35, 44)), + array(6, array(12, 123, 23, 14, 152, 17, 93, 35, 44)), + array(8, array(12, 12, 12, 23, 14, 152, 17, 93, 35, 44)), + array(0, array(121, 123, 234, 145, 152, 176, 93, 135, 144)), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php new file mode 100644 index 0000000000000..552900b2b89aa --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Histogram; + +use Symfony\Component\Image\Image\Histogram\Range; +use Symfony\Component\Image\Tests\TestCase; + +class RangeTest extends TestCase +{ + private $start = 0; + private $end = 63; + + /** + * @dataProvider getExpectedResultsAndValues + * + * @param Boolean $contains + * @param integer $value + */ + public function testShouldDetermineIfContainsValue($contains, $value) + { + $range = new Range($this->start, $this->end); + + $this->assertEquals($contains, $range->contains($value)); + } + + public function getExpectedResultsAndValues() + { + return array( + array(true, 12), + array(true, 0), + array(false, 128), + array(false, 63), + ); + } + + /** + * @expectedException \Symfony\Component\Image\Exception\OutOfBoundsException + */ + public function testShouldThrowExceptionIfEndIsSmallerThanStart() + { + new Range($this->end, $this->start); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php b/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php new file mode 100644 index 0000000000000..66a2e30fffc1a --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Metadata/DefaultMetadataReaderTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Metadata; + +use Symfony\Component\Image\Image\Metadata\DefaultMetadataReader; + +class DefaultMetadataReaderTest extends MetadataReaderTestCase +{ + protected function getReader() + { + return new DefaultMetadataReader(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php b/src/Symfony/Component/Image/Tests/Image/Metadata/ExifMetadataReaderTest.php new file mode 100644 index 0000000000000..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..f97b8d48f7861 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php @@ -0,0 +1,96 @@ + + * + * 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..9b65b2821b556 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette; + +use Symfony\Component\Image\Image\ProfileInterface; +use Symfony\Component\Image\Tests\TestCase; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; + +abstract class AbstractPaletteTest extends TestCase +{ + /** + * @dataProvider provideColorAndAlphaTuples + */ + public function testColor($expected, $color, $alpha) + { + $result = $this->getPalette()->color($color, $alpha); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertEquals((string) $expected, (string) $result); + } + + /** + * @dataProvider provideColorAndAlpha + */ + public function testColorIsCached($color, $alpha) + { + $this->assertSame($this->getPalette()->color($color, $alpha), $this->getPalette()->color($color, $alpha)); + } + + /** + * @dataProvider provideColorAndAlpha + */ + public function testColorWithDifferentAlphasAreNotSame($color, $alpha) + { + $this->assertNotSame($this->getPalette()->color($color, 2), $this->getPalette()->color($color, 0)); + } + + /** + * @dataProvider provideColorsForBlending + */ + public function testBlend($expected, $color1, $color2, $amount) + { + $result = $this->getPalette()->blend($color1, $color2, $amount); + $this->assertInstanceOf(ColorInterface::class, $result); + $this->assertEquals((string) $expected, (string) $result); + } + + public function testUseProfile() + { + $this->getMockBuilder(ProfileInterface::class)->getMock(); + + $palette = $this->getPalette(); + + $new = $this->getMockBuilder(ProfileInterface::class)->getMock(); + $palette->useProfile($new); + + $this->assertEquals($new, $palette->profile()); + + } + + public function testProfile() + { + $this->assertInstanceOf(ProfileInterface::class, $this->getPalette()->profile()); + } + + public function testName() + { + $this->assertInternalType('string', $this->getPalette()->name()); + } + + public function testPixelDefinition() + { + $this->assertInternalType('array', $this->getPalette()->pixelDefinition()); + + $available = array( + ColorInterface::COLOR_RED, + ColorInterface::COLOR_GREEN, + ColorInterface::COLOR_BLUE, + ColorInterface::COLOR_CYAN, + ColorInterface::COLOR_MAGENTA, + ColorInterface::COLOR_YELLOW, + ColorInterface::COLOR_KEYLINE, + ColorInterface::COLOR_GRAY, + ); + + foreach ($this->getPalette()->pixelDefinition() as $color) { + $this->assertTrue(in_array($color, $available)); + } + } + + public function testSupportsAlpha() + { + $this->assertInternalType('boolean', $this->getPalette()->supportsAlpha()); + } + + abstract public function provideColorAndAlphaTuples(); + + abstract public function provideColorsForBlending(); + + /** + * @return PaletteInterface + */ + abstract protected function getPalette(); +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php new file mode 100644 index 0000000000000..91daf77009713 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette; + +use Symfony\Component\Image\Image\Palette\CMYK; +use Symfony\Component\Image\Image\Palette\Color\CMYK as CMYKColor; + +class CMYKTest extends AbstractPaletteTest +{ + public function provideColorAndAlphaTuples() + { + $palette = $this->getPalette(); + + return array( + array(new CMYKColor($palette, array(1, 2, 3, 4)), array(1, 2, 3, 4), null), + array(new CMYKColor($palette, array(4, 3, 2, 1)), array(4, 3, 2, 1), null), + array(new CMYKColor($palette, array(0, 33, 67, 99)), array(3, 2, 1), null), + array(new CMYKColor($palette, array(0, 0, 0, 0)), array(255, 255, 255), null), + array(new CMYKColor($palette, array(0, 0, 0, 100)), array(0, 0, 0), null), + ); + } + + public function provideColorAndAlpha() + { + return array( + array(array(4, 3, 2, 1), null) + ); + } + + public function testColorWithDifferentAlphasAreNotSame($color = null, $alpha = null) + { + $this->markTestSkipped('CMYK does not support alpha'); + } + + public function provideColorsForBlending() + { + $palette = $this->getPalette(); + + return array( + array( + new CMYKColor($palette, array(56, 29, 38, 48)), + new CMYKColor($palette, array(1, 2, 3, 4)), + new CMYKColor($palette, array(50, 25, 32, 40)), + 1.1, + ), + array( + new CMYKColor($palette, array(21, 12, 15, 20)), + new CMYKColor($palette, array(1, 2, 3, 4)), + new CMYKColor($palette, array(50, 25, 32, 40)), + 0.4, + ), + ); + } + + protected function getPalette() + { + return new CMYK(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php new file mode 100644 index 0000000000000..967879cab9982 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/AbstractColorTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\PaletteInterface; +use Symfony\Component\Image\Tests\TestCase; + +abstract class AbstractColorTest extends TestCase +{ + /** + * @dataProvider provideColorAndAlphaTuples + */ + public function testGetAlpha($expected, $color) + { + $this->assertEquals($expected, $color->getAlpha()); + } + + public function testGetPalette() + { + $this->assertInstanceOf(PaletteInterface::class, $this->getColor()->getPalette()); + } + + /** + * @dataProvider provideColorAndValueComponents + */ + public function testGetvalue($expected, $color) + { + $data = array(); + + foreach ($color->getPalette()->pixelDefinition() as $component) { + $data[$component] = $color->getValue($component); + } + + $this->assertEquals($expected, $data); + } + + public function testDissolve() + { + $color = $this->getColor(); + $alpha = $color->getAlpha(); + $signature = (string) $color; + + $color = $color->dissolve(2); + + $this->assertEquals(2 + $alpha, $color->getAlpha()); + $this->assertEquals($signature, (string) $color); + } + + public function testLighten() + { + $color = $this->getColor(); + + $data = array(); + + foreach ($color->getPalette()->pixelDefinition() as $component) { + $data[$component] = $color->getValue($component); + } + + $color->lighten(4); + + foreach ($color->getPalette()->pixelDefinition() as $component) { + $this->assertLessThanOrEqual($data[$component], $color->getValue($component)); + } + } + + public function testDarken() + { + $color = $this->getColor(); + + $data = array(); + + foreach ($color->getPalette()->pixelDefinition() as $component) { + $data[$component] = $color->getValue($component); + } + + $color->darken(4); + + foreach ($color->getPalette()->pixelDefinition() as $component) { + $this->assertGreaterThanOrEqual($data[$component], $color->getValue($component)); + } + } + + /** + * @dataProvider provideGrayscaleData + */ + public function testGrayscale($expected, $color) + { + $this->assertEquals($expected, (string) $color->grayscale()); + } + + /** + * @dataProvider provideOpaqueColors + */ + public function testIsOpaque($color) + { + $this->assertTrue($color->isOpaque()); + } + + /** + * @dataProvider provideNotOpaqueColors + */ + public function testIsNotOpaque($color) + { + $this->assertFalse($color->isOpaque()); + } + + abstract public function provideColorAndValueComponents(); + + abstract public function provideOpaqueColors(); + + abstract public function provideNotOpaqueColors(); + + abstract public function provideGrayscaleData(); + + abstract public function provideColorAndAlphaTuples(); + + /** + * @return ColorInterface + */ + abstract protected function getColor(); +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php new file mode 100644 index 0000000000000..38d7f492479e8 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\Color\CMYK; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\CMYK as CMYKPalette; + +class CMYKTest extends AbstractColorTest +{ + /** + * @expectedException \Symfony\Component\Image\Exception\RuntimeException + */ + public function testDissolve() + { + $this->getColor()->dissolve(1); + } + + public function provideOpaqueColors() + { + return array( + array($this->getColor()), + ); + } + + public function testIsNotOpaque($color = null) + { + $this->markTestSkipped('CMYK color can not be not opaque'); + } + + public function provideNotOpaqueColors() + { + $this->markTestSkipped('CMYK color can not be not opaque'); + } + + public function provideGrayscaleData() + { + return array( + array('cmyk(42%, 42%, 42%, 25%)', $this->getColor()), + ); + } + + public function provideColorAndAlphaTuples() + { + return array( + array(null, $this->getColor()) + ); + } + + protected function getColor() + { + return new CMYK(new CMYKPalette(), array(12, 23, 45, 25)); + } + + public function provideColorAndValueComponents() + { + return array( + array(array( + ColorInterface::COLOR_CYAN => 12, + ColorInterface::COLOR_MAGENTA => 23, + ColorInterface::COLOR_YELLOW => 45, + ColorInterface::COLOR_KEYLINE => 25, + ), $this->getColor()), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php new file mode 100644 index 0000000000000..efb5d7739046f --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\Color\Gray; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\Grayscale; + +class GrayTest extends AbstractColorTest +{ + public function provideOpaqueColors() + { + return array( + array(new Gray(new Grayscale(), array(12), 100)), + array(new Gray(new Grayscale(), array(0), 100)), + array(new Gray(new Grayscale(), array(255), 100)), + ); + } + public function provideNotOpaqueColors() + { + return array( + array($this->getColor()), + array(new Gray(new Grayscale(), array(12), 23)), + array(new Gray(new Grayscale(), array(0), 45)), + array(new Gray(new Grayscale(), array(255), 0)), + ); + } + + public function provideGrayscaleData() + { + return array( + array('#0c0c0c', $this->getColor()), + ); + } + + public function provideColorAndAlphaTuples() + { + return array( + array(14, $this->getColor()) + ); + } + + protected function getColor() + { + return new Gray(new Grayscale(), array(12), 14); + } + + public function provideColorAndValueComponents() + { + return array( + array(array( + ColorInterface::COLOR_GRAY => 12, + ), $this->getColor()), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php new file mode 100644 index 0000000000000..de7fa13f28f96 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette\Color; + +use Symfony\Component\Image\Image\Palette\Color\RGB; +use Symfony\Component\Image\Image\Palette\Color\ColorInterface; +use Symfony\Component\Image\Image\Palette\RGB as RGBPalette; + +class RGBTest extends AbstractColorTest +{ + public function provideOpaqueColors() + { + return array( + array(new RGB(new RGBPalette(), array(12, 123, 245), 100)), + array(new RGB(new RGBPalette(), array(0, 0, 0), 100)), + array(new RGB(new RGBPalette(), array(255, 255, 255), 100)), + ); + } + public function provideNotOpaqueColors() + { + return array( + array($this->getColor()), + array(new RGB(new RGBPalette(), array(12, 123, 245), 23)), + array(new RGB(new RGBPalette(), array(0, 0, 0), 45)), + array(new RGB(new RGBPalette(), array(255, 255, 255), 0)), + ); + } + + public function provideGrayscaleData() + { + return array( + array('#686868', $this->getColor()), + ); + } + + public function provideColorAndAlphaTuples() + { + return array( + array(14, $this->getColor()) + ); + } + + protected function getColor() + { + return new RGB(new RGBPalette(), array(12, 123, 245), 14); + } + + public function provideColorAndValueComponents() + { + return array( + array(array( + ColorInterface::COLOR_RED => 12, + ColorInterface::COLOR_GREEN => 123, + ColorInterface::COLOR_BLUE => 245, + ), $this->getColor()), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php new file mode 100644 index 0000000000000..0a49973c93445 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/ColorParserTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette; + +use Symfony\Component\Image\Image\Palette\ColorParser; +use Symfony\Component\Image\Tests\TestCase; + +class ColorParserTest extends TestCase +{ + /** + * @dataProvider provideRGBdataToParse + */ + public function testParseToRGB($expected, $value) + { + $parser = new ColorParser(); + + $this->assertEquals($expected, $parser->parseToRGB($value)); + } + + /** + * @dataProvider provideRGBdataThatFail + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + */ + public function testParseToRGBThatFails($value) + { + $parser = new ColorParser(); + $parser->parseToRGB($value); + } + + /** + * @dataProvider provideCMYKdataToParse + */ + public function testParseToCMYK($expected, $value) + { + $parser = new ColorParser(); + + $this->assertEquals($expected, $parser->parseToCMYK($value)); + } + + /** + * @dataProvider provideCMYKdataThatFail + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + */ + public function testParseToCMYKThatFails($value) + { + $parser = new ColorParser(); + $parser->parseToCMYK($value); + } + + public function provideRGBdataToParse() + { + return array( + array(array(255, 255, 0), 'ff0'), + array(array(255, 255, 0), '#ff0'), + array(array(205, 162, 52), 'CDA234'), + array(array(205, 162, 52), '#CDA234'), + array(array(205, 162, 52), 13476404), + array(array(124, 32, 125), array(124, 32, 125)), + ); + } + + public function provideCMYKdataToParse() + { + return array( + array(array(0, 0, 0, 0), 'FFFFFF'), + array(array(0, 0, 0, 100), '000000'), + array(array(0, 21, 75, 20), 'CDA234'), + array(array(0, 21, 75, 20), '#CDA234'), + array(array(0, 21, 75, 20), 'cmyk(0, 21, 75, 20)'), + array(array(0, 21, 75, 20), 'cmyk(0,21,75,20)'), + array(array(0, 21, 75, 20), 'cmyk(0%, 21%, 75%, 20%)'), + array(array(0, 21, 75, 20), 'cmyk(0%,21%,75%,20%)'), + array(array(0, 21, 75, 20), 13476404), + array(array(100, 0, 100, 0), '#00FF00'), + array(array(24, 32, 75, 12), array(24, 32, 75, 12)), + ); + } + + public function provideRGBdataThatFail() + { + $data = array( + array(array(0, 1)), + array(array(0, 1, 0, 1, 0)), + array('1234'), + array('#1234'), + ); + + if (function_exists('imagecreatetruecolor')) { + $data[] = array(imagecreatetruecolor(10, 10)); + } + + return $data; + } + + public function provideCMYKdataThatFail() + { + $data = array( + array(array(0, 1)), + array(array(0, 1, 0, 1, 0)), + array('1234'), + array('#1234'), + ); + + if (function_exists('imagecreatetruecolor')) { + $data[] = array(imagecreatetruecolor(10, 10)); + } + + return $data; + } + + /** + * @dataProvider provideGrayscaledataToParse + */ + public function testParseToGrayscale($expected, $value) + { + $parser = new ColorParser(); + + $this->assertEquals($expected, $parser->parseToGrayscale($value)); + } + + /** + * @dataProvider provideGrayscaledataThatFail + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + */ + public function testParseToGrayscaleThatFails($value) + { + $parser = new ColorParser(); + $parser->parseToGrayscale($value); + } + + public function provideGrayscaledataToParse() + { + return array( + array(array(23), array(23, 23, 23)), + array(array(0), array(0, 0, 0)), + array(array(255), array(255, 255, 255)), + array(array(23), array(23)), + array(array(0), array(0)), + array(array(255), array(255)), + array(array(136), '#888888'), + array(array(153), '999999'), + array(array(0), '#000000'), + array(array(255), 'FFFFFF'), + ); + } + + public function provideGrayscaledataThatFail() + { + return array( + array(array(23, 23, 24)), + array(array(0, 0, 1)), + array('#656666'), + array('777677'), + ); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php new file mode 100644 index 0000000000000..d5d88fc7813a4 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/GrayscaleTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette; + +use Symfony\Component\Image\Image\Palette\Grayscale; +use Symfony\Component\Image\Image\Palette\Color\Gray; + +class GrayscaleTest extends AbstractPaletteTest +{ + public function provideColorAndAlphaTuples() + { + $palette = $this->getPalette(); + + return array( + array(new Gray($palette, array(23), 0), array(23, 23, 23), null), + array(new Gray($palette, array(24), 3), array(24, 24, 24), 3), + array(new Gray($palette, array(23), 0), array(23), null), + array(new Gray($palette, array(24), 3), array(24), 3), + array(new Gray($palette, array(255), 0), array(255), null), + array(new Gray($palette, array(0), 0), array(0), null), + ); + } + + public function provideColorAndAlpha() + { + return array( + array(array(23, 23, 23), 0.5), + ); + } + + public function provideColorsForBlending() + { + $palette = $this->getPalette(); + + return array( + array( + new Gray($palette, array(55), 0), + new Gray($palette, array(1), 0), + new Gray($palette, array(50), 0), + 1.1, + ), + array( + new Gray($palette, array(21), 0), + new Gray($palette, array(1), 0), + new Gray($palette, array(50), 0), + 0.4, + ), + ); + } + + protected function getPalette() + { + return new Grayscale(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php new file mode 100644 index 0000000000000..95cb4412b67c8 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Palette/RGBTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Palette; + +use Symfony\Component\Image\Image\Palette\RGB; +use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; + +class RGBTest extends AbstractPaletteTest +{ + public function provideColorAndAlphaTuples() + { + $palette = $this->getPalette(); + + return array( + array(new RGBColor($palette, array(23, 24, 0), 0), array(23, 24, 0), null), + array(new RGBColor($palette, array(23, 24, 0), 0), array(23, 24, 0), 0), + array(new RGBColor($palette, array(23, 24, 0), 3), array(23, 24, 0), 3), + array(new RGBColor($palette, array(129, 127, 168), 3), array(23, 24, 0, 34), 3), + array(new RGBColor($palette, array(255, 255, 255), 0), array(0, 0, 0, 0), null), + array(new RGBColor($palette, array(0, 0, 0), 0), array(0, 0, 0, 100), null), + ); + } + + public function provideColorAndAlpha() + { + return array( + array(array(23, 24, 0), 0.5), + ); + } + + public function provideColorsForBlending() + { + $palette = $this->getPalette(); + + return array( + array( + new RGBColor($palette, array(240, 0, 0), 0), + new RGBColor($palette, array(230, 0, 0), 0), + new RGBColor($palette, array(128, 0, 0), 0), + 1.1, + ), + array( + new RGBColor($palette, array(21, 11, 15), 0), + new RGBColor($palette, array(1, 2, 3), 0), + new RGBColor($palette, array(50, 25, 32), 0), + 0.4, + ), + ); + } + + protected function getPalette() + { + return new RGB(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php new file mode 100644 index 0000000000000..5ca41d5f00b98 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image\Point; + +use Symfony\Component\Image\Image\Point\Center; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Image\PointInterface; +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Tests\TestCase; + +class CenterTest extends TestCase +{ + /** + * @covers \Symfony\Component\Image\Image\Point\Center::getX + * @covers \Symfony\Component\Image\Image\Point\Center::getY + * + * @dataProvider getSizesAndCoordinates + * + * @param \Symfony\Component\Image\Image\BoxInterface $box + * @param \Symfony\Component\Image\Image\PointInterface $expected + */ + public function testShouldGetCenterCoordinates(BoxInterface $box, PointInterface $expected) + { + $point = new Center($box); + + $this->assertEquals($expected->getX(), $point->getX()); + $this->assertEquals($expected->getY(), $point->getY()); + } + + /** + * Data provider for testShouldGetCenterCoordinates + * + * @return array + */ + public function getSizesAndCoordinates() + { + return array( + array(new Box(10, 15), new Point(5, 8)), + array(new Box(40, 23), new Point(20, 12)), + array(new Box(14, 8), new Point(7, 4)), + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Point::getX + * @covers \Symfony\Component\Image\Image\Point::getY + * @covers \Symfony\Component\Image\Image\Point::move + * + * @dataProvider getMoves + * + * @param \Symfony\Component\Image\Image\BoxInterface $box + * @param integer $move + * @param integer $x1 + * @param integer $y1 + */ + public function testShouldMoveByGivenAmount(BoxInterface $box, $move, $x1, $y1) + { + $point = new Center($box); + $shift = $point->move($move); + + $this->assertEquals($x1, $shift->getX()); + $this->assertEquals($y1, $shift->getY()); + } + + public function getMoves() + { + return array( + array(new Box(10, 20), 5, 10, 15), + array(new Box(5, 37), 2, 5, 21), + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Point\Center::__toString + */ + public function testToString() + { + $this->assertEquals('(50, 50)', (string) new Center(new Box(100, 100))); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/PointTest.php b/src/Symfony/Component/Image/Tests/Image/PointTest.php new file mode 100644 index 0000000000000..1afdc03503960 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Image/PointTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Image; + +use Symfony\Component\Image\Image\Box; +use Symfony\Component\Image\Image\BoxInterface; +use Symfony\Component\Image\Image\Point; +use Symfony\Component\Image\Tests\TestCase; + +class PointTest extends TestCase +{ + /** + * @covers \Symfony\Component\Image\Image\Point::getX + * @covers \Symfony\Component\Image\Image\Point::getY + * @covers \Symfony\Component\Image\Image\Point::in + * + * @dataProvider getCoordinates + * + * @param integer $x + * @param integer $y + * @param BoxInterface $box + * @param Boolean $expected + */ + public function testShouldAssignXYCoordinates($x, $y, BoxInterface $box, $expected) + { + $coordinate = new Point($x, $y); + + $this->assertEquals($x, $coordinate->getX()); + $this->assertEquals($y, $coordinate->getY()); + + $this->assertEquals($expected, $coordinate->in($box)); + } + + /** + * Data provider for testShouldAssignXYCoordinates + * + * @return array + */ + public function getCoordinates() + { + return array( + array(0, 0, new Box(5, 5), true), + array(5, 15, new Box(5, 5), false), + array(10, 23, new Box(10, 10), false), + array(42, 30, new Box(50, 50), true), + array(81, 16, new Box(50, 10), false), + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Point::__construct + * + * @expectedException \Symfony\Component\Image\Exception\InvalidArgumentException + * + * @dataProvider getInvalidCoordinates + * + * @param integer $x + * @param integer $y + */ + public function testShouldThrowExceptionOnInvalidCoordinates($x, $y) + { + new Point($x, $y); + } + + /** + * Data provider for testShouldThrowExceptionOnInvalidCoordinates + * + * @return array + */ + public function getInvalidCoordinates() + { + return array( + array(-1, 0), + array(0, -1) + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Point::getX + * @covers \Symfony\Component\Image\Image\Point::getY + * @covers \Symfony\Component\Image\Image\Point::move + * + * @dataProvider getMoves + * + * @param integer $x + * @param integer $y + * @param integer $move + * @param integer $x1 + * @param integer $y1 + */ + public function testShouldMoveByGivenAmount($x, $y, $move, $x1, $y1) + { + $point = new Point($x, $y); + $shift = $point->move($move); + + $this->assertEquals($x1, $shift->getX()); + $this->assertEquals($y1, $shift->getY()); + } + + public function getMoves() + { + return array( + array(0, 0, 5, 5, 5), + array(20, 30, 5, 25, 35), + array(0, 2, 7, 7, 9), + ); + } + + /** + * @covers \Symfony\Component\Image\Image\Point::__toString + */ + public function testToString() + { + $this->assertEquals('(50, 50)', (string) new Point(50, 50)); + } +} diff --git a/src/Symfony/Component/Image/Tests/Image/ProfileTest.php b/src/Symfony/Component/Image/Tests/Image/ProfileTest.php new file mode 100644 index 0000000000000..df27f490968e3 --- /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..7c36584c1401e --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php @@ -0,0 +1,155 @@ + + * + * 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(__DIR__.'/../results/large.png') + ; + + $this->assertSame(1500, $image->getSize()->getWidth()); + $this->assertSame(750, $image->getSize()->getHeight()); + + $image + ->resize(new Box(100, 50)) + ->save(__DIR__.'/../results/small.png') + ; + + $this->assertSame(100, $image->getSize()->getWidth()); + $this->assertSame(50, $image->getSize()->getHeight()); + + unlink(__DIR__.'/../results/large.png'); + unlink(__DIR__.'/../results/small.png'); + } + + public function testAnimatedGifResize() + { + $loader = $this->getLoader(); + $image = $loader->open(FixturesLoader::getFixture('anima3.gif')); + $image + ->resize(new Box(150, 100)) + ->save(__DIR__.'/../results/anima3-150x100-actual.gif', array('animated' => true)) + ; + $this->assertImageEquals( + $loader->open(FixturesLoader::getFixture('resize/anima3-150x100.gif')), + $loader->open(__DIR__.'/../results/anima3-150x100-actual.gif') + ); + unlink(__DIR__.'/../results/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(__DIR__.'/../results/anima3-topleft-actual.gif', array('animated' => true)) + ; + $this->assertImageEquals( + $loader->open(FixturesLoader::getFixture('crop/anima3-topleft.gif')), + $loader->open(__DIR__.'/../results/anima3-topleft-actual.gif') + ); + unlink(__DIR__.'/../results/anima3-topleft-actual.gif'); + } + + + protected function supportMultipleLayers() + { + return true; + } + + protected function getImageResolution(ImageInterface $image) + { + return $image->getImagick()->getImageResolution(); + } +} diff --git a/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php new file mode 100644 index 0000000000000..afad59e7a7ec4 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests\Imagick; + +use Symfony\Component\Image\Image\Metadata\MetadataBag; +use Symfony\Component\Image\Imagick\Image; +use Symfony\Component\Image\Imagick\Layers; +use Symfony\Component\Image\Imagick\Loader; +use Symfony\Component\Image\Tests\Image\AbstractLayersTest; +use Symfony\Component\Image\Image\ImageInterface; +use Symfony\Component\Image\Image\Palette\RGB; + +class LayersTest extends AbstractLayersTest +{ + protected function setUp() + { + parent::setUp(); + + if (!class_exists('Imagick')) { + $this->markTestSkipped('Imagick is not installed'); + } + } + + public function testCount() + { + if (!$this->supportsMockingImagick()) { + $this->markTestSkipped('Imagick can not be mocked on this platform'); + } + + $palette = new RGB(); + $resource = $this->getMockBuilder('\Imagick')->getMock(); + + $resource->expects($this->once()) + ->method('getNumberImages') + ->will($this->returnValue(42)); + + $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource); + + $this->assertCount(42, $layers); + } + + public function testGetLayer() + { + if (!$this->supportsMockingImagick()) { + $this->markTestSkipped('Imagick can not be mocked on this platform'); + } + + $palette = new RGB(); + $resource = $this->getMockBuilder('\Imagick')->getMock(); + + $resource->expects($this->any()) + ->method('getNumberImages') + ->will($this->returnValue(2)); + + $layer = $this->getMockBuilder('\Imagick')->getMock(); + + $resource->expects($this->any()) + ->method('getImage') + ->will($this->returnValue($layer)); + + $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource); + + foreach ($layers as $layer) { + $this->assertInstanceOf(ImageInterface::class, $layer); + } + } + + public function testCoalesce() + { + $width = null; + $height = null; + + $resource = new \Imagick; + $palette = new RGB(); + $resource->newImage(20, 10, new \ImagickPixel("black")); + $resource->newImage(10, 10, new \ImagickPixel("black")); + + $layers = new Layers(new Image($resource, $palette, new MetadataBag()), $palette, $resource); + $layers->coalesce(); + + foreach ($layers as $layer) { + $size = $layer->getSize(); + + if ($width === null) { + $width = $size->getWidth(); + } else { + $this->assertEquals($width, $size->getWidth()); + } + + if ($height === null) { + $height = $size->getHeight(); + } else { + $this->assertEquals($height, $size->getHeight()); + } + } + } + + public function getImage($path = null) + { + if ($path) { + return new Image(new \Imagick($path), new RGB(), new MetadataBag()); + } else { + return new Image(new \Imagick(), new RGB(), new MetadataBag()); + } + } + + protected function getLoader() + { + return new Loader(); + } + + protected function assertLayersEquals($expected, $actual) + { + $this->assertEquals($expected->getImagick(), $actual->getImagick()); + } +} diff --git a/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php b/src/Symfony/Component/Image/Tests/Imagick/LoaderTest.php new file mode 100644 index 0000000000000..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/Issues/Issue131Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php new file mode 100644 index 0000000000000..80628d4a68400 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php @@ -0,0 +1,107 @@ +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/Issues/Issue17Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php new file mode 100644 index 0000000000000..2ebefd94fc771 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php @@ -0,0 +1,42 @@ +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(__DIR__.'/../results/resized.jpg'); + + $this->assertTrue(file_exists(__DIR__.'/../results/resized.jpg')); + $this->assertEquals( + $size, + $loader->open(__DIR__.'/../results/resized.jpg')->getSize() + ); + + unlink(__DIR__.'/../results/resized.jpg'); + } +} diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php new file mode 100644 index 0000000000000..de8db5520b78c --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php @@ -0,0 +1,38 @@ +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()); + + unlink($new); + } +} diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php b/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php new file mode 100644 index 0000000000000..3ee1851c9f9a2 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/Issues/Issue67Test.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/TestCase.php b/src/Symfony/Component/Image/Tests/TestCase.php new file mode 100644 index 0000000000000..ae7d33d5213d3 --- /dev/null +++ b/src/Symfony/Component/Image/Tests/TestCase.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Image\Tests; + +use PHPUnit\Framework\TestCase as PHPUnitTestCase; +use Symfony\Component\Image\Tests\Constraint\IsImageEqual; + +class TestCase extends PHPUnitTestCase +{ + const HTTP_IMAGE = 'http://symfony.com/images/common/logo/logo_symfony_header.png'; + + private static $supportMockingImagick; + + /** + * Asserts that two images are equal using color histogram comparison method + * + * @param ImageInterface $expected + * @param ImageInterface $actual + * @param string $message + * @param float $delta + * @param integer $buckets + */ + public static function assertImageEquals($expected, $actual, $message = '', $delta = 0.1, $buckets = 4) + { + $constraint = new IsImageEqual($expected, $delta, $buckets); + + self::assertThat($actual, $constraint, $message); + } + + public function setExpectedException($exception, $message = null, $code = null) + { + if (method_exists(parent::class, 'expectException')) { + parent::expectException($exception); + if (null !== $message) { + parent::expectExceptionMessage($message); + } + if (null !== $code) { + parent::expectExceptionCode($code); + } + } else { + return parent::setExpectedException($exception, $message, $code); + } + } + + /** + * Actually it's not possible on some HHVM versions + */ + protected function supportsMockingImagick() + { + if (null !== self::$supportMockingImagick) { + return self::$supportMockingImagick; + } + + try { + @$this->getMockBuilder('\Imagick')->disableOriginalConstructor()->getMock(); + } catch (\Exception $e) { + return self::$supportMockingImagick = false; + } + + return self::$supportMockingImagick = true; + } +} diff --git a/src/Symfony/Component/Image/Tests/results/in_out/.placeholder b/src/Symfony/Component/Image/Tests/results/in_out/.placeholder new file mode 100644 index 0000000000000..e69de29bb2d1d From a80d5f0f012201fd29a1015a2eb114e525dafeeb Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Mon, 27 Mar 2017 17:47:29 +0200 Subject: [PATCH 2/2] Integrate new Image Component --- .github/gmagick.sh | 61 ++++++++ .github/imagick.sh | 51 +++++++ .travis.yml | 12 +- appveyor.yml | 2 + composer.json | 1 + src/Symfony/Component/Image/.gitignore | 3 + src/Symfony/Component/Image/CHANGELOG.md | 7 + .../Image/Filter/Basic/WebOptimization.php | 17 ++- .../Image/Filter/FilterInterface.php | 4 +- .../Component/Image/Filter/LoaderAware.php | 2 +- .../Component/Image/Filter/Transformation.php | 12 +- src/Symfony/Component/Image/Gd/Drawer.php | 26 ++-- src/Symfony/Component/Image/Gd/Effects.php | 8 +- src/Symfony/Component/Image/Gd/Font.php | 14 +- src/Symfony/Component/Image/Gd/Image.php | 135 +++++++--------- src/Symfony/Component/Image/Gd/Loader.php | 6 +- .../Component/Image/Gmagick/Drawer.php | 48 +++--- .../Component/Image/Gmagick/Effects.php | 6 +- src/Symfony/Component/Image/Gmagick/Font.php | 8 +- src/Symfony/Component/Image/Gmagick/Image.php | 118 +++++++------- .../Component/Image/Gmagick/Layers.php | 12 +- .../Component/Image/Gmagick/Loader.php | 6 +- .../Component/Image/Image/AbstractFont.php | 12 +- .../Component/Image/Image/AbstractImage.php | 31 +++- .../Component/Image/Image/AbstractLoader.php | 2 +- src/Symfony/Component/Image/Image/Box.php | 18 +-- .../Component/Image/Image/BoxInterface.php | 34 ++--- .../Image/Image/Fill/FillInterface.php | 4 +- .../Image/Image/Fill/Gradient/Horizontal.php | 2 +- .../Image/Image/Fill/Gradient/Linear.php | 16 +- .../Image/Image/Fill/Gradient/Vertical.php | 2 +- .../Component/Image/Image/FontInterface.php | 16 +- .../Image/Image/Histogram/Bucket.php | 14 +- .../Component/Image/Image/Histogram/Range.php | 16 +- .../Component/Image/Image/ImageInterface.php | 24 +-- .../Component/Image/Image/LayersInterface.php | 32 ++-- .../Component/Image/Image/LoaderInterface.php | 14 +- .../Image/Image/ManipulatorInterface.php | 30 ++-- .../Image/Metadata/AbstractMetadataReader.php | 8 +- .../Image/Metadata/DefaultMetadataReader.php | 2 +- .../Image/Metadata/ExifMetadataReader.php | 8 +- .../Image/Image/Metadata/MetadataBag.php | 6 +- .../Metadata/MetadataReaderInterface.php | 12 +- .../Component/Image/Image/Palette/CMYK.php | 4 +- .../Image/Image/Palette/Color/CMYK.php | 29 ++-- .../Image/Palette/Color/ColorInterface.php | 26 ++-- .../Image/Image/Palette/Color/Gray.php | 17 +-- .../Image/Image/Palette/Color/RGB.php | 39 ++--- .../Image/Image/Palette/ColorParser.php | 26 ++-- .../Image/Image/Palette/Grayscale.php | 4 +- .../Image/Image/Palette/PaletteInterface.php | 16 +- .../Component/Image/Image/Palette/RGB.php | 4 +- src/Symfony/Component/Image/Image/Point.php | 14 +- .../Component/Image/Image/Point/Center.php | 4 +- .../Component/Image/Image/PointInterface.php | 21 +-- src/Symfony/Component/Image/Image/Profile.php | 4 +- .../Image/Image/ProfileInterface.php | 8 +- .../Component/Image/Imagick/Drawer.php | 52 +++---- .../Component/Image/Imagick/Effects.php | 2 +- src/Symfony/Component/Image/Imagick/Font.php | 8 +- src/Symfony/Component/Image/Imagick/Image.php | 144 +++++++++--------- .../Component/Image/Imagick/Layers.php | 9 +- .../Component/Image/Imagick/Loader.php | 8 +- src/Symfony/Component/Image/LICENSE | 19 +++ src/Symfony/Component/Image/README.md | 11 ++ .../Image/Tests/Constraint/IsImageEqual.php | 49 +++--- .../Image/Tests/Draw/AbstractDrawerTest.php | 102 +++++-------- .../Tests/Effects/AbstractEffectsTest.php | 1 - .../Tests/Filter/Advanced/BorderTest.php | 8 +- .../Tests/Filter/Advanced/CanvasTest.php | 2 +- .../Tests/Filter/Advanced/GrayscaleTest.php | 14 +- .../Tests/Filter/Basic/AutorotateTest.php | 2 +- .../Image/Tests/Filter/Basic/CopyTest.php | 4 +- .../Image/Tests/Filter/Basic/CropTest.php | 4 +- .../Filter/Basic/FlipHorizontallyTest.php | 2 +- .../Tests/Filter/Basic/FlipVerticallyTest.php | 2 +- .../Image/Tests/Filter/Basic/PasteTest.php | 6 +- .../Image/Tests/Filter/Basic/ResizeTest.php | 4 +- .../Image/Tests/Filter/Basic/RotateTest.php | 4 +- .../Image/Tests/Filter/Basic/SaveTest.php | 4 +- .../Image/Tests/Filter/Basic/ShowTest.php | 4 +- .../Image/Tests/Filter/Basic/StripTest.php | 2 +- .../Tests/Filter/Basic/ThumbnailTest.php | 6 +- .../Filter/Basic/WebOptimizationTest.php | 36 ++--- .../Tests/Filter/DummyLoaderAwareFilter.php | 3 +- .../Image/Tests/Filter/TransformationTest.php | 24 +-- .../GdTransparentGifHandlingTest.php | 6 +- .../Image/Tests/Gmagick/ImageTest.php | 1 - .../Image/Tests/Image/AbstractImageTest.php | 111 ++++++-------- .../Image/Tests/Image/AbstractLayersTest.php | 17 +-- .../Image/Tests/Image/AbstractLoaderTest.php | 34 ++--- .../Component/Image/Tests/Image/BoxTest.php | 34 ++--- .../Image/Fill/Gradient/HorizontalTest.php | 15 +- .../Tests/Image/Fill/Gradient/LinearTest.php | 6 +- .../Image/Fill/Gradient/VerticalTest.php | 15 +- .../Tests/Image/Histogram/BucketTest.php | 4 +- .../Image/Tests/Image/Histogram/RangeTest.php | 6 +- .../Image/Metadata/MetadataReaderTestCase.php | 2 - .../Image/Palette/AbstractPaletteTest.php | 1 - .../Image/Tests/Image/Palette/CMYKTest.php | 2 +- .../Tests/Image/Palette/Color/CMYKTest.php | 2 +- .../Tests/Image/Palette/Color/GrayTest.php | 3 +- .../Tests/Image/Palette/Color/RGBTest.php | 3 +- .../Image/Tests/Image/Point/CenterTest.php | 8 +- .../Component/Image/Tests/Image/PointTest.php | 26 ++-- .../Image/Tests/Image/ProfileTest.php | 2 +- .../Image/Tests/Imagick/ImageTest.php | 18 +-- .../Image/Tests/Imagick/LayersTest.php | 6 +- .../RegressionErrorTest.php} | 10 +- .../RegressionGIFTest.php} | 8 +- .../RegressionResizeTest.php} | 14 +- .../RegressionSaveTest.php} | 13 +- .../Component/Image/Tests/TestCase.php | 29 +++- src/Symfony/Component/Image/composer.json | 52 +++++++ src/Symfony/Component/Image/phpunit.xml.dist | 28 ++++ 115 files changed, 1157 insertions(+), 933 deletions(-) create mode 100755 .github/gmagick.sh create mode 100755 .github/imagick.sh create mode 100644 src/Symfony/Component/Image/.gitignore create mode 100644 src/Symfony/Component/Image/CHANGELOG.md create mode 100644 src/Symfony/Component/Image/LICENSE create mode 100644 src/Symfony/Component/Image/README.md rename src/Symfony/Component/Image/Tests/{Issues/Issue67Test.php => Regression/RegressionErrorTest.php} (74%) rename src/Symfony/Component/Image/Tests/{Issues/Issue59Test.php => Regression/RegressionGIFTest.php} (83%) rename src/Symfony/Component/Image/Tests/{Issues/Issue17Test.php => Regression/RegressionResizeTest.php} (68%) rename src/Symfony/Component/Image/Tests/{Issues/Issue131Test.php => Regression/RegressionSaveTest.php} (88%) create mode 100644 src/Symfony/Component/Image/composer.json create mode 100644 src/Symfony/Component/Image/phpunit.xml.dist 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/Filter/Basic/WebOptimization.php b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php index 06fb5102d423d..08beab5f004c1 100644 --- a/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php +++ b/src/Symfony/Component/Image/Filter/Basic/WebOptimization.php @@ -16,7 +16,7 @@ use Symfony\Component\Image\Filter\FilterInterface; /** - * A filter to render web-optimized images + * A filter to render web-optimized images. */ class WebOptimization implements FilterInterface { @@ -28,10 +28,19 @@ 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, + '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(); } diff --git a/src/Symfony/Component/Image/Filter/FilterInterface.php b/src/Symfony/Component/Image/Filter/FilterInterface.php index 3313b4cedcf01..c09e623941674 100644 --- a/src/Symfony/Component/Image/Filter/FilterInterface.php +++ b/src/Symfony/Component/Image/Filter/FilterInterface.php @@ -14,13 +14,13 @@ use Symfony\Component\Image\Image\ImageInterface; /** - * Interface for filters + * Interface for filters. */ interface FilterInterface { /** * Applies scheduled transformation to ImageInterface instance - * Returns processed ImageInterface instance + * Returns processed ImageInterface instance. * * @param ImageInterface $image * diff --git a/src/Symfony/Component/Image/Filter/LoaderAware.php b/src/Symfony/Component/Image/Filter/LoaderAware.php index 834853dd7c775..a92161b48f0b5 100644 --- a/src/Symfony/Component/Image/Filter/LoaderAware.php +++ b/src/Symfony/Component/Image/Filter/LoaderAware.php @@ -15,7 +15,7 @@ use Symfony\Component\Image\Image\LoaderInterface; /** - * LoaderAware base class + * LoaderAware base class. */ abstract class LoaderAware implements FilterInterface { diff --git a/src/Symfony/Component/Image/Filter/Transformation.php b/src/Symfony/Component/Image/Filter/Transformation.php index c6bc5ae5f89e2..2bad5343996e0 100644 --- a/src/Symfony/Component/Image/Filter/Transformation.php +++ b/src/Symfony/Component/Image/Filter/Transformation.php @@ -34,7 +34,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * A transformation filter + * A transformation filter. */ final class Transformation implements FilterInterface, ManipulatorInterface { @@ -67,12 +67,13 @@ public function __construct(LoaderInterface $loader = null) /** * Applies a given FilterInterface onto given ImageInterface and returns - * modified ImageInterface + * modified ImageInterface. * * @param ImageInterface $image * @param FilterInterface $filter * * @return ImageInterface + * * @throws InvalidArgumentException */ public function applyFilter(ImageInterface $image, FilterInterface $filter) @@ -224,10 +225,11 @@ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_ /** * Registers a given FilterInterface in an internal array of filters for - * later application to an instance of ImageInterface + * later application to an instance of ImageInterface. + * + * @param FilterInterface $filter + * @param int $priority * - * @param FilterInterface $filter - * @param int $priority * @return Transformation */ public function add(FilterInterface $filter, $priority = 0) diff --git a/src/Symfony/Component/Image/Gd/Drawer.php b/src/Symfony/Component/Image/Gd/Drawer.php index b543d4c8e0aba..09d62a16a2eba 100644 --- a/src/Symfony/Component/Image/Gd/Drawer.php +++ b/src/Symfony/Component/Image/Gd/Drawer.php @@ -21,7 +21,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Drawer implementation using the GD library + * Drawer implementation using the GD library. */ final class Drawer implements DrawerInterface { @@ -36,7 +36,7 @@ final class Drawer implements DrawerInterface private $info; /** - * Constructs Drawer with a given gd image resource + * Constructs Drawer with a given gd image resource. * * @param resource $resource */ @@ -70,7 +70,7 @@ public function arc(PointInterface $center, BoxInterface $size, $start, $end, Co } /** - * This function does not work properly because of a bug in GD + * This function does not work properly because of a bug in GD. * * {@inheritdoc} */ @@ -248,11 +248,11 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang throw new RuntimeException('GD is not compiled with FreeType support'); } - $angle = -1 * $angle; + $angle = -1 * $angle; $fontsize = $font->getSize(); $fontfile = $font->getFile(); - $x = $position->getX(); - $y = $position->getY() + $fontsize; + $x = $position->getX(); + $y = $position->getY() + $fontsize; if ($width !== null) { $string = $this->wrapText($string, $font, $angle, $width); @@ -275,9 +275,7 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang } /** - * Internal - * - * Generates a GD color from Color instance + * Generates a GD color from Color instance. * * @param ColorInterface $color * @@ -310,21 +308,19 @@ private function loadGdInfo() } /** - * Internal - * - * Fits a string into box with given width + * 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; + $teststring = $result.' '.$word; $testbox = imagettfbbox($font->getSize(), $angle, $font->getFile(), $teststring); if ($testbox[2] > $width) { - $result .= ($result == '' ? '' : "\n") . $word; + $result .= ($result == '' ? '' : "\n").$word; } else { - $result .= ($result == '' ? '' : ' ') . $word; + $result .= ($result == '' ? '' : ' ').$word; } } diff --git a/src/Symfony/Component/Image/Gd/Effects.php b/src/Symfony/Component/Image/Gd/Effects.php index 4044ba0489593..e3de2d4d56ddf 100644 --- a/src/Symfony/Component/Image/Gd/Effects.php +++ b/src/Symfony/Component/Image/Gd/Effects.php @@ -17,7 +17,7 @@ use Symfony\Component\Image\Image\Palette\Color\RGB as RGBColor; /** - * Effects implementation using the GD library + * Effects implementation using the GD library. */ class Effects implements EffectsInterface { @@ -46,7 +46,7 @@ public function gamma($correction) public function negative() { if (false === imagefilter($this->resource, IMG_FILTER_NEGATE)) { - throw new RuntimeException('Failed to negate the image'); + throw new RuntimeException('Failed to negate the image'); } return $this; @@ -58,7 +58,7 @@ public function negative() public function grayscale() { if (false === imagefilter($this->resource, IMG_FILTER_GRAYSCALE)) { - throw new RuntimeException('Failed to grayscale the image'); + throw new RuntimeException('Failed to grayscale the image'); } return $this; @@ -85,7 +85,7 @@ public function colorize(ColorInterface $color) */ public function sharpen() { - $sharpenMatrix = array(array(-1,-1,-1), array(-1,16,-1), array(-1,-1,-1)); + $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)) { diff --git a/src/Symfony/Component/Image/Gd/Font.php b/src/Symfony/Component/Image/Gd/Font.php index bb141af92ae46..5c0465182d390 100644 --- a/src/Symfony/Component/Image/Gd/Font.php +++ b/src/Symfony/Component/Image/Gd/Font.php @@ -16,7 +16,7 @@ use Symfony\Component\Image\Image\Box; /** - * Font implementation using the GD library + * Font implementation using the GD library. */ final class Font extends AbstractFont { @@ -29,12 +29,12 @@ public function box($string, $angle = 0) 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)); + $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 index 051c89408c791..e01e3cf47214a 100644 --- a/src/Symfony/Component/Image/Gd/Image.php +++ b/src/Symfony/Component/Image/Gd/Image.php @@ -29,7 +29,7 @@ use Symfony\Component\Image\Exception\RuntimeException; /** - * Image implementation using the GD library + * Image implementation using the GD library. */ final class Image extends AbstractImage { @@ -49,7 +49,7 @@ final class Image extends AbstractImage private $palette; /** - * Constructs a new Image instance + * Constructs a new Image instance. * * @param resource $resource * @param PaletteInterface $palette @@ -63,7 +63,7 @@ public function __construct($resource, PaletteInterface $palette, MetadataBag $m } /** - * Makes sure the current image resource is destroyed + * Makes sure the current image resource is destroyed. */ public function __destruct() { @@ -73,7 +73,7 @@ public function __destruct() } /** - * Returns Gd resource + * Returns Gd resource. * * @return resource */ @@ -96,7 +96,7 @@ final public function copy() throw new RuntimeException('Image copy operation failed'); } - return new Image($copy, $this->palette, $this->metadata); + return new self($copy, $this->palette, $this->metadata); } /** @@ -110,7 +110,7 @@ final public function crop(PointInterface $start, BoxInterface $size) 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(); + $width = $size->getWidth(); $height = $size->getHeight(); $dest = $this->createImage($size, 'crop'); @@ -166,7 +166,7 @@ final public function resize(BoxInterface $size, $filter = ImageInterface::FILTE throw new InvalidArgumentException('Unsupported filter type, GD only supports ImageInterface::FILTER_UNDEFINED filter'); } - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); $dest = $this->createImage($size, 'resize'); @@ -275,12 +275,12 @@ public function __toString() */ final public function flipHorizontally() { - $size = $this->getSize(); - $width = $size->getWidth(); + $size = $this->getSize(); + $width = $size->getWidth(); $height = $size->getHeight(); - $dest = $this->createImage($size, 'flip'); + $dest = $this->createImage($size, 'flip'); - for ($i = 0; $i < $width; $i++) { + 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'); } @@ -300,12 +300,12 @@ final public function flipHorizontally() */ final public function flipVertically() { - $size = $this->getSize(); - $width = $size->getWidth(); + $size = $this->getSize(); + $width = $size->getWidth(); $height = $size->getHeight(); - $dest = $this->createImage($size, 'flip'); + $dest = $this->createImage($size, 'flip'); - for ($i = 0; $i < $height; $i++) { + 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'); } @@ -364,19 +364,19 @@ public function applyMask(ImageInterface $mask) throw new InvalidArgumentException('Cannot mask non-gd images'); } - $size = $this->getSize(); + $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); + 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)); + $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'); @@ -396,8 +396,8 @@ 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++) { + 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'); } @@ -426,11 +426,11 @@ public function mask() */ public function histogram() { - $size = $this->getSize(); + $size = $this->getSize(); $colors = array(); - for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) { - for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) { + 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)); } } @@ -448,7 +448,7 @@ public function getColorAt(PointInterface $point) } $index = imagecolorat($this->resource, $point->getX(), $point->getY()); - $info = imagecolorsforindex($this->resource, $index); + $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)); } @@ -471,9 +471,9 @@ public function layers() public function interlace($scheme) { static $supportedInterlaceSchemes = array( - ImageInterface::INTERLACE_NONE => 0, - ImageInterface::INTERLACE_LINE => 1, - ImageInterface::INTERLACE_PLANE => 1, + ImageInterface::INTERLACE_NONE => 0, + ImageInterface::INTERLACE_LINE => 1, + ImageInterface::INTERLACE_PLANE => 1, ImageInterface::INTERLACE_PARTITION => 1, ); @@ -517,9 +517,7 @@ public function usePalette(PaletteInterface $palette) } /** - * Internal - * - * Performs save or show operation using one of GD's image... functions + * Performs save or show operation using one of GD's image... functions. * * @param string $format * @param array $options @@ -539,14 +537,6 @@ private function saveOrOutput($format, array $options, $filename = null) $save = 'image'.$format; $args = array(&$this->resource, $filename); - // Preserve BC until version 1.0 - if (isset($options['quality']) && !isset($options['png_compression_level'])) { - $options['png_compression_level'] = round((100 - $options['quality']) * 9 / 100); - } - if (isset($options['filters']) && !isset($options['png_compression_filter'])) { - $options['png_compression_filter'] = $options['filters']; - } - $options = $this->updateSaveOptions($options); if ($format === 'jpeg' && isset($options['jpeg_quality'])) { @@ -575,19 +565,25 @@ private function saveOrOutput($format, array $options, $filename = null) $args[] = $options['foreground']; } - $this->setExceptionHandler(); + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + if (0 === error_reporting()) { + return; + } - if (false === call_user_func_array($save, $args)) { - throw new RuntimeException('Save operation failed'); - } + throw new RuntimeException($errstr, $errno, new \ErrorException($errstr, 0, $errno, $errfile, $errline)); + }); - $this->resetExceptionHandler(); + try { + if (false === call_user_func_array($save, $args)) { + throw new RuntimeException('Save operation failed'); + } + } finally { + restore_error_handler(); + } } /** - * Internal - * - * Generates a GD image + * Generates a GD image. * * @param BoxInterface $size * @param string the operation initiating the creation @@ -595,7 +591,6 @@ private function saveOrOutput($format, array $options, $filename = null) * @return resource * * @throws RuntimeException - * */ private function createImage(BoxInterface $size, $operation) { @@ -621,13 +616,11 @@ private function createImage(BoxInterface $size, $operation) } /** - * Internal - * - * Generates a GD color from Color instance + * Generates a GD color from Color instance. * * @param ColorInterface $color * - * @return integer A color identifier + * @return int A color identifier * * @throws RuntimeException * @throws InvalidArgumentException @@ -648,9 +641,7 @@ private function getColor(ColorInterface $color) } /** - * Internal - * - * Normalizes a given format name + * Normalizes a given format name. * * @param string $format * @@ -668,13 +659,11 @@ private function normalizeFormat($format) } /** - * Internal - * - * Checks whether a given format is supported by GD library + * Checks whether a given format is supported by GD library. * * @param string $format * - * @return Boolean + * @return bool */ private function supported($format = null) { @@ -687,25 +676,7 @@ private function supported($format = null) return in_array($format, $formats); } - private function setExceptionHandler() - { - set_error_handler(function ($errno, $errstr, $errfile, $errline) { - if (0 === error_reporting()) { - return; - } - - throw new RuntimeException($errstr, $errno, new \ErrorException($errstr, 0, $errno, $errfile, $errline)); - }, E_WARNING | E_NOTICE); - } - - private function resetExceptionHandler() - { - restore_error_handler(); - } - /** - * Internal - * * Get the mime type based on format. * * @param string $format @@ -724,10 +695,10 @@ private function getMimeType($format) static $mimeTypes = array( 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', + 'gif' => 'image/gif', + 'png' => 'image/png', 'wbmp' => 'image/vnd.wap.wbmp', - 'xbm' => 'image/xbm', + 'xbm' => 'image/xbm', ); return $mimeTypes[$format]; diff --git a/src/Symfony/Component/Image/Gd/Loader.php b/src/Symfony/Component/Image/Gd/Loader.php index 85437cad45c78..ed3d5f1a31fdf 100644 --- a/src/Symfony/Component/Image/Gd/Loader.php +++ b/src/Symfony/Component/Image/Gd/Loader.php @@ -22,7 +22,7 @@ use Symfony\Component\Image\Exception\RuntimeException; /** - * Loader implementation using the GD library + * Loader implementation using the GD library. */ final class Loader extends AbstractLoader { @@ -45,7 +45,7 @@ public function __construct() */ public function create(BoxInterface $size, ColorInterface $color = null) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); $resource = imagecreatetruecolor($width, $height); @@ -143,7 +143,7 @@ private function wrap($resource, PaletteInterface $palette, MetadataBag $metadat list($width, $height) = array(imagesx($resource), imagesy($resource)); // create transparent truecolor canvas - $truecolor = imagecreatetruecolor($width, $height); + $truecolor = imagecreatetruecolor($width, $height); $transparent = imagecolorallocatealpha($truecolor, 255, 255, 255, 127); imagefill($truecolor, 0, 0, $transparent); diff --git a/src/Symfony/Component/Image/Gmagick/Drawer.php b/src/Symfony/Component/Image/Gmagick/Drawer.php index 327d880639bda..c67f8eceeebaa 100644 --- a/src/Symfony/Component/Image/Gmagick/Drawer.php +++ b/src/Symfony/Component/Image/Gmagick/Drawer.php @@ -22,7 +22,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Drawer implementation using the Gmagick PHP extension + * Drawer implementation using the Gmagick PHP extension. */ final class Drawer implements DrawerInterface { @@ -44,14 +44,14 @@ public function __construct(\Gmagick $gmagick) */ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) { - $x = $center->getX(); - $y = $center->getY(); - $width = $size->getWidth(); + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); $height = $size->getHeight(); try { $pixel = $this->getColor($color); - $arc = new \GmagickDraw(); + $arc = new \GmagickDraw(); $arc->setstrokecolor($pixel); $arc->setstrokewidth(max(1, (int) $thickness)); @@ -65,7 +65,7 @@ public function arc(PointInterface $center, BoxInterface $size, $start, $end, Co $end ); - $this->gmagick->drawImage($arc); + $this->gmagick->drawimage($arc); $pixel = null; @@ -82,9 +82,9 @@ public function arc(PointInterface $center, BoxInterface $size, $start, $end, Co */ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) { - $x = $center->getX(); - $y = $center->getY(); - $width = $size->getWidth(); + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); $height = $size->getHeight(); try { @@ -109,7 +109,7 @@ public function chord(PointInterface $center, BoxInterface $size, $start, $end, $chord->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end); - $this->gmagick->drawImage($chord); + $this->gmagick->drawimage($chord); $pixel = null; @@ -126,11 +126,11 @@ public function chord(PointInterface $center, BoxInterface $size, $start, $end, */ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); try { - $pixel = $this->getColor($color); + $pixel = $this->getColor($color); $ellipse = new \GmagickDraw(); $ellipse->setstrokecolor($pixel); @@ -150,7 +150,7 @@ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterfa 0, 360 ); - $this->gmagick->drawImage($ellipse); + $this->gmagick->drawimage($ellipse); $pixel = null; @@ -169,7 +169,7 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface { try { $pixel = $this->getColor($color); - $line = new \GmagickDraw(); + $line = new \GmagickDraw(); $line->setstrokecolor($pixel); $line->setstrokewidth(max(1, (int) $thickness)); @@ -181,7 +181,7 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface $end->getY() ); - $this->gmagick->drawImage($line); + $this->gmagick->drawimage($line); $pixel = null; @@ -198,7 +198,7 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface */ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start))); @@ -267,7 +267,7 @@ public function polygon(array $coordinates, ColorInterface $color, $fill = false }, $coordinates); try { - $pixel = $this->getColor($color); + $pixel = $this->getColor($color); $polygon = new \GmagickDraw(); $polygon->setstrokecolor($pixel); @@ -281,7 +281,7 @@ public function polygon(array $coordinates, ColorInterface $color, $fill = false $polygon->polygon($points); - $this->gmagick->drawImage($polygon); + $this->gmagick->drawimage($polygon); unset($pixel, $polygon); } catch (\GmagickException $e) { @@ -298,10 +298,10 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang { try { $pixel = $this->getColor($font->getColor()); - $text = new \GmagickDraw(); + $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 @@ -310,9 +310,9 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang $text->setfillcolor($pixel); $info = $this->gmagick->queryfontmetrics($text, $string); - $rad = deg2rad($angle); - $cos = cos($rad); - $sin = sin($rad); + $rad = deg2rad($angle); + $cos = cos($rad); + $sin = sin($rad); $x1 = round(0 * $cos - 0 * $sin); $x2 = round($info['textWidth'] * $cos - $info['textHeight'] * $sin); @@ -337,7 +337,7 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang } /** - * Gets specifically formatted color string from Color instance + * Gets specifically formatted color string from Color instance. * * @param ColorInterface $color * diff --git a/src/Symfony/Component/Image/Gmagick/Effects.php b/src/Symfony/Component/Image/Gmagick/Effects.php index 488a00b560a0a..d14a01ccace18 100644 --- a/src/Symfony/Component/Image/Gmagick/Effects.php +++ b/src/Symfony/Component/Image/Gmagick/Effects.php @@ -17,7 +17,7 @@ use Symfony\Component\Image\Exception\NotSupportedException; /** - * Effects implementation using the Gmagick PHP extension + * Effects implementation using the Gmagick PHP extension. */ class Effects implements EffectsInterface { @@ -66,7 +66,7 @@ public function negative() public function grayscale() { try { - $this->gmagick->setImageType(2); + $this->gmagick->setimagetype(2); } catch (\GmagickException $e) { throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e); } @@ -96,7 +96,7 @@ public function sharpen() public function blur($sigma = 1) { try { - $this->gmagick->blurImage(0, $sigma); + $this->gmagick->blurimage(0, $sigma); } catch (\GmagickException $e) { throw new RuntimeException('Failed to blur the image', $e->getCode(), $e); } diff --git a/src/Symfony/Component/Image/Gmagick/Font.php b/src/Symfony/Component/Image/Gmagick/Font.php index 7641426bbf185..6709403f08fe9 100644 --- a/src/Symfony/Component/Image/Gmagick/Font.php +++ b/src/Symfony/Component/Image/Gmagick/Font.php @@ -16,7 +16,7 @@ use Symfony\Component\Image\Image\Palette\Color\ColorInterface; /** - * Font implementation using the Gmagick PHP extension + * Font implementation using the Gmagick PHP extension. */ final class Font extends AbstractFont { @@ -28,7 +28,7 @@ final class Font extends AbstractFont /** * @param \Gmagick $gmagick * @param string $file - * @param integer $size + * @param int $size * @param ColorInterface $color */ public function __construct(\Gmagick $gmagick, $file, $size, ColorInterface $color) @@ -43,10 +43,10 @@ public function __construct(\Gmagick $gmagick, $file, $size, ColorInterface $col */ public function box($string, $angle = 0) { - $text = new \GmagickDraw(); + $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 diff --git a/src/Symfony/Component/Image/Gmagick/Image.php b/src/Symfony/Component/Image/Gmagick/Image.php index 72ff395c92298..86872a49afb84 100644 --- a/src/Symfony/Component/Image/Gmagick/Image.php +++ b/src/Symfony/Component/Image/Gmagick/Image.php @@ -27,7 +27,7 @@ use Symfony\Component\Image\Image\ProfileInterface; /** - * Image implementation using the Gmagick PHP extension + * Image implementation using the Gmagick PHP extension. */ final class Image extends AbstractImage { @@ -47,11 +47,11 @@ final class Image extends AbstractImage private static $colorspaceMapping = array( PaletteInterface::PALETTE_CMYK => \Gmagick::COLORSPACE_CMYK, - PaletteInterface::PALETTE_RGB => \Gmagick::COLORSPACE_RGB, + PaletteInterface::PALETTE_RGB => \Gmagick::COLORSPACE_RGB, ); /** - * Constructs a new Image instance + * Constructs a new Image instance. * * @param \Gmagick $gmagick * @param PaletteInterface $palette @@ -66,7 +66,7 @@ public function __construct(\Gmagick $gmagick, PaletteInterface $palette, Metada } /** - * Destroys allocated gmagick resources + * Destroys allocated gmagick resources. */ public function __destruct() { @@ -77,7 +77,7 @@ public function __destruct() } /** - * Returns gmagick instance + * Returns gmagick instance. * * @return \Gmagick */ @@ -203,21 +203,21 @@ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDE { 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_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 + ImageInterface::FILTER_SINC => \Gmagick::FILTER_SINC, + ImageInterface::FILTER_TRIANGLE => \Gmagick::FILTER_TRIANGLE, ); if (!array_key_exists($filter, $supportedFilters)) { @@ -255,9 +255,7 @@ public function rotate($angle, ColorInterface $background = null) } /** - * Internal - * - * Applies options before save or output + * Applies options before save or output. * * @param \Gmagick $image * @param array $options @@ -272,7 +270,7 @@ private function applyImageOptions(\Gmagick $image, array $options, $path) } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) { $format = $extension; } else { - $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION); + $format = pathinfo($image->getimagefilename(), \PATHINFO_EXTENSION); } $format = strtolower($format); @@ -307,16 +305,16 @@ private function applyImageOptions(\Gmagick $image, array $options, $path) $image->setCompressionQuality($compression); } - if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) { - if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) { + 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) { + } 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']); + $image->setimageresolution($options['resolution_x'], $options['resolution_y']); } } @@ -327,7 +325,7 @@ private function applyImageOptions(\Gmagick $image, array $options, $path) */ public function save($path = null, array $options = array()) { - $path = null === $path ? $this->gmagick->getImageFilename() : $path; + $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'); @@ -433,7 +431,7 @@ public function getSize() try { $i = $this->gmagick->getimageindex(); $this->gmagick->setimageindex(0); //rewind - $width = $this->gmagick->getimagewidth(); + $width = $this->gmagick->getimagewidth(); $height = $this->gmagick->getimageheight(); $this->gmagick->setimageindex($i); } catch (\GmagickException $e) { @@ -501,8 +499,8 @@ public function fill(FillInterface $fill) $w = $size->getWidth(); $h = $size->getHeight(); - for ($x = 0; $x < $w; $x++) { - for ($y = 0; $y < $h; $y++) { + for ($x = 0; $x < $w; ++$x) { + for ($y = 0; $y < $h; ++$y) { $pixel = $this->getColor($fill->getColor(new Point($x, $y))); $draw->setfillcolor($pixel); @@ -550,10 +548,10 @@ public function getColorAt(PointInterface $point) } try { - $cropped = clone $this->gmagick; + $cropped = clone $this->gmagick; $histogram = $cropped ->cropImage(1, 1, $point->getX(), $point->getY()) - ->getImageHistogram(); + ->getimagehistogram(); } catch (\GmagickException $e) { throw new RuntimeException('Unable to get the pixel', $e->getCode(), $e); } @@ -566,7 +564,7 @@ public function getColorAt(PointInterface $point) } /** - * Returns a color given a pixel, depending the Palette context + * Returns a color given a pixel, depending the Palette context. * * Note : this method is public for PHP 5.3 compatibility * @@ -579,15 +577,15 @@ public function getColorAt(PointInterface $point) 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_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_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, + ColorInterface::COLOR_GRAY => \Gmagick::COLOR_RED, ); if ($this->palette->supportsAlpha()) { @@ -629,9 +627,9 @@ public function layers() 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_NONE => \Gmagick::INTERLACE_NO, + ImageInterface::INTERLACE_LINE => \Gmagick::INTERLACE_LINE, + ImageInterface::INTERLACE_PLANE => \Gmagick::INTERLACE_PLANE, ImageInterface::INTERLACE_PARTITION => \Gmagick::INTERLACE_PARTITION, ); @@ -639,7 +637,7 @@ public function interlace($scheme) throw new InvalidArgumentException('Unsupported interlace type'); } - $this->gmagick->setInterlaceScheme($supportedInterlaceSchemes[$scheme]); + $this->gmagick->setinterlacescheme($supportedInterlaceSchemes[$scheme]); return $this; } @@ -649,8 +647,8 @@ public function interlace($scheme) */ public function usePalette(PaletteInterface $palette) { - if (!isset(static::$colorspaceMapping[$palette->name()])) { - throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver',$palette->name())); + if (!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()) { @@ -659,7 +657,7 @@ public function usePalette(PaletteInterface $palette) try { try { - $hasICCProfile = (Boolean) $this->gmagick->getimageprofile('ICM'); + $hasICCProfile = (bool) $this->gmagick->getimageprofile('ICM'); } catch (\GmagickException $e) { $hasICCProfile = false; } @@ -706,18 +704,16 @@ public function profile(ProfileInterface $profile) } /** - * Internal - * * Flatten the image. */ private function flatten() { - /** + /* * @see http://pecl.php.net/bugs/bug.php?id=22435 */ - if (method_exists($this->gmagick, 'flattenImages')) { + if (method_exists($this->gmagick, 'flattenimages')) { try { - $this->gmagick = $this->gmagick->flattenImages(); + $this->gmagick = $this->gmagick->flattenimages(); } catch (\GmagickException $e) { throw new RuntimeException('Flatten operation failed', $e->getCode(), $e); } @@ -725,7 +721,7 @@ private function flatten() } /** - * Gets specifically formatted color string from Color instance + * Gets specifically formatted color string from Color instance. * * @param ColorInterface $color * @@ -743,8 +739,6 @@ private function getColor(ColorInterface $color) } /** - * Internal - * * Get the mime type based on format. * * @param string $format @@ -757,15 +751,15 @@ private function getMimeType($format) { static $mimeTypes = array( 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', 'wbmp' => 'image/vnd.wap.wbmp', - 'xbm' => 'image/xbm', + '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)); + throw new InvalidArgumentException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(', ', array_keys($mimeTypes)), $format)); } return $mimeTypes[$format]; @@ -780,11 +774,11 @@ private function getMimeType($format) */ private function setColorspace(PaletteInterface $palette) { - if (!isset(static::$colorspaceMapping[$palette->name()])) { + if (!isset(self::$colorspaceMapping[$palette->name()])) { throw new InvalidArgumentException(sprintf('The palette %s is not supported by Gmagick driver', $palette->name())); } - $this->gmagick->setimagecolorspace(static::$colorspaceMapping[$palette->name()]); + $this->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 index 448f213f975f3..3f513d12b6b02 100644 --- a/src/Symfony/Component/Image/Gmagick/Layers.php +++ b/src/Symfony/Component/Image/Gmagick/Layers.php @@ -32,7 +32,7 @@ class Layers extends AbstractLayers private $resource; /** - * @var integer + * @var int */ private $offset = 0; @@ -120,10 +120,12 @@ public function current() } /** - * Tries to extract layer at given offset + * Tries to extract layer at given offset. + * + * @param int $offset * - * @param integer $offset * @return Image + * * @throws RuntimeException */ private function extractAt($offset) @@ -235,11 +237,11 @@ public function offsetSet($offset, $image) } $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->setimageindex($offset + 1); $this->resource->nextimage(); $this->resource->addimage($frame); unset($this[0]); diff --git a/src/Symfony/Component/Image/Gmagick/Loader.php b/src/Symfony/Component/Image/Gmagick/Loader.php index d12b09e827051..a415a65580b5c 100644 --- a/src/Symfony/Component/Image/Gmagick/Loader.php +++ b/src/Symfony/Component/Image/Gmagick/Loader.php @@ -24,7 +24,7 @@ use Symfony\Component\Image\Exception\RuntimeException; /** - * Loader implementation using the Gmagick PHP extension + * Loader implementation using the Gmagick PHP extension. */ class Loader extends AbstractLoader { @@ -73,10 +73,10 @@ public function create(BoxInterface $size, ColorInterface $color = null) if ($color instanceof CMYKColor) { $switchPalette = $palette; $palette = new RGB(); - $pixel = new \GmagickPixel($palette->color((string) $color)); + $pixel = new \GmagickPixel($palette->color((string) $color)); } else { $switchPalette = null; - $pixel = new \GmagickPixel((string) $color); + $pixel = new \GmagickPixel((string) $color); } if ($color->getPalette()->supportsAlpha() && $color->getAlpha() < 100) { diff --git a/src/Symfony/Component/Image/Image/AbstractFont.php b/src/Symfony/Component/Image/Image/AbstractFont.php index d5c057652a4d2..1609d79adcda0 100644 --- a/src/Symfony/Component/Image/Image/AbstractFont.php +++ b/src/Symfony/Component/Image/Image/AbstractFont.php @@ -14,7 +14,7 @@ use Symfony\Component\Image\Image\Palette\Color\ColorInterface; /** - * Abstract font base class + * Abstract font base class. */ abstract class AbstractFont implements FontInterface { @@ -24,7 +24,7 @@ abstract class AbstractFont implements FontInterface protected $file; /** - * @var integer + * @var int */ protected $size; @@ -34,18 +34,18 @@ abstract class AbstractFont implements FontInterface protected $color; /** - * Constructs a font with specified $file, $size and $color + * Constructs a font with specified $file, $size and $color. * * The font size is to be specified in points (e.g. 10pt means 10) * * @param string $file - * @param integer $size + * @param int $size * @param ColorInterface $color */ public function __construct($file, $size, ColorInterface $color) { - $this->file = $file; - $this->size = $size; + $this->file = $file; + $this->size = $size; $this->color = $color; } diff --git a/src/Symfony/Component/Image/Image/AbstractImage.php b/src/Symfony/Component/Image/Image/AbstractImage.php index d01bd679a4575..cc86b0b22ef1e 100644 --- a/src/Symfony/Component/Image/Image/AbstractImage.php +++ b/src/Symfony/Component/Image/Image/AbstractImage.php @@ -36,7 +36,7 @@ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_ $imageSize = $this->getSize(); $ratios = array( $size->getWidth() / $imageSize->getWidth(), - $size->getHeight() / $imageSize->getHeight() + $size->getHeight() / $imageSize->getHeight(), ); $thumbnail = $this->copy(); @@ -83,7 +83,7 @@ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_ } /** - * Updates a given array of save options for backward compatibility with legacy names + * Updates a given array of save options for backward compatibility with legacy names. * * @param array $options * @@ -91,11 +91,33 @@ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_ */ protected function updateSaveOptions(array $options) { - // Preserve BC until version 1.0 + 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; } @@ -108,7 +130,7 @@ public function metadata() } /** - * Assures the metadata instance will be cloned, too + * Assures the metadata instance will be cloned, too. */ public function __clone() { @@ -116,5 +138,4 @@ public function __clone() $this->metadata = clone $this->metadata; } } - } diff --git a/src/Symfony/Component/Image/Image/AbstractLoader.php b/src/Symfony/Component/Image/Image/AbstractLoader.php index 4006aff865d53..f1b7cc7f7e814 100644 --- a/src/Symfony/Component/Image/Image/AbstractLoader.php +++ b/src/Symfony/Component/Image/Image/AbstractLoader.php @@ -57,7 +57,7 @@ public function getMetadataReader() * * @return string * - * @throws InvalidArgumentException In case the given path is invalid. + * @throws InvalidArgumentException in case the given path is invalid */ protected function checkPath($path) { diff --git a/src/Symfony/Component/Image/Image/Box.php b/src/Symfony/Component/Image/Image/Box.php index 2872e48a6f670..d0b427846f947 100644 --- a/src/Symfony/Component/Image/Image/Box.php +++ b/src/Symfony/Component/Image/Image/Box.php @@ -14,25 +14,25 @@ use Symfony\Component\Image\Exception\InvalidArgumentException; /** - * A box implementation + * A box implementation. */ final class Box implements BoxInterface { /** - * @var integer + * @var int */ private $width; /** - * @var integer + * @var int */ private $height; /** - * Constructs the Size with given width and height + * Constructs the Size with given width and height. * - * @param integer $width - * @param integer $height + * @param int $width + * @param int $height * * @throws InvalidArgumentException */ @@ -42,7 +42,7 @@ public function __construct($width, $height) 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->width = (int) $width; $this->height = (int) $height; } @@ -67,7 +67,7 @@ public function getHeight() */ public function scale($ratio) { - return new Box(round($ratio * $this->width), round($ratio * $this->height)); + return new self(round($ratio * $this->width), round($ratio * $this->height)); } /** @@ -75,7 +75,7 @@ public function scale($ratio) */ public function increase($size) { - return new Box((int) $size + $this->width, (int) $size + $this->height); + return new self((int) $size + $this->width, (int) $size + $this->height); } /** diff --git a/src/Symfony/Component/Image/Image/BoxInterface.php b/src/Symfony/Component/Image/Image/BoxInterface.php index 0fde68689da20..de6a6f3692732 100644 --- a/src/Symfony/Component/Image/Image/BoxInterface.php +++ b/src/Symfony/Component/Image/Image/BoxInterface.php @@ -12,26 +12,26 @@ namespace Symfony\Component\Image\Image; /** - * Interface for a box + * Interface for a box. */ interface BoxInterface { /** - * Gets current image height + * Gets current image height. * - * @return integer + * @return int */ public function getHeight(); /** - * Gets current image width + * Gets current image width. * - * @return integer + * @return int */ public function getWidth(); /** - * Creates new BoxInterface instance with ratios applied to both sides + * Creates new BoxInterface instance with ratios applied to both sides. * * @param float $ratio * @@ -40,9 +40,9 @@ public function getWidth(); public function scale($ratio); /** - * Creates new BoxInterface, adding given size to both sides + * Creates new BoxInterface, adding given size to both sides. * - * @param integer $size + * @param int $size * * @return BoxInterface */ @@ -50,43 +50,43 @@ 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) + * start position defaults to top left corner xy(0,0). * * @param BoxInterface $box * @param PointInterface $start * - * @return Boolean + * @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 + * given box. * - * @return integer + * @return int */ public function square(); /** - * Returns a string representation of the current box + * 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 + * Resizes box to given width, constraining proportions and returns the new box. * - * @param integer $width + * @param int $width * * @return BoxInterface */ public function widen($width); /** - * Resizes box to given height, constraining proportions and returns the new box + * Resizes box to given height, constraining proportions and returns the new box. * - * @param integer $height + * @param int $height * * @return BoxInterface */ diff --git a/src/Symfony/Component/Image/Image/Fill/FillInterface.php b/src/Symfony/Component/Image/Image/Fill/FillInterface.php index 2ec2d32c73df6..822adf2cffdab 100644 --- a/src/Symfony/Component/Image/Image/Fill/FillInterface.php +++ b/src/Symfony/Component/Image/Image/Fill/FillInterface.php @@ -15,12 +15,12 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Interface for the fill + * Interface for the fill. */ interface FillInterface { /** - * Gets color of the fill for the given position + * Gets color of the fill for the given position. * * @param PointInterface $position * diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php index 68bad65ff5921..9d40d76b4a3a0 100644 --- a/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Horizontal.php @@ -14,7 +14,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Horizontal gradient fill + * Horizontal gradient fill. */ final class Horizontal extends Linear { diff --git a/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php index 5335a16bc674e..14da307afc111 100644 --- a/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Linear.php @@ -16,12 +16,12 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Linear gradient fill + * Linear gradient fill. */ abstract class Linear implements FillInterface { /** - * @var integer + * @var int */ private $length; @@ -37,17 +37,17 @@ abstract class Linear implements FillInterface /** * Constructs a linear gradient with overall gradient length, and start and - * end shades, which default to 0 and 255 accordingly + * end shades, which default to 0 and 255 accordingly. * - * @param integer $length + * @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; + $this->start = $start; + $this->end = $end; } /** @@ -85,11 +85,11 @@ final public function getEnd() } /** - * Get the distance of the position relative to the beginning of the gradient + * Get the distance of the position relative to the beginning of the gradient. * * @param PointInterface $position * - * @return integer + * @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 index cd422d04b3026..a537ee5e43baf 100644 --- a/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php +++ b/src/Symfony/Component/Image/Image/Fill/Gradient/Vertical.php @@ -14,7 +14,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Vertical gradient fill + * Vertical gradient fill. */ final class Vertical extends Linear { diff --git a/src/Symfony/Component/Image/Image/FontInterface.php b/src/Symfony/Component/Image/Image/FontInterface.php index 3bb4ab41e2303..6caf053eda8f2 100644 --- a/src/Symfony/Component/Image/Image/FontInterface.php +++ b/src/Symfony/Component/Image/Image/FontInterface.php @@ -14,36 +14,36 @@ use Symfony\Component\Image\Image\Palette\Color\ColorInterface; /** - * The font interface + * The font interface. */ interface FontInterface { /** - * Gets the fontfile for current font + * Gets the fontfile for current font. * * @return string */ public function getFile(); /** - * Gets font's integer point size + * Gets font's integer point size. * - * @return integer + * @return int */ public function getSize(); /** - * Gets font's color + * Gets font's color. * * @return ColorInterface */ public function getColor(); /** - * Gets BoxInterface of font size on the image based on string and angle + * Gets BoxInterface of font size on the image based on string and angle. * - * @param string $string - * @param integer $angle + * @param string $string + * @param int $angle * * @return BoxInterface */ diff --git a/src/Symfony/Component/Image/Image/Histogram/Bucket.php b/src/Symfony/Component/Image/Image/Histogram/Bucket.php index c5b63cb8fa657..c030b65214eba 100644 --- a/src/Symfony/Component/Image/Image/Histogram/Bucket.php +++ b/src/Symfony/Component/Image/Image/Histogram/Bucket.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Image\Image\Histogram; /** - * Bucket histogram + * Bucket histogram. */ final class Bucket implements \Countable { @@ -22,13 +22,13 @@ final class Bucket implements \Countable private $range; /** - * @var integer + * @var int */ private $count; /** - * @param Range $range - * @param integer $count + * @param Range $range + * @param int $count */ public function __construct(Range $range, $count = 0) { @@ -37,17 +37,17 @@ public function __construct(Range $range, $count = 0) } /** - * @param integer $value + * @param int $value */ public function add($value) { if ($this->range->contains($value)) { - $this->count++; + ++$this->count; } } /** - * @return integer The number of elements in the bucket. + * @return int the number of elements in the bucket */ public function count() { diff --git a/src/Symfony/Component/Image/Image/Histogram/Range.php b/src/Symfony/Component/Image/Image/Histogram/Range.php index ff28ce9786902..0a48b9a259441 100644 --- a/src/Symfony/Component/Image/Image/Histogram/Range.php +++ b/src/Symfony/Component/Image/Image/Histogram/Range.php @@ -14,23 +14,23 @@ use Symfony\Component\Image\Exception\OutOfBoundsException; /** - * Range histogram + * Range histogram. */ final class Range { /** - * @var integer + * @var int */ private $start; /** - * @var integer + * @var int */ private $end; /** - * @param integer $start - * @param integer $end + * @param int $start + * @param int $end * * @throws OutOfBoundsException */ @@ -41,13 +41,13 @@ public function __construct($start, $end) } $this->start = $start; - $this->end = $end; + $this->end = $end; } /** - * @param integer $value + * @param int $value * - * @return Boolean + * @return bool */ public function contains($value) { diff --git a/src/Symfony/Component/Image/Image/ImageInterface.php b/src/Symfony/Component/Image/Image/ImageInterface.php index 562d47cdbd8b1..95f2d1de4130d 100644 --- a/src/Symfony/Component/Image/Image/ImageInterface.php +++ b/src/Symfony/Component/Image/Image/ImageInterface.php @@ -19,7 +19,7 @@ use Symfony\Component\Image\Exception\OutOfBoundsException; /** - * The image interface + * The image interface. */ interface ImageInterface extends ManipulatorInterface { @@ -49,7 +49,7 @@ interface ImageInterface extends ManipulatorInterface const FILTER_SINC = 'sinc'; /** - * Returns the image content as a binary string + * Returns the image content as a binary string. * * @param string $format * @param array $options @@ -61,7 +61,7 @@ interface ImageInterface extends ManipulatorInterface public function get($format, array $options = array()); /** - * Returns the image content as a PNG binary string + * Returns the image content as a PNG binary string. * * @throws RuntimeException * @@ -70,7 +70,7 @@ public function get($format, array $options = array()); public function __toString(); /** - * Instantiates and returns a DrawerInterface instance for image drawing + * Instantiates and returns a DrawerInterface instance for image drawing. * * @return DrawerInterface */ @@ -82,7 +82,7 @@ public function draw(); public function effects(); /** - * Returns current image size + * Returns current image size. * * @return BoxInterface */ @@ -90,21 +90,21 @@ public function getSize(); /** * Transforms creates a grayscale mask from current image, returns a new - * image, while keeping the existing image unmodified + * 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 + * 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 + * Returns color at specified positions of current image. * * @param PointInterface $point * @@ -125,7 +125,7 @@ public function getColorAt(PointInterface $point); public function layers(); /** - * Enables or disables interlacing + * Enables or disables interlacing. * * @param string $scheme * @@ -136,7 +136,7 @@ public function layers(); public function interlace($scheme); /** - * Return the current color palette + * Return the current color palette. * * @return PaletteInterface */ @@ -154,7 +154,7 @@ public function palette(); public function usePalette(PaletteInterface $palette); /** - * Applies a color profile on the Image + * Applies a color profile on the Image. * * @param ProfileInterface $profile * @@ -165,7 +165,7 @@ public function usePalette(PaletteInterface $palette); public function profile(ProfileInterface $profile); /** - * Returns the Image's meta data + * Returns the Image's meta data. * * @return Metadata\MetadataBag */ diff --git a/src/Symfony/Component/Image/Image/LayersInterface.php b/src/Symfony/Component/Image/Image/LayersInterface.php index 991d86adbe631..f47915f18ee52 100644 --- a/src/Symfony/Component/Image/Image/LayersInterface.php +++ b/src/Symfony/Component/Image/Image/LayersInterface.php @@ -16,23 +16,23 @@ use Symfony\Component\Image\Exception\OutOfBoundsException; /** - * The layers interface + * The layers interface. */ interface LayersInterface extends \Iterator, \Countable, \ArrayAccess { /** - * Merge layers into the original objects + * Merge layers into the original objects. * * @throws RuntimeException */ public function merge(); /** - * Animates layers + * Animates layers. * - * @param string $format The output output format - * @param integer $delay The delay in milliseconds between two frames - * @param integer $loops The number of loops, 0 means infinite + * @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 * @@ -48,7 +48,7 @@ public function animate($format, $delay, $loops); public function coalesce(); /** - * Adds an image at the end of the layers stack + * Adds an image at the end of the layers stack. * * @param ImageInterface $image * @@ -59,9 +59,9 @@ public function coalesce(); public function add(ImageInterface $image); /** - * Set an image at offset + * Set an image at offset. * - * @param integer $offset + * @param int $offset * @param ImageInterface $image * * @return LayersInterface @@ -73,9 +73,9 @@ public function add(ImageInterface $image); public function set($offset, ImageInterface $image); /** - * Removes the image at offset + * Removes the image at offset. * - * @param integer $offset + * @param int $offset * * @return LayersInterface * @@ -85,9 +85,9 @@ public function set($offset, ImageInterface $image); public function remove($offset); /** - * Returns the image at offset + * Returns the image at offset. * - * @param integer $offset + * @param int $offset * * @return ImageInterface * @@ -97,11 +97,11 @@ public function remove($offset); public function get($offset); /** - * Returns true if a layer at offset is preset + * Returns true if a layer at offset is preset. * - * @param integer $offset + * @param int $offset * - * @return Boolean + * @return bool */ public function has($offset); } diff --git a/src/Symfony/Component/Image/Image/LoaderInterface.php b/src/Symfony/Component/Image/Image/LoaderInterface.php index 34791f29f8992..39c0d5125a896 100644 --- a/src/Symfony/Component/Image/Image/LoaderInterface.php +++ b/src/Symfony/Component/Image/Image/LoaderInterface.php @@ -16,12 +16,12 @@ use Symfony\Component\Image\Exception\RuntimeException; /** - * The loader interface + * The loader interface. */ interface LoaderInterface { /** - * Creates a new empty image with an optional background color + * Creates a new empty image with an optional background color. * * @param BoxInterface $size * @param ColorInterface $color @@ -34,7 +34,7 @@ interface LoaderInterface public function create(BoxInterface $size, ColorInterface $color = null); /** - * Opens an existing image from $path + * Opens an existing image from $path. * * @param string $path * @@ -45,7 +45,7 @@ public function create(BoxInterface $size, ColorInterface $color = null); public function open($path); /** - * Loads an image from a binary $string + * Loads an image from a binary $string. * * @param string $string * @@ -56,7 +56,7 @@ public function open($path); public function load($string); /** - * Loads an image from a resource $resource + * Loads an image from a resource $resource. * * @param resource $resource * @@ -67,12 +67,12 @@ public function load($string); public function read($resource); /** - * Constructs a font with specified $file, $size and $color + * Constructs a font with specified $file, $size and $color. * * The font size is to be specified in points (e.g. 10pt means 10) * * @param string $file - * @param integer $size + * @param int $size * @param ColorInterface $color * * @return FontInterface diff --git a/src/Symfony/Component/Image/Image/ManipulatorInterface.php b/src/Symfony/Component/Image/Image/ManipulatorInterface.php index 2510222877222..af087e3348114 100644 --- a/src/Symfony/Component/Image/Image/ManipulatorInterface.php +++ b/src/Symfony/Component/Image/Image/ManipulatorInterface.php @@ -18,15 +18,15 @@ use Symfony\Component\Image\Image\Fill\FillInterface; /** - * The manipulator interface + * The manipulator interface. */ interface ManipulatorInterface { - const THUMBNAIL_INSET = 'inset'; + const THUMBNAIL_INSET = 'inset'; const THUMBNAIL_OUTBOUND = 'outbound'; /** - * Copies current source image into a new ImageInterface instance + * Copies current source image into a new ImageInterface instance. * * @throws RuntimeException * @@ -36,7 +36,7 @@ public function copy(); /** * Crops a specified box out of the source image (modifies the source image) - * Returns cropped self + * Returns cropped self. * * @param PointInterface $start * @param BoxInterface $size @@ -49,7 +49,7 @@ public function copy(); public function crop(PointInterface $start, BoxInterface $size); /** - * Resizes current image and returns self + * Resizes current image and returns self. * * @param BoxInterface $size * @param string $filter @@ -65,7 +65,7 @@ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDE * Optional $background can be used to specify the fill color of the empty * area of rotated image. * - * @param integer $angle + * @param int $angle * @param ColorInterface $background * * @throws RuntimeException @@ -77,7 +77,7 @@ 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 + * operation fails. * * Returns source image * @@ -95,7 +95,7 @@ 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 + * supported. * * @param string $path * @param array $options @@ -107,7 +107,7 @@ public function paste(ImageInterface $image, PointInterface $start); public function save($path = null, array $options = array()); /** - * Outputs the image content + * Outputs the image content. * * @param string $format * @param array $options @@ -119,7 +119,7 @@ public function save($path = null, array $options = array()); public function show($format, array $options = array()); /** - * Flips current image using horizontal axis + * Flips current image using horizontal axis. * * @throws RuntimeException * @@ -128,7 +128,7 @@ public function show($format, array $options = array()); public function flipHorizontally(); /** - * Flips current image using vertical axis + * Flips current image using vertical axis. * * @throws RuntimeException * @@ -137,7 +137,7 @@ public function flipHorizontally(); public function flipVertically(); /** - * Remove all profiles and comments + * Remove all profiles and comments. * * @throws RuntimeException * @@ -147,7 +147,7 @@ public function strip(); /** * Generates a thumbnail from a current image - * Returns it as a new image, doesn't modify the current image + * Returns it as a new image, doesn't modify the current image. * * @param BoxInterface $size * @param string $mode @@ -160,7 +160,7 @@ public function strip(); public function thumbnail(BoxInterface $size, $mode = self::THUMBNAIL_INSET, $filter = ImageInterface::FILTER_UNDEFINED); /** - * Applies a given mask to current image's alpha channel + * Applies a given mask to current image's alpha channel. * * @param ImageInterface $mask * @@ -171,7 +171,7 @@ 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 + * returns modified image. * * @param FillInterface $fill * diff --git a/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php index 0c35323a48983..7a4916fcbf57a 100644 --- a/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php +++ b/src/Symfony/Component/Image/Image/Metadata/AbstractMetadataReader.php @@ -56,7 +56,7 @@ public function readStream($resource) } /** - * Gets the URI from a stream resource + * Gets the URI from a stream resource. * * @param resource $resource * @@ -77,7 +77,7 @@ private function getStreamMetadata($resource) } /** - * Extracts metadata from a file + * Extracts metadata from a file. * * @param $file * @@ -86,7 +86,7 @@ private function getStreamMetadata($resource) abstract protected function extractFromFile($file); /** - * Extracts metadata from raw data + * Extracts metadata from raw data. * * @param $data * @@ -95,7 +95,7 @@ abstract protected function extractFromFile($file); abstract protected function extractFromData($data); /** - * Extracts metadata from a stream + * Extracts metadata from a stream. * * @param $resource * diff --git a/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php index c8f23833e8f10..c9d2ae7b5361e 100644 --- a/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php +++ b/src/Symfony/Component/Image/Image/Metadata/DefaultMetadataReader.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Image\Image\Metadata; /** - * Default metadata reader + * Default metadata reader. */ class DefaultMetadataReader extends AbstractMetadataReader { diff --git a/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php index 7dc1881d5b6e4..77ac4dff13e16 100644 --- a/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php +++ b/src/Symfony/Component/Image/Image/Metadata/ExifMetadataReader.php @@ -15,7 +15,7 @@ use Symfony\Component\Image\Exception\NotSupportedException; /** - * Metadata driven by Exif information + * Metadata driven by Exif information. */ class ExifMetadataReader extends AbstractMetadataReader { @@ -68,7 +68,7 @@ protected function extractFromStream($resource) } /** - * Extracts metadata from raw data, merges with existing metadata + * Extracts metadata from raw data, merges with existing metadata. * * @param string $data * @@ -82,13 +82,13 @@ private function doReadData($data) $mime = 'image/jpeg'; } - return $this->extract('data://' . $mime . ';base64,' . base64_encode($data)); + 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. + * @param string $path the path to the file or the data-URI representation * * @return MetadataBag */ diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php index 91087a03249ab..e18ce99da59ae 100644 --- a/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php +++ b/src/Symfony/Component/Image/Image/Metadata/MetadataBag.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Image\Image\Metadata; /** - * An interface for Image Metadata + * An interface for Image Metadata. */ class MetadataBag implements \ArrayAccess, \IteratorAggregate, \Countable { @@ -25,7 +25,7 @@ public function __construct(array $data = array()) } /** - * Returns the metadata key, default value if it does not exist + * Returns the metadata key, default value if it does not exist. * * @param string $key * @param mixed|null $default @@ -86,7 +86,7 @@ public function offsetGet($offset) } /** - * Returns metadata as an array + * Returns metadata as an array. * * @return array An associative array */ diff --git a/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php index 110b809e06ebf..57ab344dc0ea3 100644 --- a/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php +++ b/src/Symfony/Component/Image/Image/Metadata/MetadataReaderInterface.php @@ -18,9 +18,9 @@ interface MetadataReaderInterface /** * Reads metadata from a file. * - * @param $file The path to the file where to read metadata. + * @param $file the path to the file where to read metadata * - * @throws InvalidArgumentException In case the file does not exist. + * @throws InvalidArgumentException in case the file does not exist * * @return MetadataBag */ @@ -29,8 +29,8 @@ 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. + * @param $data the binary string to read + * @param $originalResource an optional resource to gather stream metadata * * @return MetadataBag */ @@ -39,9 +39,9 @@ public function readData($data, $originalResource = null); /** * Reads metadata from a stream. * - * @param $resource The stream to read. + * @param $resource the stream to read * - * @throws InvalidArgumentException In case the resource is not valid. + * @throws InvalidArgumentException in case the resource is not valid * * @return MetadataBag */ diff --git a/src/Symfony/Component/Image/Image/Palette/CMYK.php b/src/Symfony/Component/Image/Image/Palette/CMYK.php index da49bb0a45ad4..fc015571745d7 100644 --- a/src/Symfony/Component/Image/Image/Palette/CMYK.php +++ b/src/Symfony/Component/Image/Image/Palette/CMYK.php @@ -82,7 +82,7 @@ public function color($color, $alpha = null) */ public function blend(ColorInterface $color1, ColorInterface $color2, $amount) { - if (!$color1 instanceof CMYKColor || ! $color2 instanceof CMYKColor) { + if (!$color1 instanceof CMYKColor || !$color2 instanceof CMYKColor) { throw new RuntimeException('CMYK palette can only blend CMYK colors'); } @@ -110,7 +110,7 @@ public function useProfile(ProfileInterface $profile) public function profile() { if (!$this->profile) { - $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/Adobe/CMYK/USWebUncoated.icc'); + $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 index 366cb46f66803..6b2e941ce07d7 100644 --- a/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php +++ b/src/Symfony/Component/Image/Image/Palette/Color/CMYK.php @@ -18,27 +18,26 @@ final class CMYK implements ColorInterface { /** - * @var integer + * @var int */ private $c; /** - * @var integer + * @var int */ private $m; /** - * @var integer + * @var int */ private $y; /** - * @var integer + * @var int */ private $k; /** - * * @var CMYK */ private $palette; @@ -69,9 +68,9 @@ public function getValue($component) } /** - * Returns Cyan value of the color + * Returns Cyan value of the color. * - * @return integer + * @return int */ public function getCyan() { @@ -79,9 +78,9 @@ public function getCyan() } /** - * Returns Magenta value of the color + * Returns Magenta value of the color. * - * @return integer + * @return int */ public function getMagenta() { @@ -89,9 +88,9 @@ public function getMagenta() } /** - * Returns Yellow value of the color + * Returns Yellow value of the color. * - * @return integer + * @return int */ public function getYellow() { @@ -99,9 +98,9 @@ public function getYellow() } /** - * Returns Key value of the color + * Returns Key value of the color. * - * @return integer + * @return int */ public function getKeyline() { @@ -187,7 +186,7 @@ public function isOpaque() } /** - * Returns hex representation of the color + * Returns hex representation of the color. * * @return string */ @@ -197,7 +196,7 @@ public function __toString() } /** - * Internal, Performs checks for color validity (an of array(C, M, Y, K)) + * Performs checks for color validity (an of array(C, M, Y, K)). * * @param array $color * diff --git a/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php index 47c128984432e..427b19e375ac5 100644 --- a/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php +++ b/src/Symfony/Component/Image/Image/Palette/Color/ColorInterface.php @@ -31,19 +31,19 @@ interface ColorInterface * * @param string $component One of the ColorInterface::COLOR_* component * - * @return Integer + * @return int */ public function getValue($component); /** - * Returns percentage of transparency of the color + * Returns percentage of transparency of the color. * - * @return integer + * @return int */ public function getAlpha(); /** - * Returns the palette attached to the current color + * Returns the palette attached to the current color. * * @return PaletteInterface */ @@ -51,9 +51,9 @@ public function getPalette(); /** * Returns a copy of current color, incrementing the alpha channel by the - * given amount + * given amount. * - * @param integer $alpha + * @param int $alpha * * @return ColorInterface */ @@ -61,9 +61,9 @@ public function dissolve($alpha); /** * Returns a copy of the current color, lightened by the specified number - * of shades + * of shades. * - * @param integer $shade + * @param int $shade * * @return ColorInterface */ @@ -71,25 +71,25 @@ public function lighten($shade); /** * Returns a copy of the current color, darkened by the specified number of - * shades + * shades. * - * @param integer $shade + * @param int $shade * * @return ColorInterface */ public function darken($shade); /** - * Returns a gray related to the current color + * Returns a gray related to the current color. * * @return ColorInterface */ public function grayscale(); /** - * Checks if the current color is opaque + * Checks if the current color is opaque. * - * @return Boolean + * @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 index c0be1212c5d2a..fc57519f2f598 100644 --- a/src/Symfony/Component/Image/Image/Palette/Color/Gray.php +++ b/src/Symfony/Component/Image/Image/Palette/Color/Gray.php @@ -17,17 +17,16 @@ final class Gray implements ColorInterface { /** - * @var integer + * @var int */ private $gray; /** - * @var integer + * @var int */ private $alpha; /** - * * @var Grayscale */ private $palette; @@ -53,9 +52,9 @@ public function getValue($component) } /** - * Returns Gray value of the color + * Returns Gray value of the color. * - * @return integer + * @return int */ public function getGray() { @@ -121,7 +120,7 @@ public function isOpaque() } /** - * Returns hex representation of the color + * Returns hex representation of the color. * * @return string */ @@ -131,9 +130,9 @@ public function __toString() } /** - * Performs checks for validity of given alpha value and sets it + * Performs checks for validity of given alpha value and sets it. * - * @param integer $alpha + * @param int $alpha * * @throws InvalidArgumentException */ @@ -147,7 +146,7 @@ private function setAlpha($alpha) } /** - * Performs checks for color validity (array of array(gray)) + * Performs checks for color validity (array of array(gray)). * * @param array $color * diff --git a/src/Symfony/Component/Image/Image/Palette/Color/RGB.php b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php index 7cf5e16569826..fe7e2f30e911d 100644 --- a/src/Symfony/Component/Image/Image/Palette/Color/RGB.php +++ b/src/Symfony/Component/Image/Image/Palette/Color/RGB.php @@ -17,27 +17,26 @@ final class RGB implements ColorInterface { /** - * @var integer + * @var int */ private $r; /** - * @var integer + * @var int */ private $g; /** - * @var integer + * @var int */ private $b; /** - * @var integer + * @var int */ private $alpha; /** - * * @var RGBPalette */ private $palette; @@ -67,9 +66,9 @@ public function getValue($component) } /** - * Returns RED value of the color + * Returns RED value of the color. * - * @return integer + * @return int */ public function getRed() { @@ -77,9 +76,9 @@ public function getRed() } /** - * Returns GREEN value of the color + * Returns GREEN value of the color. * - * @return integer + * @return int */ public function getGreen() { @@ -87,9 +86,9 @@ public function getGreen() } /** - * Returns BLUE value of the color + * Returns BLUE value of the color. * - * @return integer + * @return int */ public function getBlue() { @@ -167,7 +166,7 @@ public function isOpaque() } /** - * Returns hex representation of the color + * Returns hex representation of the color. * * @return string */ @@ -177,11 +176,9 @@ public function __toString() } /** - * Internal - * - * Performs checks for validity of given alpha value and sets it + * Performs checks for validity of given alpha value and sets it. * - * @param integer $alpha + * @param int $alpha * * @throws InvalidArgumentException */ @@ -195,9 +192,7 @@ private function setAlpha($alpha) } /** - * Internal - * - * Performs checks for color validity (array of array(R, G, B)) + * Performs checks for color validity (array of array(R, G, B)). * * @param array $color * @@ -209,6 +204,12 @@ private function setColor(array $color) 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 index e63ad588e4c97..73136b61a576f 100644 --- a/src/Symfony/Component/Image/Image/Palette/ColorParser.php +++ b/src/Symfony/Component/Image/Image/Palette/ColorParser.php @@ -16,9 +16,9 @@ class ColorParser { /** - * Parses a color to a RGB tuple + * Parses a color to a RGB tuple. * - * @param string|array|integer $color + * @param string|array|int $color * * @return array * @@ -40,9 +40,9 @@ public function parseToRGB($color) } /** - * Parses a color to a CMYK tuple + * Parses a color to a CMYK tuple. * - * @param string|array|integer $color + * @param string|array|int $color * * @return array * @@ -60,10 +60,10 @@ public function parseToCMYK($color) $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) + 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), ); } @@ -71,9 +71,9 @@ public function parseToCMYK($color) } /** - * Parses a color to a grayscale value + * Parses a color to a grayscale value. * - * @param string|array|integer $color + * @param string|array|int $color * * @return array * @@ -95,9 +95,9 @@ public function parseToGrayscale($color) } /** - * Parses a color + * Parses a color. * - * @param string|array|integer $color + * @param string|array|int $color * * @return array * @@ -137,7 +137,7 @@ private function parse($color) } if (strlen($color) === 3) { - $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2]; + $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; } $color = array_map('hexdec', str_split($color, 2)); diff --git a/src/Symfony/Component/Image/Image/Palette/Grayscale.php b/src/Symfony/Component/Image/Image/Palette/Grayscale.php index 01f34eaba02e0..275ada8060f14 100644 --- a/src/Symfony/Component/Image/Image/Palette/Grayscale.php +++ b/src/Symfony/Component/Image/Image/Palette/Grayscale.php @@ -79,7 +79,7 @@ public function useProfile(ProfileInterface $profile) public function profile() { if (!$this->profile) { - $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC'); + $this->profile = Profile::fromPath(__DIR__.'/../../Resources/colormanagement.org/ISOcoated_v2_grey1c_bas.ICC'); } return $this->profile; @@ -109,7 +109,7 @@ public function color($color, $alpha = null) */ public function blend(ColorInterface $color1, ColorInterface $color2, $amount) { - if (!$color1 instanceof GrayColor || ! $color2 instanceof GrayColor) { + if (!$color1 instanceof GrayColor || !$color2 instanceof GrayColor) { throw new RuntimeException('Grayscale palette can only blend Grayscale colors'); } diff --git a/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php index 04d8aae3f8ba1..f4cdceba85569 100644 --- a/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php +++ b/src/Symfony/Component/Image/Image/Palette/PaletteInterface.php @@ -21,10 +21,10 @@ interface PaletteInterface const PALETTE_CMYK = 'cmyk'; /** - * Returns a color given some values + * Returns a color given some values. * - * @param string|array|integer $color A color - * @param integer|null $alpha Set alpha to null to disable it + * @param string|array|int $color A color + * @param int|null $alpha Set alpha to null to disable it * * @return ColorInterface * @@ -34,7 +34,7 @@ interface PaletteInterface public function color($color, $alpha = null); /** - * Blend two colors given an amount + * Blend two colors given an amount. * * @param ColorInterface $color1 * @param ColorInterface $color2 @@ -64,9 +64,9 @@ public function profile(); /** * Returns the name of this Palette, one of PaletteInterface::PALETTE_* - * constants + * constants. * - * @return String + * @return string */ public function name(); @@ -79,9 +79,9 @@ public function name(); public function pixelDefinition(); /** - * Tells if alpha channel is supported in this palette + * Tells if alpha channel is supported in this palette. * - * @return Boolean + * @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 index 639aaa27d74c4..80ce34248e9eb 100644 --- a/src/Symfony/Component/Image/Image/Palette/RGB.php +++ b/src/Symfony/Component/Image/Image/Palette/RGB.php @@ -83,7 +83,7 @@ public function useProfile(ProfileInterface $profile) public function profile() { if (!$this->profile) { - $this->profile = Profile::fromPath(__DIR__ . '/../../Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc'); + $this->profile = Profile::fromPath(__DIR__.'/../../Resources/color.org/sRGB_IEC61966-2-1_black_scaled.icc'); } return $this->profile; @@ -113,7 +113,7 @@ public function color($color, $alpha = null) */ public function blend(ColorInterface $color1, ColorInterface $color2, $amount) { - if (!$color1 instanceof RGBColor || ! $color2 instanceof RGBColor) { + if (!$color1 instanceof RGBColor || !$color2 instanceof RGBColor) { throw new RuntimeException('RGB palette can only blend RGB colors'); } diff --git a/src/Symfony/Component/Image/Image/Point.php b/src/Symfony/Component/Image/Image/Point.php index a33f3eada01f5..609c80ae52922 100644 --- a/src/Symfony/Component/Image/Image/Point.php +++ b/src/Symfony/Component/Image/Image/Point.php @@ -14,25 +14,25 @@ use Symfony\Component\Image\Exception\InvalidArgumentException; /** - * The point class + * The point class. */ final class Point implements PointInterface { /** - * @var integer + * @var int */ private $x; /** - * @var integer + * @var int */ private $y; /** - * Constructs a point of coordinates + * Constructs a point of coordinates. * - * @param integer $x - * @param integer $y + * @param int $x + * @param int $y * * @throws InvalidArgumentException */ @@ -75,7 +75,7 @@ public function in(BoxInterface $box) */ public function move($amount) { - return new Point($this->x + $amount, $this->y + $amount); + return new self($this->x + $amount, $this->y + $amount); } /** diff --git a/src/Symfony/Component/Image/Image/Point/Center.php b/src/Symfony/Component/Image/Image/Point/Center.php index 9a28b70828f71..63a01a9a3d9be 100644 --- a/src/Symfony/Component/Image/Image/Point/Center.php +++ b/src/Symfony/Component/Image/Image/Point/Center.php @@ -16,7 +16,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Point center + * Point center. */ final class Center implements PointInterface { @@ -26,7 +26,7 @@ final class Center implements PointInterface private $box; /** - * Constructs coordinate with size instance, it needs to be relative to + * Constructs coordinate with size instance, it needs to be relative to. * * @param BoxInterface $box */ diff --git a/src/Symfony/Component/Image/Image/PointInterface.php b/src/Symfony/Component/Image/Image/PointInterface.php index f42217e27c19d..19b50a015ee14 100644 --- a/src/Symfony/Component/Image/Image/PointInterface.php +++ b/src/Symfony/Component/Image/Image/PointInterface.php @@ -12,43 +12,44 @@ namespace Symfony\Component\Image\Image; /** - * The point interface + * The point interface. */ interface PointInterface { /** - * Gets points x coordinate + * Gets points x coordinate. * - * @return integer + * @return int */ public function getX(); /** - * Gets points y coordinate + * Gets points y coordinate. * - * @return integer + * @return int */ public function getY(); /** - * Checks if current coordinate is inside a given box + * Checks if current coordinate is inside a given box. * * @param BoxInterface $box * - * @return Boolean + * @return bool */ public function in(BoxInterface $box); /** - * Returns another point, moved by a given amount from current coordinates + * Returns another point, moved by a given amount from current coordinates. + * + * @param int $amount * - * @param integer $amount * @return ImageInterface */ public function move($amount); /** - * Gets a string representation for the current point + * Gets a string representation for the current point. * * @return string */ diff --git a/src/Symfony/Component/Image/Image/Profile.php b/src/Symfony/Component/Image/Image/Profile.php index 7374e6f525d36..6a0ad0c033d31 100644 --- a/src/Symfony/Component/Image/Image/Profile.php +++ b/src/Symfony/Component/Image/Image/Profile.php @@ -41,9 +41,9 @@ public function data() } /** - * Creates a profile from a path to a file + * Creates a profile from a path to a file. * - * @param String $path + * @param string $path * * @return Profile * diff --git a/src/Symfony/Component/Image/Image/ProfileInterface.php b/src/Symfony/Component/Image/Image/ProfileInterface.php index 3e09656c75ea7..5aeff94ec2b90 100644 --- a/src/Symfony/Component/Image/Image/ProfileInterface.php +++ b/src/Symfony/Component/Image/Image/ProfileInterface.php @@ -14,16 +14,16 @@ interface ProfileInterface { /** - * Returns the name of the profile + * Returns the name of the profile. * - * @return String + * @return string */ public function name(); /** - * Returns the profile data + * Returns the profile data. * - * @return String + * @return string */ public function data(); } diff --git a/src/Symfony/Component/Image/Imagick/Drawer.php b/src/Symfony/Component/Image/Imagick/Drawer.php index f91d68f942c0e..03d450983a030 100644 --- a/src/Symfony/Component/Image/Imagick/Drawer.php +++ b/src/Symfony/Component/Image/Imagick/Drawer.php @@ -21,7 +21,7 @@ use Symfony\Component\Image\Image\PointInterface; /** - * Drawer implementation using the Imagick PHP extension + * Drawer implementation using the Imagick PHP extension. */ final class Drawer implements DrawerInterface { @@ -43,14 +43,14 @@ public function __construct(\Imagick $imagick) */ public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1) { - $x = $center->getX(); - $y = $center->getY(); - $width = $size->getWidth(); + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); $height = $size->getHeight(); try { $pixel = $this->getColor($color); - $arc = new \ImagickDraw(); + $arc = new \ImagickDraw(); $arc->setStrokeColor($pixel); $arc->setStrokeWidth(max(1, (int) $thickness)); @@ -76,9 +76,9 @@ public function arc(PointInterface $center, BoxInterface $size, $start, $end, Co */ public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) { - $x = $center->getX(); - $y = $center->getY(); - $width = $size->getWidth(); + $x = $center->getX(); + $y = $center->getY(); + $width = $size->getWidth(); $height = $size->getHeight(); try { @@ -128,11 +128,11 @@ public function chord(PointInterface $center, BoxInterface $size, $start, $end, */ public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); try { - $pixel = $this->getColor($color); + $pixel = $this->getColor($color); $ellipse = new \ImagickDraw(); $ellipse->setStrokeColor($pixel); @@ -175,7 +175,7 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface { try { $pixel = $this->getColor($color); - $line = new \ImagickDraw(); + $line = new \ImagickDraw(); $line->setStrokeColor($pixel); $line->setStrokeWidth(max(1, (int) $thickness)); @@ -206,7 +206,7 @@ public function line(PointInterface $start, PointInterface $end, ColorInterface */ public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start))); @@ -250,7 +250,7 @@ public function dot(PointInterface $position, ColorInterface $color) $point->setFillColor($pixel); $point->point($x, $y); - $this->imagick->drawimage($point); + $this->imagick->drawImage($point); $pixel->clear(); $pixel->destroy(); @@ -278,7 +278,7 @@ public function polygon(array $coordinates, ColorInterface $color, $fill = false }, $coordinates); try { - $pixel = $this->getColor($color); + $pixel = $this->getColor($color); $polygon = new \ImagickDraw(); $polygon->setStrokeColor($pixel); @@ -312,15 +312,15 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang { try { $pixel = $this->getColor($font->getColor()); - $text = new \ImagickDraw(); + $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", ">=")) { + if (version_compare(phpversion('imagick'), '3.0.2', '>=')) { $text->setResolution(96, 96); $text->setFontSize($font->getSize()); } else { @@ -330,9 +330,9 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang $text->setTextAntialias(true); $info = $this->imagick->queryFontMetrics($text, $string); - $rad = deg2rad($angle); - $cos = cos($rad); - $sin = sin($rad); + $rad = deg2rad($angle); + $cos = cos($rad); + $sin = sin($rad); // round(0 * $cos - 0 * $sin) $x1 = 0; @@ -366,7 +366,7 @@ public function text($string, AbstractFont $font, PointInterface $position, $ang } /** - * Gets specifically formatted color string from ColorInterface instance + * Gets specifically formatted color string from ColorInterface instance. * * @param ColorInterface $color * @@ -381,21 +381,19 @@ private function getColor(ColorInterface $color) } /** - * Internal - * - * Fits a string into box with given width + * 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; + $teststring = $result.' '.$word; $testbox = $this->imagick->queryFontMetrics($text, $teststring, true); if ($testbox['textWidth'] > $width) { - $result .= ($result == '' ? '' : "\n") . $word; + $result .= ($result == '' ? '' : "\n").$word; } else { - $result .= ($result == '' ? '' : ' ') . $word; + $result .= ($result == '' ? '' : ' ').$word; } } diff --git a/src/Symfony/Component/Image/Imagick/Effects.php b/src/Symfony/Component/Image/Imagick/Effects.php index 0c484b713a45f..11fa3ef7861e8 100644 --- a/src/Symfony/Component/Image/Imagick/Effects.php +++ b/src/Symfony/Component/Image/Imagick/Effects.php @@ -18,7 +18,7 @@ use Symfony\Component\Image\Image\Palette\Color\RGB; /** - * Effects implementation using the Imagick PHP extension + * Effects implementation using the Imagick PHP extension. */ class Effects implements EffectsInterface { diff --git a/src/Symfony/Component/Image/Imagick/Font.php b/src/Symfony/Component/Image/Imagick/Font.php index d6093cbd9b5cf..4114505f8573c 100644 --- a/src/Symfony/Component/Image/Imagick/Font.php +++ b/src/Symfony/Component/Image/Imagick/Font.php @@ -16,7 +16,7 @@ use Symfony\Component\Image\Image\Palette\Color\ColorInterface; /** - * Font implementation using the Imagick PHP extension + * Font implementation using the Imagick PHP extension. */ final class Font extends AbstractFont { @@ -28,7 +28,7 @@ final class Font extends AbstractFont /** * @param \Imagick $imagick * @param string $file - * @param integer $size + * @param int $size * @param ColorInterface $color */ public function __construct(\Imagick $imagick, $file, $size, ColorInterface $color) @@ -47,12 +47,12 @@ public function box($string, $angle = 0) $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", ">=")) { + if (version_compare(phpversion('imagick'), '3.0.2', '>=')) { $text->setResolution(96, 96); $text->setFontSize($this->size); } else { diff --git a/src/Symfony/Component/Image/Imagick/Image.php b/src/Symfony/Component/Image/Imagick/Image.php index afb17ca4ebdec..32f5758f37022 100644 --- a/src/Symfony/Component/Image/Imagick/Image.php +++ b/src/Symfony/Component/Image/Imagick/Image.php @@ -29,7 +29,7 @@ use Symfony\Component\Image\Image\Palette\PaletteInterface; /** - * Image implementation using the Imagick PHP extension + * Image implementation using the Imagick PHP extension. */ final class Image extends AbstractImage { @@ -47,18 +47,18 @@ final class Image extends AbstractImage private $palette; /** - * @var Boolean + * @var bool */ private static $supportsColorspaceConversion; private static $colorspaceMapping = array( - PaletteInterface::PALETTE_CMYK => \Imagick::COLORSPACE_CMYK, - PaletteInterface::PALETTE_RGB => \Imagick::COLORSPACE_RGB, + PaletteInterface::PALETTE_CMYK => \Imagick::COLORSPACE_CMYK, + PaletteInterface::PALETTE_RGB => \Imagick::COLORSPACE_RGB, PaletteInterface::PALETTE_GRAYSCALE => \Imagick::COLORSPACE_GRAY, ); /** - * Constructs a new Image instance + * Constructs a new Image instance. * * @param \Imagick $imagick * @param PaletteInterface $palette @@ -69,7 +69,7 @@ public function __construct(\Imagick $imagick, PaletteInterface $palette, Metada $this->metadata = $metadata; $this->detectColorspaceConversionSupport(); $this->imagick = $imagick; - if (static::$supportsColorspaceConversion) { + if (self::$supportsColorspaceConversion) { $this->setColorspace($palette); } $this->palette = $palette; @@ -77,7 +77,7 @@ public function __construct(\Imagick $imagick, PaletteInterface $palette, Metada } /** - * Destroys allocated imagick resources + * Destroys allocated imagick resources. */ public function __destruct() { @@ -88,7 +88,7 @@ public function __destruct() } /** - * Returns the underlying \Imagick instance + * Returns the underlying \Imagick instance. * * @return \Imagick */ @@ -105,7 +105,7 @@ public function getImagick() public function copy() { try { - if (version_compare(phpversion("imagick"), "3.1.0b1", ">=") || defined("HHVM_VERSION")) { + if (version_compare(phpversion('imagick'), '3.1.0b1', '>=') || defined('HHVM_VERSION')) { $clone = clone $this->imagick; } else { $clone = $this->imagick->clone(); @@ -145,6 +145,7 @@ public function crop(PointInterface $start, BoxInterface $size) } catch (\ImagickException $e) { throw new RuntimeException('Crop operation failed', $e->getCode(), $e); } + return $this; } @@ -244,6 +245,7 @@ public function resize(BoxInterface $size, $filter = ImageInterface::FILTER_UNDE } catch (\ImagickException $e) { throw new RuntimeException('Resize operation failed', $e->getCode(), $e); } + return $this; } @@ -259,7 +261,7 @@ public function rotate($angle, ColorInterface $background = null) try { $pixel = $this->getColor($color); - $this->imagick->rotateimage($pixel, $angle); + $this->imagick->rotateImage($pixel, $angle); $pixel->clear(); $pixel->destroy(); @@ -326,9 +328,9 @@ public function get($format, array $options = array()) 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_NONE => \Imagick::INTERLACE_NO, + ImageInterface::INTERLACE_LINE => \Imagick::INTERLACE_LINE, + ImageInterface::INTERLACE_PLANE => \Imagick::INTERLACE_PLANE, ImageInterface::INTERLACE_PARTITION => \Imagick::INTERLACE_PARTITION, ); @@ -402,7 +404,7 @@ public function getSize() try { $i = $this->imagick->getIteratorIndex(); $this->imagick->rewind(); - $width = $this->imagick->getImageWidth(); + $width = $this->imagick->getImageWidth(); $height = $this->imagick->getImageHeight(); $this->imagick->setIteratorIndex($i); } catch (\ImagickException $e) { @@ -510,7 +512,7 @@ public function histogram() return array_map(function (\ImagickPixel $pixel) use ($image) { return $image->pixelToColor($pixel); - },$pixels); + }, $pixels); } /** @@ -532,7 +534,7 @@ public function getColorAt(PointInterface $point) } /** - * Returns a color given a pixel, depending the Palette context + * Returns a color given a pixel, depending the Palette context. * * Note : this method is public for PHP 5.3 compatibility * @@ -545,15 +547,15 @@ public function getColorAt(PointInterface $point) 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_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_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, + ColorInterface::COLOR_GRAY => \Imagick::COLOR_RED, ); $alpha = $this->palette->supportsAlpha() ? (int) round($pixel->getColorValue(\Imagick::COLOR_ALPHA) * 100) : null; @@ -585,7 +587,7 @@ public function layers() */ public function usePalette(PaletteInterface $palette) { - if (!isset(static::$colorspaceMapping[$palette->name()])) { + if (!isset(self::$colorspaceMapping[$palette->name()])) { throw new InvalidArgumentException(sprintf('The palette %s is not supported by Imagick driver', $palette->name())); } @@ -593,13 +595,13 @@ public function usePalette(PaletteInterface $palette) return $this; } - if (!static::$supportsColorspaceConversion) { + if (!self::$supportsColorspaceConversion) { throw new RuntimeException('Your version of Imagick does not support colorspace conversions.'); } try { try { - $hasICCProfile = (Boolean) $this->imagick->getImageProfile('icc'); + $hasICCProfile = (bool) $this->imagick->getImageProfile('icc'); } catch (\ImagickException $e) { $hasICCProfile = false; } @@ -640,13 +642,11 @@ public function profile(ProfileInterface $profile) } /** - * Internal - * * Flatten the image. */ private function flatten() { - /** + /* * @see https://github.com/mkoppanen/imagick/issues/45 */ try { @@ -661,9 +661,7 @@ private function flatten() } /** - * Internal - * - * Applies options before save or output + * Applies options before save or output. * * @param \Imagick $image * @param array $options @@ -714,27 +712,27 @@ private function applyImageOptions(\Imagick $image, array $options, $path) $image->setImageCompressionQuality($compression); } - if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) { - if ($options['resolution-units'] == ImageInterface::RESOLUTION_PIXELSPERCENTIMETER) { + 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) { + } 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']; + 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); + $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 + * Gets specifically formatted color string from Color instance. * * @param ColorInterface $color * @@ -749,11 +747,11 @@ private function getColor(ColorInterface $color) } /** - * Checks whether given $fill is linear and opaque + * Checks whether given $fill is linear and opaque. * * @param FillInterface $fill * - * @return Boolean + * @return bool */ private function isLinearOpaque(FillInterface $fill) { @@ -761,15 +759,15 @@ private function isLinearOpaque(FillInterface $fill) } /** - * Performs optimized gradient fill for non-opaque linear gradients + * 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()); + $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); @@ -784,7 +782,7 @@ private function applyFastLinear(Linear $fill) } /** - * Internal + * Internal. * * Get the mime type based on format. * @@ -798,15 +796,15 @@ private function getMimeType($format) { static $mimeTypes = array( 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', 'wbmp' => 'image/vnd.wap.wbmp', - 'xbm' => 'image/xbm', + '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)); + throw new RuntimeException(sprintf('Unsupported format given. Only %s are supported, %s given', implode(', ', array_keys($mimeTypes)), $format)); } return $mimeTypes[$format]; @@ -829,17 +827,17 @@ private function setColorspace(PaletteInterface $palette) // 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_CMYK => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7, + PaletteInterface::PALETTE_RGB => defined('\Imagick::IMGTYPE_TRUECOLORMATTE') ? \Imagick::IMGTYPE_TRUECOLORMATTE : 7, PaletteInterface::PALETTE_GRAYSCALE => defined('\Imagick::IMGTYPE_GRAYSCALEMATTE') ? \Imagick::IMGTYPE_GRAYSCALEMATTE : 3, ); - if (!isset(static::$colorspaceMapping[$palette->name()])) { + 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(static::$colorspaceMapping[$palette->name()]); + $this->imagick->setColorspace(self::$colorspaceMapping[$palette->name()]); $this->palette = $palette; } @@ -847,15 +845,15 @@ private function setColorspace(PaletteInterface $palette) * Older imagemagick versions does not support colorspace conversions. * Let's detect if it is supported. * - * @return Boolean + * @return bool */ private function detectColorspaceConversionSupport() { - if (null !== static::$supportsColorspaceConversion) { - return static::$supportsColorspaceConversion; + if (null !== self::$supportsColorspaceConversion) { + return self::$supportsColorspaceConversion; } - return static::$supportsColorspaceConversion = method_exists('Imagick', 'setColorspace'); + return self::$supportsColorspaceConversion = method_exists('Imagick', 'setColorspace'); } /** @@ -865,27 +863,27 @@ private function detectColorspaceConversionSupport() * * @return string * - * @throws InvalidArgumentException If the filter is unsupported. + * @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_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 + ImageInterface::FILTER_SINC => \Imagick::FILTER_SINC, + ImageInterface::FILTER_TRIANGLE => \Imagick::FILTER_TRIANGLE, ); if (!array_key_exists($filter, $supportedFilters)) { diff --git a/src/Symfony/Component/Image/Imagick/Layers.php b/src/Symfony/Component/Image/Imagick/Layers.php index 860ee8f6f5331..3e5af706a880c 100644 --- a/src/Symfony/Component/Image/Imagick/Layers.php +++ b/src/Symfony/Component/Image/Imagick/Layers.php @@ -29,7 +29,7 @@ class Layers extends AbstractLayers */ private $resource; /** - * @var integer + * @var int */ private $offset = 0; /** @@ -110,7 +110,7 @@ public function coalesce() } $count = $coalescedResource->getNumberImages(); - for ($offset = 0; $offset < $count; $offset++) { + for ($offset = 0; $offset < $count; ++$offset) { try { $coalescedResource->setIteratorIndex($offset); $this->layers[$offset] = new Image($coalescedResource->getImage(), $this->palette, new MetadataBag()); @@ -129,11 +129,12 @@ public function current() } /** - * Tries to extract layer at given offset + * Tries to extract layer at given offset. * - * @param integer $offset + * @param int $offset * * @return Image + * * @throws RuntimeException */ private function extractAt($offset) diff --git a/src/Symfony/Component/Image/Imagick/Loader.php b/src/Symfony/Component/Image/Imagick/Loader.php index 46991c797473a..378b54bccd1b5 100644 --- a/src/Symfony/Component/Image/Imagick/Loader.php +++ b/src/Symfony/Component/Image/Imagick/Loader.php @@ -23,7 +23,7 @@ use Symfony\Component\Image\Image\Palette\Grayscale; /** - * Loader implementation using the Imagick PHP extension + * Loader implementation using the Imagick PHP extension. */ final class Loader extends AbstractLoader { @@ -65,7 +65,7 @@ public function open($path) */ public function create(BoxInterface $size, ColorInterface $color = null) { - $width = $size->getWidth(); + $width = $size->getWidth(); $height = $size->getHeight(); $palette = null !== $color ? $color->getPalette() : new RGB(); @@ -144,7 +144,7 @@ public function font($file, $size, ColorInterface $color) } /** - * Returns the palette corresponding to an \Imagick resource colorspace + * Returns the palette corresponding to an \Imagick resource colorspace. * * @param \Imagick $imagick * @@ -168,7 +168,7 @@ private function createPalette(\Imagick $imagick) } /** - * Returns ImageMagick version + * Returns ImageMagick version. * * @param \Imagick $imagick * 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/Tests/Constraint/IsImageEqual.php b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php index 75fbd1f2809f9..f3de42290d0ef 100644 --- a/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php +++ b/src/Symfony/Component/Image/Tests/Constraint/IsImageEqual.php @@ -12,14 +12,29 @@ 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{} + abstract class PHPUnitConstraint extends \PHPUnit_Framework_Constraint + { + } } else { - abstract class PHPUnitConstraint extends Constraint{} + 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 @@ -35,33 +50,33 @@ class IsImageEqual extends PHPUnitConstraint private $delta; /** - * @var integer + * @var int */ private $buckets; /** * @param \Symfony\Component\Image\Image\ImageInterface $value - * @param float $delta - * @param integer $buckets + * @param float $delta + * @param int $buckets * * @throws InvalidArgumentException */ public function __construct($value, $delta = 0.1, $buckets = 4) { if (!$value instanceof ImageInterface) { - throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class); + throw PHPUnitInvalidArgumentHelper::factory(1, ImageInterface::class); } if (!is_numeric($delta)) { - throw \PHPUnit_Util_InvalidArgumentHelper::factory(2, 'numeric'); + throw PHPUnitInvalidArgumentHelper::factory(2, 'numeric'); } if (!is_integer($buckets) || $buckets <= 0) { - throw \PHPUnit_Util_InvalidArgumentHelper::factory(3, 'integer'); + throw PHPUnitInvalidArgumentHelper::factory(3, 'integer'); } - $this->value = $value; - $this->delta = $delta; + $this->value = $value; + $this->delta = $delta; $this->buckets = $buckets; } @@ -71,11 +86,11 @@ public function __construct($value, $delta = 0.1, $buckets = 4) public function evaluate($other, $description = '', $returnResult = false) { if (!$other instanceof ImageInterface) { - throw \PHPUnit_Util_InvalidArgumentHelper::factory(1, ImageInterface::class); + throw PHPUnitInvalidArgumentHelper::factory(1, ImageInterface::class); } list($currentRed, $currentGreen, $currentBlue, $currentAlpha) = $this->normalize($this->value); - list($otherRed, $otherGreen, $otherBlue, $otherAlpha) = $this->normalize($other); + list($otherRed, $otherGreen, $otherBlue, $otherAlpha) = $this->normalize($other); $total = 0; @@ -113,18 +128,18 @@ public function toString() */ private function normalize(ImageInterface $image) { - $step = (int) round(255 / $this->buckets); + $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); + 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); + $blue[] = new Bucket($range); $alpha[] = new Bucket($range); } diff --git a/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php index cf45706f4a137..ec0daa883b7b9 100644 --- a/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php +++ b/src/Symfony/Component/Image/Tests/Draw/AbstractDrawerTest.php @@ -33,83 +33,73 @@ public function testDrawASmileyFace() ->ellipse(new Point(125, 100), new Box(50, 50), $this->getColor('fff')) ->ellipse(new Point(275, 100), new Box(50, 50), $this->getColor('fff'), true); - $canvas->save(__DIR__.'/../results/smiley.png'); + $canvas->save($this->getTempDir().'/smiley.png'); - $this->assertTrue(file_exists(__DIR__.'/../results/smiley.png')); - - unlink(__DIR__.'/../results/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 = $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(__DIR__.'/../results/ellipse.png'); - - $this->assertTrue(file_exists(__DIR__.'/../results/ellipse.png')); + $canvas->save($this->getTempDir().'/ellipse.png'); - unlink(__DIR__.'/../results/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 = $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(__DIR__.'/../results/pie.png'); - - $this->assertTrue(file_exists(__DIR__.'/../results/pie.png')); + $canvas->save($this->getTempDir().'/pie.png'); - unlink(__DIR__.'/../results/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 = $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(__DIR__.'/../results/chord.png'); - - $this->assertTrue(file_exists(__DIR__.'/../results/chord.png')); + $canvas->save($this->getTempDir().'/chord.png'); - unlink(__DIR__.'/../results/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 = $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(__DIR__.'/../results/lines.png'); + $canvas->save($this->getTempDir().'/lines.png'); - $this->assertTrue(file_exists(__DIR__.'/../results/lines.png')); - - unlink(__DIR__.'/../results/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 = $loader->create(new Box(400, 300), $this->getColor('000')); $canvas->draw() ->polygon(array( @@ -119,18 +109,16 @@ public function testDrawAPolygon() new Point(50, 280), ), $this->getColor('fff'), true); - $canvas->save(__DIR__.'/../results/polygon.png'); + $canvas->save($this->getTempDir().'/polygon.png'); - $this->assertTrue(file_exists(__DIR__.'/../results/polygon.png')); - - unlink(__DIR__.'/../results/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 = $loader->create(new Box(400, 300), $this->getColor('000')); $canvas->draw() ->dot(new Point(200, 150), $this->getColor('fff')) @@ -138,28 +126,24 @@ public function testDrawADot() ->dot(new Point(200, 152), $this->getColor('fff')) ->dot(new Point(200, 153), $this->getColor('fff')); - $canvas->save(__DIR__.'/../results/dot.png'); - - $this->assertTrue(file_exists(__DIR__.'/../results/dot.png')); + $canvas->save($this->getTempDir().'/dot.png'); - unlink(__DIR__.'/../results/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 = $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(__DIR__.'/../results/arc.png'); - - $this->assertTrue(file_exists(__DIR__.'/../results/arc.png')); + $canvas->save($this->getTempDir().'/arc.png'); - unlink(__DIR__.'/../results/arc.png'); + $this->assertFileExists($this->getTempDir().'/arc.png'); } public function testDrawText() @@ -168,16 +152,16 @@ public function testDrawText() $this->markTestSkipped('This install does not support font tests'); } - $path = Loader::getFixture('font/Arial.ttf'); - $black = $this->getColor('000'); - $file36 = __DIR__.'/../results/bulat36.png'; - $file24 = __DIR__.'/../results/bulat24.png'; - $file18 = __DIR__.'/../results/bulat18.png'; - $file12 = __DIR__.'/../results/bulat12.png'; + $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 = $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); @@ -186,12 +170,10 @@ public function testDrawText() unset($canvas); - $this->assertTrue(file_exists($file36)); - - unlink($file36); + $this->assertFileExists($file36); $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); - $font = $loader->font($path, 24, $black); + $font = $loader->font($path, 24, $black); $canvas->draw() ->text('Bulat', $font, new Point(24, 24)); @@ -200,12 +182,10 @@ public function testDrawText() unset($canvas); - $this->assertTrue(file_exists($file24)); - - unlink($file24); + $this->assertFileExists($file24); $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); - $font = $loader->font($path, 18, $black); + $font = $loader->font($path, 18, $black); $canvas->draw() ->text('Bulat', $font, new Point(18, 18)); @@ -214,12 +194,10 @@ public function testDrawText() unset($canvas); - $this->assertTrue(file_exists($file18)); - - unlink($file18); + $this->assertFileExists($file18); $canvas = $loader->create(new Box(400, 300), $this->getColor('fff')); - $font = $loader->font($path, 12, $black); + $font = $loader->font($path, 12, $black); $canvas->draw() ->text('Bulat', $font, new Point(12, 12)); @@ -228,9 +206,7 @@ public function testDrawText() unset($canvas); - $this->assertTrue(file_exists($file12)); - - unlink($file12); + $this->assertFileExists($file12); } private function getColor($color) diff --git a/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php index 301a776211cb7..b7b08e5e68022 100644 --- a/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php +++ b/src/Symfony/Component/Image/Tests/Effects/AbstractEffectsTest.php @@ -19,7 +19,6 @@ abstract class AbstractEffectsTest extends TestCase { - public function testNegate() { $palette = new RGB(); diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php index 18b82364f9053..5727db3e22961 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/BorderTest.php @@ -20,10 +20,10 @@ class BorderTest extends FilterTestCase { public function testBorderImage() { - $color = $this->getMockBuilder(ColorInterface::class)->getMock(); - $width = 2; - $height = 4; - $image = $this->getImage(); + $color = $this->getMockBuilder(ColorInterface::class)->getMock(); + $width = 2; + $height = 4; + $image = $this->getImage(); $size = $this->getMockBuilder(BoxInterface::class)->getMock(); $size->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php index 7e72bc4c2784d..493d4493e3dba 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/CanvasTest.php @@ -47,7 +47,7 @@ public function testShouldCanvasImageAndReturnResult(BoxInterface $size, PointIn } /** - * Data provider for testShouldCanvasImageAndReturnResult + * Data provider for testShouldCanvasImageAndReturnResult. * * @return array */ diff --git a/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php b/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php index 0d3f457dafd62..21877a322e154 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Advanced/GrayscaleTest.php @@ -31,8 +31,8 @@ class GrayscaleTest extends FilterTestCase */ public function testGrayscaling(BoxInterface $size, ColorInterface $color, ColorInterface $filteredColor) { - $image = $this->getImage(); - $imageWidth = $size->getWidth(); + $image = $this->getImage(); + $imageWidth = $size->getWidth(); $imageHeight = $size->getHeight(); $size = $this->getMockBuilder(BoxInterface::class)->getMock(); @@ -48,20 +48,20 @@ public function testGrayscaling(BoxInterface $size, ColorInterface $color, Color ->method('getSize') ->will($this->returnValue($size)); - $image->expects($this->exactly($imageWidth*$imageHeight)) + $image->expects($this->exactly($imageWidth * $imageHeight)) ->method('getColorAt') ->will($this->returnValue($color)); - $color->expects($this->exactly($imageWidth*$imageHeight)) + $color->expects($this->exactly($imageWidth * $imageHeight)) ->method('grayscale') ->will($this->returnValue($filteredColor)); $draw = $this->getDrawer(); - $draw->expects($this->exactly($imageWidth*$imageHeight)) + $draw->expects($this->exactly($imageWidth * $imageHeight)) ->method('dot') ->with($this->isInstanceOf(Point::class), $this->equalTo($filteredColor)); - $image->expects($this->exactly($imageWidth*$imageHeight)) + $image->expects($this->exactly($imageWidth * $imageHeight)) ->method('draw') ->will($this->returnValue($draw)); @@ -70,7 +70,7 @@ public function testGrayscaling(BoxInterface $size, ColorInterface $color, Color } /** - * Data provider for testShouldCanvasImageAndReturnResult + * Data provider for testShouldCanvasImageAndReturnResult. * * @return array */ diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php index b13fd4793fcfb..56a12ab593180 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/AutorotateTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Image\Tests\Filter\Basic; -USE Symfony\Component\Image\Filter\Basic\Autorotate; +use Symfony\Component\Image\Filter\Basic\Autorotate; use Symfony\Component\Image\Image\Metadata\MetadataBag; use Symfony\Component\Image\Tests\Filter\FilterTestCase; diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php index 76fb138006761..ed7709ca03530 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CopyTest.php @@ -19,8 +19,8 @@ class CopyTest extends FilterTestCase public function testShouldCopyAndReturnResultingImage() { $command = new Copy(); - $image = $this->getImage(); - $clone = $this->getImage(); + $image = $this->getImage(); + $clone = $this->getImage(); $image->expects($this->once()) ->method('copy') diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php index 4ce0acc61f426..331b57e49ad90 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/CropTest.php @@ -43,7 +43,7 @@ public function testShouldApplyCropAndReturnResult(PointInterface $start, BoxInt } /** - * Provides coordinates and sizes for testShouldApplyCropAndReturnResult + * Provides coordinates and sizes for testShouldApplyCropAndReturnResult. * * @return array */ @@ -51,7 +51,7 @@ public function getDataSet() { return array( array(new Point(0, 0), new Box(40, 50)), - array(new Point(0, 15), new Box(50, 32)) + 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 index 6f8236f9863c2..501bd19ad77ab 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipHorizontallyTest.php @@ -18,7 +18,7 @@ class FlipHorizontallyTest extends FilterTestCase { public function testShouldFlipImage() { - $image = $this->getImage(); + $image = $this->getImage(); $filter = new FlipHorizontally(); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php index c1014c27f6cd3..0abcc069aad49 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/FlipVerticallyTest.php @@ -18,7 +18,7 @@ class FlipVerticallyTest extends FilterTestCase { public function testShouldFlipImage() { - $image = $this->getImage(); + $image = $this->getImage(); $filter = new FlipVertically(); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php index 096d9147b4da7..75a39e8a55128 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/PasteTest.php @@ -19,10 +19,10 @@ class PasteTest extends FilterTestCase { public function testShouldFlipImage() { - $start = new Point(0, 0); - $image = $this->getImage(); + $start = new Point(0, 0); + $image = $this->getImage(); $toPaste = $this->getImage(); - $filter = new Paste($toPaste, $start); + $filter = new Paste($toPaste, $start); $image->expects($this->once()) ->method('paste') diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php index 2c0569298331c..5ea21e1471db1 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ResizeTest.php @@ -40,7 +40,7 @@ public function testShouldResizeImageAndReturnResult(BoxInterface $size) } /** - * Data provider for testShouldResizeImageAndReturnResult + * Data provider for testShouldResizeImageAndReturnResult. * * @return array */ @@ -50,7 +50,7 @@ public function getDataSet() array(new Box(50, 15)), array(new Box(300, 25)), array(new Box(123, 23)), - array(new Box(45, 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 index e9ee8e0572ceb..fb2ace3eeef45 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/RotateTest.php @@ -18,8 +18,8 @@ class RotateTest extends FilterTestCase { public function testShouldRotateImageAndReturnResult() { - $image = $this->getImage(); - $angle = 90; + $image = $this->getImage(); + $angle = 90; $command = new Rotate($angle); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php index b80987a5791f3..b70d162cb0c7b 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/SaveTest.php @@ -18,8 +18,8 @@ class SaveTest extends FilterTestCase { public function testShouldSaveImageAndReturnResult() { - $image = $this->getImage(); - $path = '/path/to/image.jpg'; + $image = $this->getImage(); + $path = '/path/to/image.jpg'; $command = new Save($path); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php index 38e5aebf541ed..5e95374e8b5ae 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ShowTest.php @@ -18,8 +18,8 @@ class ShowTest extends FilterTestCase { public function testShouldShowImageAndReturnResult() { - $image = $this->getImage(); - $format = 'jpg'; + $image = $this->getImage(); + $format = 'jpg'; $command = new Show($format); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php index 208ee36b2ddc6..688de7d1fe420 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/StripTest.php @@ -18,7 +18,7 @@ class StripTest extends FilterTestCase { public function testShouldStripImage() { - $image = $this->getImage(); + $image = $this->getImage(); $filter = new Strip(); $image->expects($this->once()) diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php index b24242c2e6b2c..d0acc2dee177f 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/ThumbnailTest.php @@ -20,10 +20,10 @@ class ThumbnailTest extends FilterTestCase { public function testShouldMakeAThumbnail() { - $image = $this->getImage(); + $image = $this->getImage(); $thumbnail = $this->getImage(); - $size = new Box(50, 50); - $filter = new Thumbnail($size); + $size = new Box(50, 50); + $filter = new Thumbnail($size); $image->expects($this->once()) ->method('thumbnail') diff --git a/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php index 6be7368926ccc..585594dd5354e 100644 --- a/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/Basic/WebOptimizationTest.php @@ -20,8 +20,8 @@ class WebOptimizationTest extends FilterTestCase { public function testShouldNotSave() { - $image = $this->getImage(); - $filter = new WebOptimization(); + $image = $this->getImage(); + $filter = new WebOptimization(); $image->expects($this->once()) ->method('usePalette') @@ -40,12 +40,14 @@ public function testShouldNotSave() public function testShouldSaveWithCallbackAndCustomOption() { - $image = $this->getImage(); - $result = '/path/to/ploum'; - $path = function (ImageInterface $image) use ($result) { return $result; }; - $filter = new WebOptimization($path, array( + $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, + 'resolution_y' => 100, )); $capturedOptions = null; @@ -71,18 +73,18 @@ public function testShouldSaveWithCallbackAndCustomOption() $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']); + $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( + $image = $this->getImage(); + $path = '/path/to/dest'; + $filter = new WebOptimization($path, array( 'custom-option' => 'custom-value', - 'resolution-y' => 100, + 'resolution_y' => 100, )); $capturedOptions = null; @@ -108,8 +110,8 @@ public function testShouldSaveWithPathAndCustomOption() $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']); + $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 index aadc75159864f..d415c2bd8c35a 100644 --- a/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php +++ b/src/Symfony/Component/Image/Tests/Filter/DummyLoaderAwareFilter.php @@ -14,7 +14,8 @@ class DummyLoaderAwareFilter extends LoaderAware /** * Apply filter. * - * @param ImageInterface $image An ImageInterface instance + * @param ImageInterface $image An ImageInterface instance + * * @return ImageInterface */ public function apply(ImageInterface $image) diff --git a/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php b/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php index 956ce09b93623..5dce68e8dc2c5 100644 --- a/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php +++ b/src/Symfony/Component/Image/Tests/Filter/TransformationTest.php @@ -23,8 +23,8 @@ class TransformationTest extends FilterTestCase public function testSimpleStack() { $image = $this->getImage(); - $size = new Box(50, 50); - $path = sys_get_temp_dir(); + $size = new Box(50, 50); + $path = sys_get_temp_dir(); $image->expects($this->once()) ->method('resize') @@ -45,13 +45,13 @@ public function testSimpleStack() 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; + $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()) @@ -91,10 +91,10 @@ public function testComplexFlow() public function testCropFlipPasteShow() { - $img1 = $this->getImage(); - $img2 = $this->getImage(); + $img1 = $this->getImage(); + $img2 = $this->getImage(); $start = new Point(0, 0); - $size = new Box(50, 50); + $size = new Box(50, 50); $img1->expects($this->once()) ->method('paste') diff --git a/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php index e0de826fb3a2b..71ed4d0cd7865 100644 --- a/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php +++ b/src/Symfony/Component/Image/Tests/Functional/GdTransparentGifHandlingTest.php @@ -33,10 +33,10 @@ private function getLoader() public function testShouldResize() { $loader = $this->getLoader(); - $new = sys_get_temp_dir()."/sample.jpeg"; + $new = sys_get_temp_dir().'/sample.jpeg'; $image = $loader->open(FixturesLoader::getFixture('xparent.gif')); - $size = $image->getSize()->scale(0.5); + $size = $image->getSize()->scale(0.5); $image ->resize($size) @@ -50,7 +50,5 @@ public function testShouldResize() $this->assertSame(272, $image->getSize()->getWidth()); $this->assertSame(171, $image->getSize()->getHeight()); - - unlink($new); } } diff --git a/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php b/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php index a4e914855b842..49a5cb16ab17f 100644 --- a/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php +++ b/src/Symfony/Component/Image/Tests/Gmagick/ImageTest.php @@ -91,7 +91,6 @@ public function testFillAlphaPrecision() $this->markTestSkipped('Alpha transparency is not supported by Gmagick'); } - protected function getLoader() { return new Loader(); diff --git a/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php index 0d913aa91d807..b70a14d1bba34 100644 --- a/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php +++ b/src/Symfony/Component/Image/Tests/Image/AbstractImageTest.php @@ -96,12 +96,11 @@ public function testUsePalette($from, $to, $color) $image->usePalette($targetPalette); $this->assertEquals($targetPalette, $image->palette()); - $image->save(__DIR__ . '/tmp.jpg'); + $image->save($this->getTempDir().'/tmp.jpg'); - $image = $this->getLoader()->open(__DIR__ . '/tmp.jpg'); + $image = $this->getLoader()->open($this->getTempDir().'/tmp.jpg'); $this->assertInstanceOf($to, $image->palette()); - unlink(__DIR__ . '/tmp.jpg'); } public function testSaveWithoutFormatShouldSaveInOriginalFormat() @@ -110,7 +109,7 @@ public function testSaveWithoutFormatShouldSaveInOriginalFormat() $this->markTestSkipped('The EXIF extension is required for this test'); } - $tmpFile = __DIR__ . '/tmpfile'; + $tmpFile = $this->getTempDir().'/tmpfile'; $this ->getLoader() @@ -119,17 +118,12 @@ public function testSaveWithoutFormatShouldSaveInOriginalFormat() $data = exif_read_data($tmpFile); $this->assertEquals('image/jpeg', $data['MimeType']); - unlink($tmpFile); } public function testSaveWithoutPathFileFromImageLoadShouldBeOkay() { $source = FixturesLoader::getFixture('google.png'); - $tmpFile = __DIR__ . '/../results/google.tmp.png'; - - if (file_exists($tmpFile)) { - unlink($tmpFile); - } + $tmpFile = $this->getTempDir().'/google.tmp.png'; copy($source, $tmpFile); @@ -142,7 +136,6 @@ public function testSaveWithoutPathFileFromImageLoadShouldBeOkay() ->save(); $this->assertNotEquals(md5_file($source), md5_file($tmpFile)); - unlink($tmpFile); } public function testSaveWithoutPathFileFromImageCreationShouldFail() @@ -227,7 +220,7 @@ public function testCopyResizedImageToImage() $factory = $this->getLoader(); $image = $factory->open(FixturesLoader::getFixture('google.png')); - $size = $image->getSize(); + $size = $image->getSize(); $image = $image->paste( $image->copy() @@ -300,7 +293,7 @@ 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"); + $image->thumbnail(new Box(20, 20), 'boumboum'); } public function testResizeShouldReturnTheImage() @@ -319,8 +312,8 @@ public function testResizeShouldReturnTheImage() 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); + $image = $factory->create(new Box($sourceW, $sourceH)); + $inset = $image->thumbnail(new Box($thumbW, $thumbH), $mode); $size = $inset->getSize(); @@ -377,13 +370,13 @@ public function testThumbnailGenerationToDimensionsLergestThanSource() $height = $test_image_height + 1; $factory = $this->getLoader(); - $image = $factory->open($test_image); + $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); + $inset = $image->thumbnail(new Box($width, $height), ImageInterface::THUMBNAIL_INSET); $size = $inset->getSize(); unset($inset); @@ -422,9 +415,9 @@ public function testCreateAndSaveEmptyImage() $palette = new RGB(); - $image = $factory->create(new Box(400, 300), $palette->color('000')); + $image = $factory->create(new Box(400, 300), $palette->color('000')); - $size = $image->getSize(); + $size = $image->getSize(); unset($image); @@ -438,8 +431,8 @@ public function testCreateTransparentGradient() $palette = new RGB(); - $size = new Box(100, 50); - $image = $factory->create($size, $palette->color('f00')); + $size = new Box(100, 50); + $image = $factory->create($size, $palette->color('f00')); $image->paste( $factory->create($size, $palette->color('ff0')) @@ -471,15 +464,13 @@ public function testMask() $image = $factory->open(FixturesLoader::getFixture('google.png')); $image->applyMask($image->mask()) - ->save(__DIR__.'/../results/mask.png'); + ->save($this->getTempDir().'/mask.png'); - $size = $factory->open(__DIR__.'/../results/mask.png') + $size = $factory->open($this->getTempDir().'/mask.png') ->getSize(); $this->assertEquals(364, $size->getWidth()); $this->assertEquals(126, $size->getHeight()); - - unlink(__DIR__.'/../results/mask.png'); } public function testColorHistogram() @@ -495,11 +486,11 @@ public function testImageResolutionChange() { $loader = $this->getLoader(); $image = $loader->open(FixturesLoader::getFixture('resize/210-design-19933.jpg')); - $outfile = __DIR__.'/../results/reduced.jpg'; + $outfile = $this->getTempDir().'/reduced.jpg'; $image->save($outfile, array( - 'resolution-units' => ImageInterface::RESOLUTION_PIXELSPERINCH, - 'resolution-x' => 144, - 'resolution-y' => 144 + 'resolution_units' => ImageInterface::RESOLUTION_PIXELSPERINCH, + 'resolution_x' => 144, + 'resolution_y' => 144, )); if ($loader instanceof ImagickLoader) { @@ -514,24 +505,22 @@ public function testImageResolutionChange() $this->assertEquals(144, $info['x']); $this->assertEquals(144, $info['y']); } - - unlink($outfile); } public function testInOutResult() { - $this->processInOut("trans", "png","png"); - $this->processInOut("trans", "png","gif"); - $this->processInOut("trans", "png","jpg"); - $this->processInOut("anima", "gif","png"); - $this->processInOut("anima", "gif","gif"); - $this->processInOut("anima", "gif","jpg"); - $this->processInOut("trans", "gif","png"); - $this->processInOut("trans", "gif","gif"); - $this->processInOut("trans", "gif","jpg"); - $this->processInOut("large", "jpg","png"); - $this->processInOut("large", "jpg","gif"); - $this->processInOut("large", "jpg","jpg"); + $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() @@ -680,8 +669,7 @@ public function testResizeAnimatedGifResizeResult() $frame->resize(new Box(121, 124)); } - $image->save(__DIR__.'/../results/anima-half-size.gif', array('animated' => true)); - @unlink(__DIR__.'/../results/anima-half-size.gif'); + $image->save($this->getTempDir().'/anima-half-size.gif', array('animated' => true)); $image = $loader->open(FixturesLoader::getFixture('anima2.gif')); @@ -694,12 +682,10 @@ public function testResizeAnimatedGifResizeResult() $frame->resize(new Box(200, 144)); } - $target = __DIR__.'/../results/anima2-half-size.gif'; + $target = $this->getTempDir().'/anima2-half-size.gif'; $image->save($target, array('animated' => true)); $this->assertFileExists($target); - - @unlink($target); } public function testMetadataReturnsMetadataInstance() @@ -732,19 +718,18 @@ public function testImageSizeOnAnimatedGif() */ public function testResolutionOnSave($source) { - $file = __DIR__ . '/test-resolution.jpg'; + $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, + 'resolution_units' => ImageInterface::RESOLUTION_PIXELSPERINCH, + 'resolution_x' => 150, + 'resolution_y' => 120, + 'resampling_filter' => ImageInterface::FILTER_LANCZOS, )); $saved = $this->getLoader()->open($file); $this->assertEquals(array('x' => 150, 'y' => 120), $this->getImageResolution($saved)); - unlink($file); } public function provideVariousSources() @@ -759,8 +744,8 @@ 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 = $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)); @@ -770,10 +755,10 @@ public function testFillAlphaPrecision() public function testImageCreatedAlpha() { $palette = new RGB(); - $image = $this->getLoader()->create(new Box(1, 1), $palette->color("#7f7f7f", 10)); + $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('#7f7f7f', (string) $actualColor); $this->assertEquals(10, $actualColor->getAlpha()); } @@ -792,17 +777,13 @@ private function getMultiLayeredImage() protected function processInOut($file, $in, $out) { $factory = $this->getLoader(); - $class = preg_replace('/\\\\/', "_", get_called_class()); + $class = preg_replace('/\\\\/', '_', get_called_class()); $image = $factory->open(FixturesLoader::getFixture($file.'.'.$in)); $thumb = $image->thumbnail(new Box(50, 50), ImageInterface::THUMBNAIL_OUTBOUND); - if (!is_dir(__DIR__.'/../results/in_out')) { - mkdir(__DIR__.'/../results/in_out', 0777, true); - } - $target = __DIR__."/../results/in_out/{$class}_{$file}_from_{$in}_to.{$out}"; + $target = $this->getTempDir()."/{$class}_{$file}_from_{$in}_to.{$out}"; $thumb->save($target); $this->assertFileExists($target); - unlink($target); } /** @@ -811,7 +792,7 @@ protected function processInOut($file, $in, $out) abstract protected function getLoader(); /** - * @return boolean + * @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 index 9461a0b34508b..225c2180ac460 100644 --- a/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php +++ b/src/Symfony/Component/Image/Tests/Image/AbstractLayersTest.php @@ -29,11 +29,11 @@ public function testMerge() 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); + ->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))); + $this->assertEquals('#ff0000', (string) $image->getColorAt(new Point(5, 5))); } public function testLayerArrayAccess() @@ -177,15 +177,13 @@ public function testAnimateEmpty() $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); - $target = __DIR__ . '/../results/temporary-gif.gif'; + $target = $this->getTempDir().'/temporary-gif.gif'; $image->save($target, array( 'animated' => true, )); $this->assertFileExists($target); - - @unlink($target); } /** @@ -199,7 +197,7 @@ public function testAnimateWithParameters($delay, $loops) $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); - $target = __DIR__ . '/../results/temporary-gif.gif'; + $target = $this->getTempDir().'/temporary-gif.gif'; $image->save($target, array( 'animated' => true, @@ -208,8 +206,6 @@ public function testAnimateWithParameters($delay, $loops) )); $this->assertFileExists($target); - - @unlink($target); } public function provideAnimationParameters() @@ -234,15 +230,13 @@ public function testAnimateWithWrongParameters($delay, $loops) $layers[] = $this->getImage(FixturesLoader::getFixture('yellow.gif')); $layers[] = $this->getImage(FixturesLoader::getFixture('blue.gif')); - $target = __DIR__ . '/../results/temporary-gif.gif'; + $target = $this->getTempDir().'/temporary-gif.gif'; $image->save($target, array( 'animated' => true, 'animated.delay' => $delay, 'animated.loops' => $loops, )); - - @unlink($target); } public function provideWrongAnimationParameters() @@ -279,5 +273,6 @@ 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 index 87c0222274b26..45f90a4b8d43c 100644 --- a/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php +++ b/src/Symfony/Component/Image/Tests/Image/AbstractLoaderTest.php @@ -27,8 +27,8 @@ abstract class AbstractLoaderTest extends TestCase public function testShouldCreateEmptyImage() { $factory = $this->getLoader(); - $image = $factory->create(new Box(50, 50)); - $size = $image->getSize(); + $image = $factory->create(new Box(50, 50)); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(50, $size->getWidth()); @@ -39,8 +39,8 @@ public function testShouldOpenAnImage() { $source = FixturesLoader::getFixture('google.png'); $factory = $this->getLoader(); - $image = $factory->open($source); - $size = $image->getSize(); + $image = $factory->open($source); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(364, $size->getWidth()); @@ -57,8 +57,8 @@ public function testShouldOpenAnSplFileResource() $source = FixturesLoader::getFixture('google.png'); $resource = new \SplFileInfo($source); $factory = $this->getLoader(); - $image = $factory->open($resource); - $size = $image->getSize(); + $image = $factory->open($resource); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(364, $size->getWidth()); @@ -89,8 +89,8 @@ public function testShouldFailOnInvalidImage() public function testShouldOpenAnHttpImage() { $factory = $this->getLoader(); - $image = $factory->open(self::HTTP_IMAGE); - $size = $image->getSize(); + $image = $factory->open(self::HTTP_IMAGE); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(240, $size->getWidth()); @@ -105,8 +105,8 @@ public function testShouldOpenAnHttpImage() public function testShouldCreateImageFromString() { $factory = $this->getLoader(); - $image = $factory->load(file_get_contents(FixturesLoader::getFixture('google.png'))); - $size = $image->getSize(); + $image = $factory->load(file_get_contents(FixturesLoader::getFixture('google.png'))); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(364, $size->getWidth()); @@ -123,8 +123,8 @@ public function testShouldCreateImageFromResource() $source = FixturesLoader::getFixture('google.png'); $factory = $this->getLoader(); $resource = fopen($source, 'r'); - $image = $factory->read($resource); - $size = $image->getSize(); + $image = $factory->read($resource); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(364, $size->getWidth()); @@ -140,8 +140,8 @@ public function testShouldCreateImageFromHttpResource() { $factory = $this->getLoader(); $resource = fopen(self::HTTP_IMAGE, 'r'); - $image = $factory->read($resource); - $size = $image->getSize(); + $image = $factory->read($resource); + $size = $image->getSize(); $this->assertInstanceOf(ImageInterface::class, $image); $this->assertEquals(240, $size->getWidth()); @@ -160,8 +160,8 @@ public function testShouldDetermineFontSize() } $palette = new RGB(); - $path = FixturesLoader::getFixture('font/Arial.ttf'); - $black = $palette->color('000'); + $path = FixturesLoader::getFixture('font/Arial.ttf'); + $black = $palette->color('000'); $factory = $this->getLoader(); $this->assertEquals($this->getEstimatedFontBox(), $factory->font($path, 36, $black)->box('string')); @@ -171,7 +171,7 @@ public function testCreateAlphaPrecision() { $loader = $this->getLoader(); $palette = new RGB(); - $image = $loader->create(new Box(1, 1), $palette->color("#f00", 17)); + $image = $loader->create(new Box(1, 1), $palette->color('#f00', 17)); $actualColor = $image->getColorAt(new Point(0, 0)); $this->assertEquals(17, $actualColor->getAlpha()); } diff --git a/src/Symfony/Component/Image/Tests/Image/BoxTest.php b/src/Symfony/Component/Image/Tests/Image/BoxTest.php index 29c1c1406f2ec..996e4a6d97c8d 100644 --- a/src/Symfony/Component/Image/Tests/Image/BoxTest.php +++ b/src/Symfony/Component/Image/Tests/Image/BoxTest.php @@ -25,8 +25,8 @@ class BoxTest extends TestCase * * @dataProvider getSizes * - * @param integer $width - * @param integer $height + * @param int $width + * @param int $height */ public function testShouldAssignWidthAndHeight($width, $height) { @@ -37,7 +37,7 @@ public function testShouldAssignWidthAndHeight($width, $height) } /** - * Data provider for testShouldAssignWidthAndHeight + * Data provider for testShouldAssignWidthAndHeight. * * @return array */ @@ -46,7 +46,7 @@ public function getSizes() return array( array(1, 1), array(10, 10), - array(15, 36) + array(15, 36), ); } @@ -57,8 +57,8 @@ public function getSizes() * * @dataProvider getInvalidSizes * - * @param integer $width - * @param integer $height + * @param int $width + * @param int $height */ public function testShouldThrowExceptionOnInvalidSize($width, $height) { @@ -66,7 +66,7 @@ public function testShouldThrowExceptionOnInvalidSize($width, $height) } /** - * Data provider for testShouldThrowExceptionOnInvalidSize + * Data provider for testShouldThrowExceptionOnInvalidSize. * * @return array */ @@ -76,7 +76,7 @@ public function getInvalidSizes() array(0, 0), array(15, 0), array(0, 25), - array(-1, 4) + array(-1, 4), ); } @@ -88,7 +88,7 @@ public function getInvalidSizes() * @param BoxInterface $size * @param BoxInterface $box * @param PointInterface $start - * @param Boolean $expected + * @param bool $expected */ public function testShouldDetermineIfASizeContainsABoxAtAStartPosition( BoxInterface $size, @@ -100,7 +100,7 @@ public function testShouldDetermineIfASizeContainsABoxAtAStartPosition( } /** - * Data provider for testShouldDetermineIfASizeContainsABoxAtAStartPosition + * Data provider for testShouldDetermineIfASizeContainsABoxAtAStartPosition. * * @return array */ @@ -140,9 +140,9 @@ public function testShouldIncreaseBox() /** * @dataProvider getSizesAndSquares * - * @param integer $width - * @param integer $height - * @param integer $square + * @param int $width + * @param int $height + * @param int $square */ public function testShouldCalculateSquare($width, $height, $square) { @@ -163,10 +163,10 @@ public function getSizesAndSquares() /** * @dataProvider getDimensionsAndTargets * - * @param integer $width - * @param integer $height - * @param integer $targetWidth - * @param integer $targetHeight + * @param int $width + * @param int $height + * @param int $targetWidth + * @param int $targetHeight */ public function testShouldResizeToTargetWidthAndHeight($width, $height, $targetWidth, $targetHeight) { diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php index 65669f9faa624..cdec4b8a40789 100644 --- a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/HorizontalTest.php @@ -7,6 +7,7 @@ * 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; @@ -16,7 +17,8 @@ class HorizontalTest extends LinearTest { /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd() */ protected function getEnd() @@ -25,7 +27,8 @@ protected function getEnd() } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart() */ protected function getStart() @@ -34,7 +37,8 @@ protected function getStart() } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask() */ protected function getFill(ColorInterface $start, ColorInterface $end) @@ -43,7 +47,8 @@ protected function getFill(ColorInterface $start, ColorInterface $end) } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades() */ public function getPointsAndColors() @@ -51,7 +56,7 @@ 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)) + 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 index cf85e74ac1490..b1ec5b527647d 100644 --- a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/LinearTest.php @@ -37,14 +37,14 @@ abstract class LinearTest extends TestCase protected function setUp() { $this->start = $this->getStart(); - $this->end = $this->getEnd(); - $this->fill = $this->getFill($this->start, $this->end); + $this->end = $this->getEnd(); + $this->fill = $this->getFill($this->start, $this->end); } /** * @dataProvider getPointsAndColors * - * @param integer $shade + * @param int $shade * @param \Symfony\Component\Image\Image\PointInterface $position */ public function testShouldProvideCorrectColorsValues(ColorInterface $color, PointInterface $position) diff --git a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php index fcb7fc1fb4f02..6b79c3672dedf 100644 --- a/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Fill/Gradient/VerticalTest.php @@ -7,6 +7,7 @@ * 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; @@ -16,7 +17,8 @@ class VerticalTest extends LinearTest { /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getEnd() */ protected function getEnd() @@ -25,7 +27,8 @@ protected function getEnd() } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getStart() */ protected function getStart() @@ -34,7 +37,8 @@ protected function getStart() } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getMask() */ protected function getFill(ColorInterface $start, ColorInterface $end) @@ -43,7 +47,8 @@ protected function getFill(ColorInterface $start, ColorInterface $end) } /** - * (non-PHPdoc) + * (non-PHPdoc). + * * @see Symfony\Component\Image\Image\Fill\Gradient\LinearTest::getPointsAndShades() */ public function getPointsAndColors() @@ -51,7 +56,7 @@ 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)) + 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 index 4c2b73b44a19c..d810c5814bc89 100644 --- a/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Histogram/BucketTest.php @@ -28,8 +28,8 @@ protected function setUp() /** * @dataProvider getCountAndValues * - * @param integer $count - * @param array $values + * @param int $count + * @param array $values */ public function testShouldOnlyRegisterValuesInRange($count, array $values) { diff --git a/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php index 552900b2b89aa..20a680d49d0d8 100644 --- a/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Histogram/RangeTest.php @@ -17,13 +17,13 @@ class RangeTest extends TestCase { private $start = 0; - private $end = 63; + private $end = 63; /** * @dataProvider getExpectedResultsAndValues * - * @param Boolean $contains - * @param integer $value + * @param bool $contains + * @param int $value */ public function testShouldDetermineIfContainsValue($contains, $value) { diff --git a/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php index f97b8d48f7861..ad4b541623d6f 100644 --- a/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php +++ b/src/Symfony/Component/Image/Tests/Image/Metadata/MetadataReaderTestCase.php @@ -16,8 +16,6 @@ use Symfony\Component\Image\Image\Metadata\MetadataReaderInterface; use Symfony\Component\Image\Tests\TestCase; -/** - */ abstract class MetadataReaderTestCase extends TestCase { /** diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php index 9b65b2821b556..8b6cb711130d2 100644 --- a/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Palette/AbstractPaletteTest.php @@ -63,7 +63,6 @@ public function testUseProfile() $palette->useProfile($new); $this->assertEquals($new, $palette->profile()); - } public function testProfile() diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php index 91daf77009713..e7c63f5299e81 100644 --- a/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Palette/CMYKTest.php @@ -32,7 +32,7 @@ public function provideColorAndAlphaTuples() public function provideColorAndAlpha() { return array( - array(array(4, 3, 2, 1), null) + array(array(4, 3, 2, 1), null), ); } diff --git a/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php index 38d7f492479e8..ef4c82f4daaad 100644 --- a/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/CMYKTest.php @@ -52,7 +52,7 @@ public function provideGrayscaleData() public function provideColorAndAlphaTuples() { return array( - array(null, $this->getColor()) + array(null, $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 index efb5d7739046f..27584ed792d7c 100644 --- a/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/GrayTest.php @@ -25,6 +25,7 @@ public function provideOpaqueColors() array(new Gray(new Grayscale(), array(255), 100)), ); } + public function provideNotOpaqueColors() { return array( @@ -45,7 +46,7 @@ public function provideGrayscaleData() public function provideColorAndAlphaTuples() { return array( - array(14, $this->getColor()) + array(14, $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 index de7fa13f28f96..a22bfd19590fc 100644 --- a/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Palette/Color/RGBTest.php @@ -25,6 +25,7 @@ public function provideOpaqueColors() array(new RGB(new RGBPalette(), array(255, 255, 255), 100)), ); } + public function provideNotOpaqueColors() { return array( @@ -45,7 +46,7 @@ public function provideGrayscaleData() public function provideColorAndAlphaTuples() { return array( - array(14, $this->getColor()) + array(14, $this->getColor()), ); } diff --git a/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php index 5ca41d5f00b98..7eb573777c162 100644 --- a/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php +++ b/src/Symfony/Component/Image/Tests/Image/Point/CenterTest.php @@ -38,7 +38,7 @@ public function testShouldGetCenterCoordinates(BoxInterface $box, PointInterface } /** - * Data provider for testShouldGetCenterCoordinates + * Data provider for testShouldGetCenterCoordinates. * * @return array */ @@ -59,9 +59,9 @@ public function getSizesAndCoordinates() * @dataProvider getMoves * * @param \Symfony\Component\Image\Image\BoxInterface $box - * @param integer $move - * @param integer $x1 - * @param integer $y1 + * @param int $move + * @param int $x1 + * @param int $y1 */ public function testShouldMoveByGivenAmount(BoxInterface $box, $move, $x1, $y1) { diff --git a/src/Symfony/Component/Image/Tests/Image/PointTest.php b/src/Symfony/Component/Image/Tests/Image/PointTest.php index 1afdc03503960..6ffc159728303 100644 --- a/src/Symfony/Component/Image/Tests/Image/PointTest.php +++ b/src/Symfony/Component/Image/Tests/Image/PointTest.php @@ -25,10 +25,10 @@ class PointTest extends TestCase * * @dataProvider getCoordinates * - * @param integer $x - * @param integer $y + * @param int $x + * @param int $y * @param BoxInterface $box - * @param Boolean $expected + * @param bool $expected */ public function testShouldAssignXYCoordinates($x, $y, BoxInterface $box, $expected) { @@ -41,7 +41,7 @@ public function testShouldAssignXYCoordinates($x, $y, BoxInterface $box, $expect } /** - * Data provider for testShouldAssignXYCoordinates + * Data provider for testShouldAssignXYCoordinates. * * @return array */ @@ -63,8 +63,8 @@ public function getCoordinates() * * @dataProvider getInvalidCoordinates * - * @param integer $x - * @param integer $y + * @param int $x + * @param int $y */ public function testShouldThrowExceptionOnInvalidCoordinates($x, $y) { @@ -72,7 +72,7 @@ public function testShouldThrowExceptionOnInvalidCoordinates($x, $y) } /** - * Data provider for testShouldThrowExceptionOnInvalidCoordinates + * Data provider for testShouldThrowExceptionOnInvalidCoordinates. * * @return array */ @@ -80,7 +80,7 @@ public function getInvalidCoordinates() { return array( array(-1, 0), - array(0, -1) + array(0, -1), ); } @@ -91,11 +91,11 @@ public function getInvalidCoordinates() * * @dataProvider getMoves * - * @param integer $x - * @param integer $y - * @param integer $move - * @param integer $x1 - * @param integer $y1 + * @param int $x + * @param int $y + * @param int $move + * @param int $x1 + * @param int $y1 */ public function testShouldMoveByGivenAmount($x, $y, $move, $x1, $y1) { diff --git a/src/Symfony/Component/Image/Tests/Image/ProfileTest.php b/src/Symfony/Component/Image/Tests/Image/ProfileTest.php index df27f490968e3..a1b6c9bf7d13a 100644 --- a/src/Symfony/Component/Image/Tests/Image/ProfileTest.php +++ b/src/Symfony/Component/Image/Tests/Image/ProfileTest.php @@ -43,7 +43,7 @@ public function testFromPath() */ public function testFromInvalidPath() { - $file = __DIR__ . '/non-existent-profile.icc'; + $file = __DIR__.'/non-existent-profile.icc'; Profile::fromPath($file); } } diff --git a/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php index 7c36584c1401e..4f41b9436c960 100644 --- a/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php +++ b/src/Symfony/Component/Image/Tests/Imagick/ImageTest.php @@ -58,7 +58,7 @@ public function testImageResizeUsesProperMethodBasedOnInputAndOutputSizes() $image ->resize(new Box(1500, 750)) - ->save(__DIR__.'/../results/large.png') + ->save($this->getTempDir().'/large.png') ; $this->assertSame(1500, $image->getSize()->getWidth()); @@ -66,14 +66,11 @@ public function testImageResizeUsesProperMethodBasedOnInputAndOutputSizes() $image ->resize(new Box(100, 50)) - ->save(__DIR__.'/../results/small.png') + ->save($this->getTempDir().'/small.png') ; $this->assertSame(100, $image->getSize()->getWidth()); $this->assertSame(50, $image->getSize()->getHeight()); - - unlink(__DIR__.'/../results/large.png'); - unlink(__DIR__.'/../results/small.png'); } public function testAnimatedGifResize() @@ -82,13 +79,12 @@ public function testAnimatedGifResize() $image = $loader->open(FixturesLoader::getFixture('anima3.gif')); $image ->resize(new Box(150, 100)) - ->save(__DIR__.'/../results/anima3-150x100-actual.gif', array('animated' => true)) + ->save($this->getTempDir().'/anima3-150x100-actual.gif', array('animated' => true)) ; $this->assertImageEquals( $loader->open(FixturesLoader::getFixture('resize/anima3-150x100.gif')), - $loader->open(__DIR__.'/../results/anima3-150x100-actual.gif') + $loader->open($this->getTempDir().'/anima3-150x100-actual.gif') ); - unlink(__DIR__.'/../results/anima3-150x100-actual.gif'); } // Older imagemagick versions does not support colorspace conversion @@ -133,16 +129,14 @@ public function testAnimatedGifCrop() new Point(0, 0), new Box(150, 100) ) - ->save(__DIR__.'/../results/anima3-topleft-actual.gif', array('animated' => true)) + ->save($this->getTempDir().'/anima3-topleft-actual.gif', array('animated' => true)) ; $this->assertImageEquals( $loader->open(FixturesLoader::getFixture('crop/anima3-topleft.gif')), - $loader->open(__DIR__.'/../results/anima3-topleft-actual.gif') + $loader->open($this->getTempDir().'/anima3-topleft-actual.gif') ); - unlink(__DIR__.'/../results/anima3-topleft-actual.gif'); } - protected function supportMultipleLayers() { return true; diff --git a/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php index afad59e7a7ec4..0b1ce8e483844 100644 --- a/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php +++ b/src/Symfony/Component/Image/Tests/Imagick/LayersTest.php @@ -79,10 +79,10 @@ public function testCoalesce() $width = null; $height = null; - $resource = new \Imagick; + $resource = new \Imagick(); $palette = new RGB(); - $resource->newImage(20, 10, new \ImagickPixel("black")); - $resource->newImage(10, 10, new \ImagickPixel("black")); + $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(); diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php b/src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php similarity index 74% rename from src/Symfony/Component/Image/Tests/Issues/Issue67Test.php rename to src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php index 3ee1851c9f9a2..c79f832551f59 100644 --- a/src/Symfony/Component/Image/Tests/Issues/Issue67Test.php +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionErrorTest.php @@ -1,13 +1,13 @@ getLoader(); $loader->open(FixturesLoader::getFixture('large.jpg')) - ->save($invalidPath . '/myfile.jpg'); + ->save($invalidPath.'/myfile.jpg'); } } diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php b/src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php similarity index 83% rename from src/Symfony/Component/Image/Tests/Issues/Issue59Test.php rename to src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php index de8db5520b78c..4ffb979ca678d 100644 --- a/src/Symfony/Component/Image/Tests/Issues/Issue59Test.php +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionGIFTest.php @@ -1,13 +1,13 @@ getLoader(); - $new = sys_get_temp_dir()."/sample.jpeg"; + $new = sys_get_temp_dir().'/sample.jpeg'; $image = $loader ->open(FixturesLoader::getFixture('sample.gif')) @@ -32,7 +32,5 @@ public function testShouldSaveGifImageWithMoreThan256TransparentPixels() $this->assertSame(700, $image->getSize()->getWidth()); $this->assertSame(440, $image->getSize()->getHeight()); - - unlink($new); } } diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php b/src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php similarity index 68% rename from src/Symfony/Component/Image/Tests/Issues/Issue17Test.php rename to src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php index 2ebefd94fc771..7873169b49c68 100644 --- a/src/Symfony/Component/Image/Tests/Issues/Issue17Test.php +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionResizeTest.php @@ -1,6 +1,6 @@ getLoader(); $loader->open(FixturesLoader::getFixture('large.jpg')) ->thumbnail($size, ImageInterface::THUMBNAIL_OUTBOUND) - ->save(__DIR__.'/../results/resized.jpg'); + ->save($this->getTempDir().'/resized.jpg'); - $this->assertTrue(file_exists(__DIR__.'/../results/resized.jpg')); + $this->assertFileExists($this->getTempDir().'/resized.jpg'); $this->assertEquals( $size, - $loader->open(__DIR__.'/../results/resized.jpg')->getSize() + $loader->open($this->getTempDir().'/resized.jpg')->getSize() ); - - unlink(__DIR__.'/../results/resized.jpg'); } } diff --git a/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php b/src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php similarity index 88% rename from src/Symfony/Component/Image/Tests/Issues/Issue131Test.php rename to src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php index 80628d4a68400..46d0e07c3adbb 100644 --- a/src/Symfony/Component/Image/Tests/Issues/Issue131Test.php +++ b/src/Symfony/Component/Image/Tests/Regression/RegressionSaveTest.php @@ -1,6 +1,6 @@ getTemporaryDir()); - $targetFile = $dir . '/myfile.png'; + $targetFile = $dir.'/myfile.png'; $loader = $this->getImagickLoader(FixturesLoader::getFixture('multi-layer.psd')); $loader->save($targetFile); - if ( ! $this->probeOneFileAndCleanup($dir, $targetFile)) { + if (!$this->probeOneFileAndCleanup($dir, $targetFile)) { $this->fail('Imagick failed to generate one file'); } } @@ -75,13 +74,13 @@ public function testShouldSaveOneFileWithImagick() public function testShouldSaveOneFileWithGmagick() { $dir = realpath($this->getTemporaryDir()); - $targetFile = $dir . '/myfile.png'; + $targetFile = $dir.'/myfile.png'; $loader = $this->getGmagickLoader(FixturesLoader::getFixture('multi-layer.psd')); $loader->save($targetFile); - if ( ! $this->probeOneFileAndCleanup($dir, $targetFile)) { + if (!$this->probeOneFileAndCleanup($dir, $targetFile)) { $this->fail('Gmagick failed to generate one file'); } } diff --git a/src/Symfony/Component/Image/Tests/TestCase.php b/src/Symfony/Component/Image/Tests/TestCase.php index ae7d33d5213d3..5cf653ed11b62 100644 --- a/src/Symfony/Component/Image/Tests/TestCase.php +++ b/src/Symfony/Component/Image/Tests/TestCase.php @@ -12,22 +12,45 @@ 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 + * Asserts that two images are equal using color histogram comparison method. * * @param ImageInterface $expected * @param ImageInterface $actual * @param string $message * @param float $delta - * @param integer $buckets + * @param int $buckets */ public static function assertImageEquals($expected, $actual, $message = '', $delta = 0.1, $buckets = 4) { @@ -52,7 +75,7 @@ public function setExpectedException($exception, $message = null, $code = null) } /** - * Actually it's not possible on some HHVM versions + * Actually it's not possible on some HHVM versions. */ protected function supportsMockingImagick() { 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:

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

Alternative Proxy