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 zn&<7HExe<&iGPJQ3f`sl!pF2u_%lsQ;c3HoR?C{_u-KJ+kKNkW*quB8?}!=ny@==* zR|!2Xx^!O30)C4l9ToY|K~X5}lZmB0GAXoMCX05;Ueij$nq38#XC7iY>#nGHn3T;rzqFJQ^cr2wftyE2Glp1NZ zawknF57L~y>Q}i-J*r<*m)d`*Q=O#_(Egl%U>EiocI$7O!G8ef zjd8B856fSZPlr?$XhB^Yo{Jf6)?7&&G(BjYW&k{v2%6H2r%BCJ8rRCERa%8KqE${q zTD3Hw)k1yRz0{*UMqS$LsY7QswdvrP0K>aU_X}#!`-!R3W0~5Q_y&3(;E;dY7Kb-* z%nFBE>{b^rmYsUCv_)T))*Bemw4o)u7DxCjUhr9hX~-~&1`HFZ&oGU840EW#jVe%PMX7X=NsR_#|d5O;v``wlBA6yW> zGwNO(Zg<$>^aEp=TP~y-GeugxT$@&z!+(Gm({JudJ?6gDWgbfH=F!w@o2VRBK*K)#j~CrA0qeZn2tEYO$45Y;lBJXz?01-|`b~p5+hR97}eQY-}Ywz#;8* z=)dBLQxUMk9p||CdDa2fd~G>2VJ)U%8#U^)F`zD63u?zbN-ee?)MOJt^)}&DYZFJ+ zHYrqTlTGC|MNFwpB~xtE#3{7t;pEwlb8~Dr@v>|W@-l3%^3rWT=BL^Ilb34C@={*n zeS54!XrJ-JsSenJL)$tp)H`mdeUXPNrigH{EfRm+H8IpX|7gpXhj55byY*AkOjc{5Z#F z{8&f!PwpY+tG=lHgE0R%#BTtr{h^O>48o)8b{9|^o*mriu0gf#hE(ZpNoDT#RO0SV zh3@{8?;b|E?y;2Zo4^PY|W~L{h3xA|?A|GKoHgoH(BAK^VM2=m@84DmWI4EB0g z807U-5ajhcU?uQ_JX!v2=$wc4{s^oCFcps4KMXY=#?pbYHGrx>5tRliQemJbd=(Kzh}<58$JA|?I{YA%eW8kB|!s32?^WurGI zJxrfc!tmV_VYU<>=1j3+-V_}cOp#&H6dsnsgoWiW!J(y`z|aPse@GACH)OTIJ7l}S zGx&_qBls?OBJc?M4X^@_z$LCC-s3U&Jj7xizzFC9&G=m%##4rI_I1eOae+0%r4`>2apeP0UsnAbD?VHA@$aFDBBJXrZ@=Eh0kF+3iPm3nkv=nkq%VQiIZF)l!39T)~3pbnIRoJ{CvqwdVXvll>6F1~*QUW_-q7>`^7a?4## zF1fbkoa;)Cxqjr36HY6063H$nn{2Yn$SS*;vCJCeSY)o}EYCc|G0V8gF-!k~W0v+a z*DRI&lOu?A8?>eX+7_r8RDr@Q^!-COwh%Q}5zhIG;Xjn{$gLF5zbsWIM?62+z6Ab5 z3H*l=d$KO^B&*^evMi1vi=uQ|UQ|S;MfGG-*hfYMGmK&We#RjGI%AOch%w0hkuk_& z8G~$=BVix(=5o<{06IZECYjS&H=*`zLH|RmG#RzYlR>K*>9^{VZi^}D zwAheViwkKs`;$g9a6$Q zLW;dMB;V^sa=pPM?nxlo?mQB8)sYm6YGKEE;?QaQ%B6*S{x)Br_>T(i6%g9M>kn zxC!ycY>7MONz5vI=vU!GHHyiU+-Nj3fn@K(hyN0Gg7>lbo`RpiFSNQ2#|3CMBIYu@ z&OyGDTCongunzmse*s;GQC$DWTNqE_`u|#7|DQqqIg9#pJ^C*;N?`{p2ULJIFadVJ z8z%tR$*s%AWaG`J@n&l`zz9By55Vg^ty1jX<-b5%-`pBL~QLI%19;fDZ^= zFX%e0!5pl^8o)o---P;iE9&oUsK0lh{@scCcNYVL730_`21-B^KO4i_umwJN9f#lL zV@C|3?PK^5;va51hXdqW*fBl?U*J2juusA~m-iqCgYbaxpM@h{Kg8(@P1||Q0kjMj zQ2+0P|8M~Q!y(lFM^OJCBb@v_=O`D3uQYy^$If33eJKXGAYlW10d_(svG7hn>^Oot zwilcSx4~PB9!%{p_KBF^bmTc^Bi12wJ$B=Bupb^2w2V%m2j?`_z*($;3-CWK!vDAo z?JMBLR`MECO_wp2t1>t_L{6^jzzbOcFpgVE0E_)rH#`yu-N4$oaU3U5XQ&Zzm*ZDC z&`#Wrb%-H)AI2P<#ODwxGlrhdRp?*GJt#Mze+&AUP866++<9sH#`D)mGaiJ1-V-_8 zf1?3U!W{Sj8~{Em#}ep{D{J zxqHa}yU73hc>fQ_ZeP)37%kn{(<%SqY0jo>H*?G5q@p}SolBo zq1u!5Ur1_x36g%07d;qWcpU~3(F2l&DzOOla3yN;dUz2na1*-VfAph1A3-HM0e|4d zb3wPkm%#UupepkxGrO>Fx zyEZ_h6&hX8=!eD#G$!!AGpLt0qh4RYk~t2q;0i`@4_?7nsNH`=1Lfc#ggxU z-iJm1VHt9+hCCa>gRsKCaz%Ur(2RnUk^~@?c zUq+=SDsVneeF2IX9gE=_?f9xoFl@7t1I)HKP#W?n09J?`&ofyXsjAI)c(a2~9Hppb5@zG|oW}1{mYASbxwLVb_7({XJRK-`MqE zXGGZLDI#Vq#A=Fu7CZE^;2scO0PWyK&^CTNZQ-ZTW_}jU@$+eepoC@xm9$P!Pt(FS zS|jYE)xt5Fkea12shu<`b%KVaZ_uFV0riXiPQ5a}Q;#f5-Ou>}+FvNce^JM76TA6W z@cJ}%`KP31>4=mv`dakhx0u6gaiGm&PuhU{gJ#8%@L1yEv82+JToyc*0veYqrB!m( zG$PkTL(95oVA%-u$xl;{{C4V+KSmu2uTh)AC)A?&JvHGA#~VQd?hlgiEp}la>0vi+ zfO-oY(?!jt35#EaLkqZ9YF<$tJuOD?TCC8|;zX0m-ZZWp438y>hVi_XL6uZ^EZNkn zQb;{2<Cf@Q_CeD)m zle^Hn> zdDbD6V;x1=Hi?vJlS%0|1(a%2Ny)a&OrmW+6K}hQiM8F%iMBn>iL$-JiM0Ki6Jh%c zC)}3(lQ*2O7Cqs?cwzp*Bwi2U5ZmX1b3FX5*?~iK4zg6~piHF>I#lFfLir9>l;hw? zSq>hQ?hrt!4&jvI5JyQ4X_VlYM{$nj6yw;)L^<{_5ss6bFvodLsN)H4u;UxtAjhx3 z&zwLq*4sxg*gkQ>0Ei~w0m4p5@_04<6M zFr-L-3kvtQqfmcW3ikJn3$!W>^%iIX)%aa0#*&9|rQ>x{s5Hff%2Q;h8ij@G zQE;dk1%_Iaf2b4rg?f=sXb^daMw4e~3VDR)l3Qp6xrDYbP9Z~#W5{}reaJ!1%HZqZ zA!lXKzc?!c**`gp_un0XG9(i70NOzvC<6ufeFnBkQ8?d@;=^+hQ!u=k0C+Ke(MII` z|G0V&@VbiYdwZ#OAzk&}d+)vX-g}oV$&#DgJMIv-a%SGiUa?bLKR~sjw+k`AvDsZ7NevQ@yg#VltbC zl-@X{w8phcY20O&)NsNqvEf#;g!&8ML$idszndr2&di}^#{2;|&NeX~H4}@#(14bK zaWL2lKU2_VP}|P>gARL@pv4q+cq^|XSUDY0%IZi`W=EFNJBpOnQLU7YRwcLhE3th- z3GJ&C*S1};ZO6=FT5mRsZh6Wqs^xvNsOE3XqMH6|7S%X2i@V`D4K9J_Vqj=MlVAkS z?r!*d*!!WE-#kHKpOtd^(I5KVmEO<#hd%U&zE~yor7E#6Pw{=_itB4s3|dTd??OfP zPAj74Vukk{R%rJP3h8fLq#X?c+}c4}Ncyn{YygG^GzJDhJG`}nf(iunA0qaWDXPJN2%Izifi_7gOZkOUQkfp+8KM zd#<#UKYO+KtTM@am5)4E2Fqh*wA@#w$h0C~t}Ck1IXdJ#Ju1iLE99_jhwPW0knNH? zWV86!vYq-^c9Y-AZqd(RM#o1O6Tn)q7>t4*xSOeW)#RK8lnHOrDsta7o3-G|H&qDzl+fywxK`lK!4c9`v2Y5 zvfkq;v)v}N4IdB!VyRRnC<6rh4l1>MCEyENPZ9X{LHH6Eqd(yMufadG+c1;*E(N2D z=?nO~iLX?yB@Qt^1Kxz~-2bqf^%r~LKY;#l5dGm0`om!}g2@Iw!wHxG6T-tG06Pw6 z0S3Us1h;YHnX&1~!NWBCO=vSuF)?@>d>W7r_OsvtCT`DwSF{=zE!v6? zXs%ZNTupro;Z47aet0c00GI!5V-dv*iam7c>zc z^pHsO5bQAS%ek-+P}!$1WdeJa$rqv=s3C{PCCbU*3O#_g})6S81O&Jvvr|8wxa2>&bSFRxMd8{jwK?Rn6oB*SyBe>;C} zy!Qe42z+AA{valFkWlW6!5@Faj&Bx#m4L?oW*1qXk?;TSJs@GcP6TRSU*` z9HV6JldK+CPR6(fJz^tS^u^?eJIT@a@oV}aGTP(l5~s=6uSb`-i`?}w^2e9ZB|ajD z{)WtZPWv$Qk9Wb_U~UbDvG&5y9t;>-OdyDXI|J?#c#J%O(Ij5PlHa2M7*EfcyZ7V?js_kFhVC#!v(3mF?Z-*q;fpW0Kj|}k@dwuC z{7%Ji*22{aPY)c!aEzg9Oc9pL;aCL+`(a?m)7bG8c07sx@Hjd5IrN7|PzN5uf(P)* zefZ)Yd~r8ycY!irKl~jnhNo=fTOWKGM*WO67&+9U46b@gYSVdQ;|cun813>X z@$m?G|3l>c528QZj}CG#cHF}{jJwG8@1RX?)9KHUEQOZ1_9A z_*|Cw+L8LYQ_leE7e&2Ns8c?rRKU{!Pdgkva16r1eIV!$x3XU7X01SJSVzsbphfJ0 z=O9|dNwkRT(IW2S>{qnVdql0F;QfS!tiu3DfnkHe|0#GEyeb>IKlgv2OFg1sIAh^V zhp!N>N_ZOJX@jQ+onR|9owqfXs$ z4Zt-D*92Tksr@P)Fx#YkW;?aV{E&8;U#T4yw`!}!Ic>3gRhz9o(I)Hfw9)#%+F(7S z^)@s5ilNL{*7gF!$m8HXhS)nD8OE(0bfdXRXD$3$&l0JV*6e|4ldYq+#X4kLrGvJO zI$+zTeRe(CV>hVXb_=xAZc^Lrr?u67ownF-(V<288C#p8oSrWfEy0@Iv0reVClHIgY;sEyW#xM&qx%rsiel2G)A z&}2=7Wos;~NDIO$H4;{@p|Ca$g!QT~d_+CrQ|dyC>4?~>wupmjjkrq9k$0;x@>w-R zex&-yZ&epL^Z)Pyybq-Cb5AO{e;W1YxHH+%KFGbXWlglXRzx{!NtC-5p~Z|vhif!C zUc=F88i>wSUv!CjqN~-#zA+s!U22aRR4ZCcbLv)HKAP1iS=qsY*#~Kzv`04RFkw!)kzywnY>r!$tP9DqTG_?r&XN%KKPr8 zlKu<0OB9@k^Y(1ofhop8z|WJ~z-4|lb}UbaA6pitS!pQMS$(P8`;Z!-_SA5-rpBo$ zHB}9%IjT!7QB7Kns?%Cjncky{^ih?Okd&scS1}923)4@iApKV5r=M3|`g_Vv|EqG- z{$rM#HZzOI;JZ1GSS(cBM+Q8irJ|f+73NGSKWDA-a&{>v=eV+&oMq)asm$!(DkJ-AWn}$q zmXS3xi-+O6zKFh9Olkl&f)!w@khwOt4e|FL>}oHtRa1eB>IytnUBKRt1>q{=Ua69T zG!+%(s<5C;`Gs}LD{NP8;h?e$7b&Z7wK58KD82BQ(h6@>O2OmcZ6)V_1%6g?9(V2L z&6wQ}-!-MA0A;iTSPQs~LF2qP!ryvJ;OD%t)Dq3bNtI=8DlPL>aao88%A%E5maN>e zY-N`fE32$pnPsg?FY8lU*|<{5Rw}t{tCGr&D53Ov#h0E_e93RXmx?d`H((dHBCg}` zovxriRMHM$C71-GpdYkX!4Imd$-k@Z(OO*5S-j9$0?}D`{zXl^(reO{R+F!knhGVO z#U$4BD4}M7;%k;Gwt9>#IH9w+E3L^_DNXGE&=jM@req~FUR1)e5P9P{DPtfj=vx_9unZa0fHE0iL6Eq=j&;0fq)N0(w9T zDM58Bzc;q?n=8obFjHEmy^^>`DWTI-ah(B*=?qs?XS^ai(-qNCps*U|IPkwEu<=c9nd|FseT6gm*B;DIOw4WK{tXUo68RDS)9^6Br9ci*@? z`&P@NcbD9IPRgzOZkf7Xkg4+%xpjOGxU;*R-*e#E)5e$tjCDYxpa-;ckaxmg(o4=W zK+Z8p?mJB0GfLjE&_)4cPVyUbm(Q55ycdSZb771;7P1G$f_%9xsFv%3cDW!VIFBxu z)5umi4j+>}n=0B5UXcC3hjQrun;iQ7CC5JIQk|>?0xRL0;5f*!12jbVgt1q(ngXrbQKa2La!Jx1;eZ!Emw%g`UD z$-P#hKdiQp;~G2Jthq>*YdmDJ+Fxd?$>~-l00v$|WL=3vS5n9dzA(Luf$tPSe=lS& z;`EOh1iuD9Xa_!C4HkhR(1E{aHK6f14Jd6XF#vDGDss=Y@UJKT-$?$wnf#xRh;--`+7l5#Fc`=nruEY=wUZ{5#3NcawkbF=HW%H35Ykd$41#2k>Xm$Buo; zfB|?f!M>M@?V+&Ud%zh66X%b*huQa}lyW1oQJIKH@yFI|Q& zjy=WqUk4v)nmUi+@7@(?Kx>FYK9;wQeh6+bAnH`AP!`LQgnz0vO}K9N?!LZ z`L^*~l)s`se66))jvL9*x1ta1LYLT27I&C@{c^O4tI64K(Z^({AK{P>$s#{sMZxT|=I;G4?g26OpqN{OVLX@RXL8+tvhLtpRm0f=R}VbHaE!yT7>;Q; z*1)k5eZaUMWhdqDhwTV1IEhwpE!pKAxa)Cp*jI7iCuH6~;K~{D>A8DRjQdfZ1%~c0 zw+6z{L0tF^-V5y|gnMA(;LW6NCGge3)ecV|93yay!?BopETKCj@FPk8kMdGyRI zZsBO)7aM-WmM_TrKPEPQ%NmT=iHn!W`=2N8{}p-vdGhTiSd(%NJ08J~hiI1v&_Vup z9mbvL54YowTk*%uWZ*Xv8`q-}T+64fp_DU3#8vp>G*NaX%%|pI*ziyACHS4p@h|s( zpwnGImwKGM{}J;32bD!B#b`Ly)TN2Kv}1oSTErmUUI5F4&Z0kDgAQ^AJ5CcHr%)N# zg#s<&D)fQd@XF&v%Im0RpJCk(=n<^H00+Ro=lMV2Xmqh(p-(-AF7+T<%-v`)x6>xK zppV@s?g1e-uECBo_~SHooFYz7qCcEKe>jdE$FSoFI>;g7<5Kq0Il#Yt_+k&1?xMsU zwB&aEo}Tm#_!&!$r%=!EYVNkTm(3Y+%;x0ZdsvThEB9;NKzyF1O|D}9;8XOI%eg=3 zGVC~t9fz^wQgo0@)D6=BT%&MJz_k>v)o^WwXAdoNOq(cW15vS#NL@pFtfrr?qQ|c^ zo0-Mu3}wa|l=DvLAK(_wujjq9=JwRpm3sP6N+_JM@TF^9#T5Y{kE3FS{+UAUw+1#Ziwih&I`>rPKSnXx^lP2tE zG!BgWfSz+>cxPz88JuxPe_(jM%-Kwr**WNtwMqMJ{IuIPTs!RI(OA;aSn|Vc})B$9Z*nysHk6 zuhs7PAGNVJi-DH{&|reOjD}MGF!(v{=Jhs)_bu46+0#}V++DTW!$;GeAzJDgqs5-d zn)J%jxL2VTdR1uDt4_mSEgJOhR=@XWqtqRm zsLs%IwTI@ZHMCUCVKr(DYf*hzk806kYQh(*Dq^iFB6g@O;;2d^ZcuULqbiDcLxqul zR$;_XU}hHQ;J-0}I7njJmyGYhYR*|Yt||UDjxD26RvL_SQZHIeXOzF%qQcY?6{Du8 zWYtGysWz%mHPMx-if&K^_bHXf45>6`k&0tisVH`v3Sy5aFZMd+#c$SP!Pb6z`4mw-IdVk2h0SyuI4vUDX`#t%mp@)y7AvIzC<% z@o6fH&r@kasfrV7Rg~DK0<@U?#Bt>&tx!(V#meI5^vtBQ%1C-h>4~o@E%9^kqtX&) zW^pfEXVS^{GKqsMd;pekK9)iKQ{m_DouDs!ieEhZKL6r-Z5`B}PRlNh(Om zQhsWY@=~jngBFvW)~n34g~~`Sy2wC8lu4EVu*y zQ}FJ~Aq~jI2VfEBBiZm{OBZMX^_iBc&T>?FmPy4~J}S%#QeIZ1a(YEX73RArFW*zy`Toi*2vd4NtWpb7&{uMlTu`dSf;uG> zbSS=HSg{3*6;rT5(FOYy$qn-n`S*Yq6rT4-MdbbiuzEL#+@JD~z`MPOb^w#btaAoE zoVS5`&MQmEIY54?HF}GaGSFgD%Y4vSf|XbnrG&CX#g%0$wya1oXfaV}F_C2hiYS{@ zSlL>ImhMqV=@kktxl2JMzgAH39~4ydor2L|f(!XA1D=DFzonFRC?gKQ0ARZlHG;}Y z=DJnn93ZpWOsTAkNvw52W8wab8uovv2~$)}tRiYs6<(94FtnHuw3uKPZUohgE1-I{ z{Hu4$uj+F7R^1_=%4g+M@q77{e=FZ|ez_`R9s&2>a@wJSScGo`^neyn3vWp+^Zf>L zZ;;%?oVywQp&9+58U3M|^$*QK3T}>6P;;UJnzQ8JT!O|?C!eM+c{eSPSJMi4G;Wi7 z!)0=7xK(cT7i6mYP;Rx~$gSo-az}?b4%d!q#={zNe=q<#Km**BOv8)d&T1j|ZiT;{ z+_Qt+ugg|}-Olpwc9(A#`UCs-dUeIfvnxd&UFZ*;Rp=b8a_t7S5w-#S_L?UP0C8JYJy zDD&=DWzqGCEIa=We$pOz);1H1U=VbGMo3Krv;n*^1Lz+^=no_0{|nF`#;oKx z;UL=ySJ{kv$!a`MmSd5!7)z4b7@d0|4qn(s#rVR4X$G4eH1y>J{9PoMzlQ81oc zAPS@sT!jFqF75@B47{7bVJdqQ1JHRI{#^#*KhX(TIS!Da{<+4Ai>p0w25vai2Lyu{z~Hx*fL}u}t)UXD`O@k=1l}36 znTM(PYv7Mun7-1AeoUdh{h$?pR}a${sdLr@8q{K9U^y|!*L~N(zYhM5@Nb5HixBw1 zR_xen2k^%>6TlzagF!6F0#&?6MYrO-t$gX?%NPjm0M8PL?=d)krp3cF6Ls#w)CRcA z@OAD|`T_Md*5HJ0AoqpSeH;8V>8$>;S9EBw9~>}_<{WKU=;g>@o3+51DG5Jr$6pJ56a^_}m z4*Ys{9ZnbiuBUxV@pJYzG$?ra2}l7)(LYYWe+vFH@SFwLfg9#=BgdP-Er23tkGFBW z16%I0=Z8Xk@_;{+MeKN}n2B>IAg~_T2B^pb6!gG78o=MJm{zr&>%l(y0d1Zwk8u7dFpgA>lxj{NxnRYlop^o87cC?Kl%rqta-`BP zL-;aeFP&ER4&ne`ms00!>YI2i{qR;|;2!!QTn?0J`2=O22hV_K!LPyd;D!G=zBK>( zMUF26!k%P#?g#AO5htJcu&F>IKPp!OI>4U@y3aQP>5A^=|;~YPa?~xg6KRJVuT_NjNTpV50 z{}&iGd6)C%6f%?i%9rO4{TkZE_iFm5ve{htQ@jVQtKQ?AS|u z?4iHxqTlXR7n(;uJ!2HjV*<@%89Zw#e=EJ_02UeExtU({7=L*KmE?0Q{ho-PnZ@tS z8FFmkw_~UU*D&0lVt6}2d>)}qE=3>PPe0j1Z0uAbTp5&{4_7H%)o?XXr&bud=^X<^ z!~(dc;93dKCQX^`(IT_s8aKZ|W9AQQfyFBtwWNnzeyd^2pEYcSR_cKMa1pYV4UKTf4>Kp zA}+Vby}6Wf+{x>$Zd~GBEVRYJK^yE$OjUff!hvUBa(~bg$7D@8W@(Wldr&%+Y0Rls z3wVyuh*PJAoCh?(JwkoXOVxAHI(542RJ+SjwYprVX4i++eR)aEgfz>YI7e^i~E$C+*hl?W1H$c4y(rVtg1X8P^IU~s_^_o<(}UI^asG> z=yYQcJ_uo25=z~Hp?ysA-V*GY#J)wI=no#~51y=l@C;I)XQX<(64d3DrVg)MwRxAQ zh5d7zyc^Zv)2TZ4&#m#9P?hgWRrp@4GQWc=@jIhpzx!0^_ks%i{s8^~W@hm~Fw=rC zra2Mh8IjZ<7~02FI8#pS8O4@ie=7~3#q^-Xbo%?L%|BEv{?TgkPf`QVl&%ZNS4}{f zssid%5!j}(z&@1(jj1?jxeECqEI;^wa)VDRC-@#^2S2Z@;E%y~U}hF~!*x1}IEcXq zvG^Y>=6o!gIUaTm@b}(OGj)d8vxbJf7ec&L9}=jVkZ`n}I8}tEs4O&FC85PC4y#fj z_bKJ0#pH#LDkprYvLZGrGh(08BTgwTg3&JGIi*B=1in>DIBO5UZSY?X@1A%xAf_3H z{xS}RdEJjK-Tb{BzcfYKu$IPI)sgP1i1bxyWC+?$6uM4=@*~rg7nQG^sB&ea#bicz zDkElCX)%kH8na%>+?<{ibA=LP?o>j|vx<-To#Lba4*m;fbQAo?;M|c!93j zQ{cyz8vb4mij!@XpX{uhWOrpI`zk#-M5!rJN=`{cPsv1EDO3VlOk8S8P|!lTtFhl$aT)gv@aCl{oa3G(~6SDGDtnGP_CP*}V$mhw9L*l?ut) zuHdX=3e3D2oL4}`dkW0>s{+&i4aoiB*aP3XY}z4*H~`(C8PtMuP?*ba|9RwHd1gvM zi%BTBNO1-1KUv_XDE1JJD2P^gL9)UMvK3NLqTqsh1r>BDpkRUg3)q9QV5@xdkI07` z*S+(elvnO=<(>PLd~*H;$o;d31vpm0F_BL{0PUaw82V3f0dw7Aa_DYwF(z>E&VZLCLHPKvsqXifua=2!#D;Le9Ty#oG9_^Zf0Ys^?jg8op4{!oYh zP>23dhyK8xF`l)_@~F*4-zb-4+UhTt zmM}TDum@&K2Kq*^9MEFyn!9D&JSLl_)v|8fEvtr8vaG*H7WFU4vhHJ9)&3RyID4Cf zu~cCUu)WHx285)fGS2hi&VW0yf!w#5+^-e=p`F~PliZ`*T28%=vhOj;uE$F@-GQ?1 zj+9k*68c7t%)2P0i!Y$XsB;;W+e#x}hR}8!9gf*b%Lm{K0`EI*hG%IFu?RXr1E>T= zb;JSODR9TOk@vtG*iHW5Oa9qU{x@hT+hKcI40E9$atA&j1jGP3+8_=d#Bs(3GXskl za5jL0bh_)Q?Bg{0+XVlg0Qc7Nu?e{PKr839pcH?z9h1_ib9@&u0B`UB`9D?Sk|^f| zkk((!i_bf*~vALK&tDi!|(e68hh zje;)f+XO24ilG5zQs=}zVgTNdQTWH;pMZZ7{>9{fOT>U;32Xs&EO!OiG3^hy$V?}K zJl>-*ms5#lQ(y~Dr#-kgNQ<8%2tNdW)*|=@sBb%{#or~kE(c$y43qmVBnIL2gVSRf z{L}DL9gEfQuL0`}SOEO7-hm4a{@92e8}Y{`?AVkHXp>D%yoWC~uHrNM`RuiP=P`7e zHz`*7kO#GAb*_}pE5{#Srq1mJDQ-?jL=m^x?TNa`AJmNDoS^bfc!jPf4@kAla+li)mf8ay?Byuk5Sz&Jh& zehpp(uYxzhyTr*y#Om*3q>oG3+<*@M2kiJ5r+>VYOvhm!h6ZBX6JzWN5)KkUF8O#l z8b~8qQ72jA02;`szUE$oFUgkvLiY40vYXGy;65Qp|3DV`dxGV6G~5R?)O%#xzs2~s zQ6YXqM*Rl)ouPdgYd?(pPmKLQjr}m@bcbK|!7%Lbgg1y!$Do0vqeT>w9af{FG*e2K zKE{sUVaNOAbnlW$y+c;@HXZdh*zpE-yoMdGlAXOwhW#R$@$)p+b7YdnGf^&314H+C zY97YEpvGE^8C*37bGRL-uZcAvzUVyRWQB=n5!uwK6rNf*S}4DZ@&_n?g!0EJe+u_6 zhh-JzZ=n3GG}msL?GPbx3XYp-%0~#3mkFEC2%CRn)!cn3cX7M}{9?nuu;s7hx}V9J zdU{gFK$2=ko;tcxFCX|q;ftjfY4GI3QBG;K zl;2GG9hBcoH)KCf^4wq6VO)(JXRzb6w&8^XWZWnCcLTZmLs;}OdGu#^=qJGM7W4Rd zp8o@mFN3G?#UteX_mcPDN#1`eZE`dE*bU^{XVIq4V8?0fphq!WwA1R{a16jPf*vsr z#}dk4MPzQof}Lm{hsfElLL<0?A@u@7>-$(|tihODe_^b__!{2#!3)6XdhE1K9qp)> z3qJEgg9xH7k?EpAV+=>9WyQA~@SO=hA9RlpN{XgFN$_OClMhcR z+DbLrN&_lGE78~mS3f;<6dhs_9Mk;UfDW+}&m19guOVvg$HEuz*6&f%jNOO-Lo~7e z0ZwB$yTcMW+lrx{qj78KC9DzJL!0nadieaPQ7Bw7l$uPPGO1HObtnFf^Er#{`?UB}sm=O!wOBu`X6tv=Wb+01L5(*5oqYt_t*%^-xZEB9+rdT?mvtBB z`%dirU~8{wD;F)X^wgwPpvJAyA8g_@YLlX2+bj**7O3C0Onr7W>alB9mwl%?><886 zFs>GdX*D@)QiJ1u)j4txhT|QocDkTSr}w~Ds&M)V%;+Kbu4O1c;!XX1m|}pHTr?hveoWfq*mujH9K<;#zpO_cj;HH z%R*JVE>k7X4=Q)vr&7}uDmLA&BGUyGn%-A|>962tFr&NSI_--O{K+!{s5@8;#+ibQ zV$TqNA9Sr zV~Gkq*DK$1w{kr%SB~ec%JMv~Owad}>Gd`EH(=cw{3inE*1)U}Vaf-_!Eg}MU~K8Z zmQEitb$Fsbc%naedaB+tKs8=rs`83ag;%o5yfaneU7%v`3KjY^C?72*mkDr=@07Cq z)+obor_%k7E7k92rLZO@+3y{|{SSWs0{rF<|4}%%hw-ypINDDHQ(lg2C!nrS(gAE~ z#+F8ZYu3-O_ky3B%Kdy)>K~+H?o%rCk5@ixV{%y=lM_&)tiW1jpv9yI4Jb8eLMg$k zloY&OiNVJdAAFPIf}c=q@Y~=E#RmNg@S8jQ2jSep6k|GSPJdzfyt+Uu_OQ)^Naa)= zVy^OF_Ff2fLErIIUWmVPLPC`l60MAoB&CIBDmAoFDQGcCXfcUly^0SXQ(X8;#fEQF zbodcPh2Nma@W&Jp_8agQ@RK6LxZfZCeei8yda)#y7>L9F9NRzx@73_PDr_kQg%LK& zi*QnQgqt!Ve9(0Ql^hYSq{uiWM5ZY|Do=4}F|pB&ijMA9RP+KxMo%j|`eKDev;RW$ zbqa|-r{JhJ0rx*d{Zk>4JVy(TUGS}mrw=3$f1ndIfg0Ycz@`#xVW|acXe^Zx>!8$F zS0%-IDk08aadDxFiHlZrTr%29wj$7C!sF`{me8rtgb@WNEK^XzW(6i(s(|=wQQg})v+Dd{#QZf{b786909FW>3|I{J*r7n?g>PGpbTq5t3Yvh&ukUW!LlV{TB z@=E#v@JuiGx4^k9g;-1_4$|-esOG#B6aXX^rKXd2WiaPOi;2o)|A#DBG!{?v6@LY0 zg()B_R(@Hj@!%BegJ zZKFUAmRwb9oviLSx6kU*I(fhJ0{8HA1-)k#8#{FO;@N@!W z4NwIr1UdQmpa35fllPR8dzLfruO#=VX6|2SBd2;tInt2=?rfZNJI(n=?4A&6Rx0{H@3%wdZ=eQc$ppNwy@cOpE-^TCH9o+xl zWg*LMI|dUcz=gMm3ri0dogN0Y9y)aojoVEj-K}64f@vJR6Fue>L3bYm&`a3+2f*H5 z)Nv55Hc-oX88Fr%rBj832HF63hQR9!uLo7)wprQtv;Kb2j0>7Iu;=393`{hr7hqr? zi2%s}Cl6E5Fkc#;;_^c*7K*_HOqc`gn6PJn!;VSpn8J>!ATGWPep5z=!>N<_Vv;W|TFqzo z(*dsI`;XHAZ-GxW1YbM#tp{aI5-d#Walp1uO^C z_+#3NLC%he2ko*7J62=ITI`rzlaj?CjAPeO;A+0SY8#(B$>;Ckd(Q*rb#S&&=PLYN z*hw7Xw)i1pU;%w#0)D>c0-xOq^p90=5?V~|7*xPUuxTEf@d@_`X{!TkPh8MOh>u;^ zu`7hsLjAYHZ}1r9UkVO`WAix9@p5nyTmcAorp|Nv$Tb|V zvp@@RKnrn0v%rqqiP76C*t3Mn-ZIU)5h`&rU%cs7)#L9H+823U2^)xkZS(XW+iIEGzAe}_6p4Y_ZQ&jYc z10+_bR0VfF+^O5>2lzFJx_ZLv2&aX?b1N{)y$?JH9tMxj9}SLkfDq>=^*Nm9cmX^I zUIed#-&oDv(EmXg`-J2Ig6{(y|2}rSzk>|Jc>af>gBa^UT!9xb?uUs58Dv7mWMwt{ zF5g0y*iCLYNPcWQA9fKL+fp*v73d#p(LXlnW3ox(8L=PGXz$TL?~tYb28X^duSGKFWs9pA}?x_ZGGjQ$ZrW|#(F0VPyWk9xAjR%+Ev`2&&;fX{HS{ z*EX7MzwReXzLy}H+Yj_PQ1TeCUS%{NN0O zGakMScnaaDAk6A1zm@VkDZh{MhX|L2DTzWdG;L z;oqYBd`&Achn&ad9Dj!Ud+-@}2fReCdjT!xakQ9+(PADT@4uJ4|4!~jxeYsR#*Q1% zWBB!&@;fNMhw=v~f0XhkD4YE_vEwr0;}}``Vg4OLwK#~i2k_uNT67<|?q0a}%;P(F zKLKxoUxA15#ocHzw-TQ>$QxBFm~I(ODamkTF(eeCy74R!;`9jm!(n3NQu6JCWYh;} zoBi0a7dv*-E<4HAx5K!VZ*9Rlo2l7Ge6fMJSP%a%_#EChzSM%w%LRo8)%nx#K#&~R?#LaC}BAsTL!}t zqINL~-xR%V3OfuK{(lL7h~FY&pHynhG%WE-)umHxUJjcg-!kVVoX zOW~=4ryk9th2FtlnD}EUeP%JGPx6-uJTQjk?3{{qw`-V|9-8f0f2DW*Oe-4qjLi*c z2LExhxgmcy=gj`#bEHnL)X593KuQd!PO;P}1)fZJ^5H3=)|K>*dbnEP>Vj(!o^i@w zj%PNYckHGo9mm3(h@i)4y*I(1@!k)B{c_;F#hPK?j@;Xx%M#eY@Vdr|%dD9Vbz|)V zn$TimV-ecaIO7Hx99$`IWx|yYR|&PRf{W*1P=RX+f2%6C&6mGdELYNo!HjyYN>W72eml5s?iDk z!O35>PNAx1zm`hpM3p0u?AUIU+j?*X|#JeR<`$&cxoKhwJa_&Ij*x|R2vu%&^|)S<=HqQz9XqCc>2ZmB5{ zZ6{oXZn4UDWB&!WEakWtDa*Y|86M3__vle7lfY!p5>t!{O$AmKU(5hTJpQLTP9*$-ypYJMQQ@zKRJBQgle9qCyfB5t^ZJw3x8a zYK8E_bTAY1ps*3>5O3t!S0b z-;z1TgJ`svh**1t#O$v4G>zTzdXlmL0+BzHdOhFD@sEs|?$tz1$&<(#@ePApn?OxY%fkN?0c^upk8Pq?E+%E_Hfpsx%1vYXnLVqZ7 zm3^U?>5s$W!DT{&uUZ4o z7#!WyuK}KNP{292T+M1w37ND3+@Va#0}9|TB>yiV|0=VReT9RpDlU?Fg*%Y54iL9h&h zZ8-V_gWg>P-LJ9vBW(Oilhlu`WyMyza~cq@X-=h5jX3HYNu7hs$-Ut9sDZy8{zjgG z+GIiF+0c>5=USbqgd6Z;fC~h4=r$U-jY8UL`Fl5@Jz6$0_#B4p7J~l*j(;DUztAW= z9YvT4N;%I1>6|B)(gxHy1b6z?k$b`G(gJ@Q{J5712@Pn)h0PW?qGfQA?Zu8h+N3{t zuA}oDBlY16Bf0M#<#VgSJ}ySr^8Lplej9sPz0OCPK^2_E)H$2;o2L5r{WDKV+#1{*D`TG*Ejn7`e zcZi9hm%+#4Rw~uQSx%h|4JeI|$5ZEUOb?*0UhulYX+MnqLRFYV&0&J$A}|T2ESU7z zFxcAj;|uMw96P3c(K#3ZSI{QY#k_|Tmn{VA_{qwd}upjc@6=!5XO@Nql%%nTmPXL`za!DetV z*ao)GV<*SmU=P@5#iWF`xzv>j5_TMk0@>&nRP@jYIthgxJP59368oTP@OKeSl`%v= zpw6Mx*9Tr#>SnibUVp*t+4A>uG;jb=1PK&ngQMUWI1VlcRB?8#h_P7vdKZ!w?6@fg zO{9SPXmIrPG~#tz&@=erHP_GX$(%FBSg@F}5dZqGM}ysp|EZI?!F3q?QudXcpPn!O zYK~{Yb>N11+{E!#a67mg+z%eMM4uo=p2Ci&;#tE{hE9RQA6tfYu?;QbunOT$hdUNO zhw_&9UdBLpt&IAd0oQ|@fl=9(cm&YpNc8_t2YHF}*Xcj+5F_sf zfK>Krt>rb2e`^K$$Tl(zLw7KA5JP(~?n5#5BsK1bi3BO+Z`hdLuE!Kj(s$#O$D)bfOxvXuJ-;M4uK;|}r?lA_-6wR`n5Lkm& zu$kKLg69x=!D+beAj^B2u=$Aa`H@_H1`qLD8Zfko|G@nt_!fKy-lJV!#}_Y>V?IZ2 z_Y8UeQ{>x^DhG~YGUqDFuc!PLLZp)r>?2%;sLw(;7QwNM^4E}MZ=q}Kp^F?tlem@~ z{sCI&HC}y5H=3b4@f#922>t=@SKuS?DtJbg_|B1Um}o0sY7k0YV&O?4JaZ_&nDQ$r zzmD?%Zym;Qd~%E&^9Xhv!j6Nq%Krqm|*Kizw)aV-$`> zl)ao5S&vSzga40U(OHI6V+YsQ$lZ;*yJm=>IeZA`^T6n0w*aF%o*+gK6Q2jk`}fl( zd&&EEllSk!j_vqkEBeD0bdb&H4;yJ4b~m6`w9o>b4B!3mj8gtD_knEYY(G|ALC$^$ zk@G8B?PH?(M=--DjkQv9UHe9kC&3}W5P|hJ)WwOR*~Ad-4Nnj}?170LE3spmyni`) z|5Eb)CA7^HZL{VCF+BDbpZXg72N*i#+|Xw5 z8^fY8{BHzn@x@BwbDB0;MnBCH^<(@?ym$$Qo|2ob$yN#--T`|2e?r zLY+J))sImj7_LaT;^0cAPMPQ(d2p4$#d9!VY2sA_ZBmD&HF&EEZ&uPC<*0XM^r}*N zTnVvR%t%m-8hb0l`EfgbW^mxr=*Z<6jDrz-rYT%<1}tsh%F z1^8aq!gZ-}x##jW&82o641ykBck+I_gE^Xqty-*{)MQ~&y*baGuweazMX0JQdH#fD zyvnUoRce*3V(UT`Sy!sSrcrsetbwo{RyK=oGws$Xor_?a{pCurzfH;Z=appl4*0VY z?Z4A0XD<10u5)8b($ZgsIRz zTKV>g%5_LrjzgZZ9Ltp9Sg&-a4y8H`DcO0k5?KhF;JjCH&Q~aghtfqmvlFuO```=k zy^h1b9ljNwOwqiUf_lRb8aQWL0oIm~8emJQiVCpNE5x=bW&j^Mm^Ir3Nd{Ez6LyJ5RT38 zE%hM=xWsq*QGZa)c{%SDV-H(zD&O4X-ghi}rK z_~Y{47D!qGDmX9Zy?pE;om3{aWO$mR@7SU5I4i-^O>x|(6yq7FD6eoudc`W-J5^z5 zF`;NN!9I-&^6gOo3$y)wSIO6RhkSgG%iH%>dHFsqPoH3m@ zdieyh|3csbc?PlnLf|&J2Og7K;LS1xJO$nXJcGjjN1ne#EWkAyLOX;KgP} z`^ngo0Al%ERFH+jgY6U&?5v<*Hw6TH%P%BQz9A9v35l0iXu3Q@3*`}3BloZlxrL3$ zHEg+D!nW$7(4%q=y-`jfkIOOSHyr;Y=iqA)Gjjzy~0Yb8d5CjSc)D z21M{W6a+m6iktr%gu4pkXQI&FzYL!#ekQ}3y$RTQz z>?2XZBhSh<;$hiDyeix9KgurrZ`ug|B<1$O+Y~vc0To8k2ArpXL=em0A~}ZeegM;B z-&hNI#o3{;IHRw)$%Q>*oY^zRDLzt;@riPX&yqb_j4jEMO?4HK4}J&1m+Xg3 zpNyp)D8CvM#p45xX&@2Af(U@@CPEI~2mK+}7J;z_C=(>}UOb56JT#d$ zNG114hd-12i{c$}tz=zbC-VX)0+@lWkioTxj$;IKA(i4;N-8V{4Gb`SU@?Vn!O2Gv zIBq52pTXu2uaw7rM zNFl}zMhz|eo%X0($#?eB`L3bi&oL0a1@WKNL;dPODaeB}179a{&i2>}rOy7i(-U6T zD)?*RuO|n^yg~u%it-+Oeaz#aQW)k35K>+>@Wbl>r#0cl^n`LjH|PPq_@mE? zpu!IJ&eafh7#H7Be}X-da{_4?2M+P&!G&NwpF7OwZ{@qhW$%Y-D5k0Ka{;^=oF|mg z52$ls9l0mGT>j($r`!o}(-RZ`~yOiZJV zV{un#7kvP8P4L=}{g3`KZInNcwH$Hj?BsV7$IakkunlYnyUdsv;gkJN=oHv-X)woR zbP=3yJy(D`RD!>=@pmGBiWsC1V5S@Obiie^>t75mgJUZ&%Ae)nxCiV5 z2f!uZAfTCN^^nUr5_UWgl11B&XcX9S9Wi=shIE!jzIp_Wgo<3r7f+v7KK?fL!ik2P zpF4Q=67dhOg;B4az~C{;$8{uHb9&1Ojt0jma1}TMt_IhF>%mRnHgFfX-wb`i0bK$+ z9wSE25&Vzh@Q24qtTyoDD!&}V9S?UH+`e$TZlnM0<-3P~!F3v(1xDF7gBtTT!_HozVj_o27} z<9?VhkU%DsMOIdf22w+o`2Vr@9?(%8+q!NEBq5RvBIle81`NgoV=@>p*_fc5W4E%p z)wx?a=Sa>uBZ0|alXC`F-D@4n}bA@F@~oG~tYkKe6c-M!{t zvu4#=^JiAA8R!CY$j@E)cY06$1>cYQh+r~>P;`kXw2xS{4-<76N#yQ1-`m))wLJ04&rzH!3JBlXb=wQE{`wlFjy(PaZu=Eh)wLIL z9moRkcii-E;Ge-ip~<`;zk5tRbC(?RCb@W)07ArOHLHJQGdu1h1sEJl-9OD(}c!sZU1`J2^lA-1IsAvK79!S);Q9NYt! zgdO%8(w-*dqHSnT7rM7Mb_UXhugILoaQ<&N>s0(WlQ5Y>OP#Udfel~6CzwzT$Ff{? zVZjrb^vYsv$sIz@!s-QC->-!FujKK+@{M2dHP1o?*T6Bb7hi107n@jbu#VhrHGQ&@ zJa;)YDb;&n4CDNxIRALgKbf{nXGqS*#yo7eVZ(>>2N4kw*wA7lk)Fv%D_Bj|d5|Nw z$>aYWhShsu#PE6_yO+TsAVYi=SWIjblk4WwC%N=V4takz`DF$?Qt?L;dQ1X32)h|# zV28297N3wqIfz{HysUR z8XP3#v?8ZCY2OUwr9fU96vRa!^M*fpzb}14rh=U|*y+UCyJM#}?deZ64#MIvY>mR! z1ayHZ*!m7zE}Y+ocS@p0LysjB$t5t|f(~&KJfv;Kwt*k;@<%Vlpi$7j{DqfPa&M4Z5OXIct3?p&7`_EVxoN=@lEaeV!TZwEHuSlOWM{Bd!1=p z5A5~9-sfV7%@8rzW~3NY>l^V!?df7b?fK$!TVK)7He7sWXAphtsDZFsCO);@C3@LX z15x{t=vn(`k&S&lHkFO=e`9Xbz;rMPjOW-GJ{#4bmKa&jL5!%=Kn%BI??>BaVyJB^ zG1#uX_|lI3AMJaJf%bjH=XJgi{p$=DeI3S$&+1MUz3a{uy(j|iSuadScRY3{T_G;`VmE{bN2E5To^+tP(%-xIr11Dezd|AXORFz*L)ygyt%<2!wt))JpO z*`w{$N84$Hw$l_{rZ?#j@Z+Qyb1MH#1c>}N`OXXz^&R8%Udn!Or*XiA8S1M}eUo=>uN-f=@Z#9dvJy6SX=O3Y(4_IT<&49V&!Phbrq90Up?zh206AxXj7udn2@!roy3nYhmBLqp<7VL)7lxPt@u$OxW}o&k1G$4;m5*Omt>G zCtMGX(6E~%6ICJv`!1Y!S{M2O4C{&yc}R|6b8&5Eu=n0auQP06mV);lx}adLD+`4R}>qw`-Wo&txsq zsP2pfz%nDzhxa`=-W7D@Z@QY=9_;rb|L<)h8hmCa>h!BCYWHUn`16 zecls%4!#DH!8{Ovb7RqAGHBo`2HHW4T!+_l>r#nXoO=R48xCX*P+#yV$8hU!8dR%K z(LZP&QvmffHo{?mt*AZ7fsU&WaVGd7OmagQ#6$7L5Qq=?3=9F|!ECS)^+hezNUt*v?xyJzP<6@R=irPbKGl|(l zye>i1fB}s!MlqRk6@Cy8%Ti2vb#0JZn z#FtQ)8c=WScH^}pZEk~0n}30Q>^2xi{xcl=BLwHRfw(P`dL1U#dN{Hnng%>3(I>LV zZW6&WiIYs^M-wOU*?hhe%y*6arikC}fa?X$^Hj{h#yBABfUFu2c6-uhhL>pjB{49R z{1>}c{bvk0=s4_8029Gvczg?wsqmNv51wNrW;SNRrBA-2Ph=<0c!gt6CHBCGSHSnTprxD?qleQE*d4$y>NSc!pv|o(GX|$&pP$&x<=oQV zJ6m4-jZ3+0=A0l9(9I$QC_xzg6M;{n>T`1lk60#Rb$9-zkuk(Y6eo&|0Lk3AF5~+@ zh!LZRMg0BQ1o{AX$r_xdw6_6vZJFMz_N1*p+6m`|3`F~wj5dOkw@0CqB#3_4?uPBQ*lvRDda=a6^rf^b+a%i~&%XpL2GYiIunMdN z($1#0koI_;F3K_Z4>m5t|XU3(LU&t^90tpNHPq$2F1>*J^Tmx%-_jp zs@KDG;I${Z!~m3w;b;-#$d9LRH$IE(WFGp58+p1H*}OlxL@@h)$Y;dHaL2AgD~LyJ z$RvL+MVDC5zi=N!GmyKZ{u_Eh_1+h&c$Irm$Qp?MgyO$}e-X8~)3?W7^?n?`kYm2# z-u*fE?GMQgZgUsUNXEvu*qBM?ItLpr*zlmKzGwwO*a^i>G_@Z(>Ln6zM-FaTLe{^X z?CLDK#7h`e*I&rm3%L(xHux9p{|sKf#S?hkC9k?kj(M3}<|29j8EQd}lfxW=#{vAY zha5&`jb%(w+wsX(e6pELa07#N9f7O}bRgG( zNDV^Tt^^Ojb#PkPP-{|`?renLT41XKwt8TvFE$1d0>cS~F`R!Qp)rLJnTbE=V8aC) zp4jlGeG2SE(?%m9ET2eHioGoip)=TgMVMFDUTAq$18MsPkYTeQYzOOzk5$CRGIGqt z^vNR85j#Dw(U(>Y;{3xn|0sHVJnfmxaG8#c+1Qv*B)Fp&_%TH&8CEKK#X=4*YcN(= zdnpPpM-qLq{z3^tz!hws1Z7|~SOD_zMHc!)Iyq)4xm^l*e=_-HB0S>I>da(~M(RQI zWR_a8Kbcjlxsb#VuhH~PBry_>Ka|922;T_e*8$|*e)z(NxbP;@ys+&FJb)ZJJAfQ= zc|cAz7GPk!XvuYB=@S*Pp(5{(fkzZPB8by4#tS#GXQGa;&l5cyHZ+nt8yVEyr^o!-t$b6eU2*QJw`#kIEfx zClc-7u`Sb}o}7l_ft>mkz#n)4cYNVWe7evl&cw!iVst({=D}kQJZ8gV7HipN zlGRU#$253M;pSp8{W6hnOyCUTXvJ81Xfzf^5=|qB?5`PR!%NkW-)ZKhvYt zzBjKfU=H{WU(95zPG`=T29Igz4^!bW1s>nRV-n-`8}!Qw@E8Y=vG5qp|5*)!t(lzP znIpbv5aINik!(${oM&OU(@L<&gw!S1%hx6ZY^w3o7%t4j#;u8fdwL~+|prF;gQEq!P z^L#3*z`u)h8+?G>ATDpN^{`)`TP$8DfH52!Rfl^`TRZW!O?rUR7|i>D9H+cr^lM;C?MfZ-nL~Zir;ele)ZR(-vTrVW z*tZeg?b&~!PB+n|PH)lKVF237FwvpzIMKe|G|{%6vuIP_U$m+pEn3!35H0E#iRSe- zie~kWh-TEpG^_hmXlZBQC&U3Zr#W$t&E@rLFc=KreLs%(0iQOiMI8(KE;MuyJ?hs- z*J&iW)N3L-)o&>})@T2P`kh6)20caF2K`0rhC@ZGhGRtw$LXTEql;+j7$};sIJq%3 zF^wD-h)*212uEsS92?#QPlbwh`eH+_Kav{IsAl*d3) zbo#^rZKr`~=ZOBmbBtPl!v2qqI*JyJdWhyEVoe(l5ltGi|AG_yFYpg@j!yofp;NSI z;1n<0P6iX(61Nv=fb+c12g|BOIH3AsV8^G@v-XKF^b`+uTbyus*3y zbCa-ZmIu}f+olJ>6;ZoMr3l5o8#d$`sBtZcgI4$e^aWCT>BjNSpaWdmQV-L*8EY?^ z*`e*!5l+n;h(;|M3C9-AM1vNs(NsE$dM$g1x~=+&I<1BZ`&Q$H9a@ZSYj;trHOg-* zJxK#Ia?8~uPh25eTqcQQ4I}oQu`vakqq+SZ(uNoWEO8M%Ko^d80BzyZx@9fVqGfH- zw52T?5BpD|#nf->C>+{05p~+~42-tzge}igs@1NKuxU34!D2Kw%2NTeUb`Tm5b;GHz9a<%s}?hcSMH(vc+kNSd^eHb zEP~e$aDqd(=VBhVCh-%gLw(VO)9~5@birOnK5NTsOWqUmq7lt&&;$LUmksqbTwp$9 zB1el6{qe;B2KhiHlrQjw6x3uvk4)s{h9U!|02cyH!FSF4ri9;?!;5%8hna=lah!W7 z_#8+%sXOS*@%Fs7;#f26Hl~Xk^v3>Yp*jA4a4qmFcORgW5IYECO3FY zgvVrfOo4}-^rzD&)9FaL=xi#4r%dL8On*!cqw|ya%`z^|hv0cnjHZ2qv1QeO;L?pY zx93<(=r{S2XV74`4qaO7olV@zO$FxyQ@}Jp$cdTwu&F6vW9tI}nB7RfGZ!hxfExLxb_QS4Ri`I!Y%Nn32w720X?2qI8(x$XG6-axt z!1sVI=Ee?NfF^RQfF6QRyx`#rkANoJ7`6pC*O!y}e9LEy53eY`m(Im{H8(9ss`mrJ zZtp>i1zgsaHa8uE{cq4;q+Mwf`rH)LCh(gr{`LYszz_HXnrPK2l=vi!zKMc|swvt* zNB+jy5jZmp;$cDDFqpV`EJi!oA_haR-&c%<5%dA>YCZ}3)3Gn@N}IB6f3)?^j)LQ1 zAOg@VZd=x+mbea z0!@2Ye(wzeKnVDwEfw$oXvfU^IFJAcwcj*~9R4lOs>p!@fd9{Fe@JBn@yw3&spb(URgRk690o)%asM{j!9NaS=Mm0{W%|pA?bT$(XCcpR_ib8d0hbHuucPsAGZYX(wDEW3MJe2SVCQbv%Uj2y?KQde&eB#B0)YF>n z%as?iVd2W9#E%{dw|Zw5zL>$JF-;tXX(c_#Gf07410sjC9QKJoP79$xPRC3aoY$7~ z)1!&9JJ7a@=D9CE(5+mQ!H?zrxXVEt^h>_`7;;E$8s*p)A{sv!+Co;wo zp<{@&(M0_yM&u~$j|7Xcl?vqaE2m{|-~yP%IIj(!wI|}(`++*48N}!ec+7yuG{(ym zvgmKgtS1p86Un|Oz+)U1#`117zZePAuW8L!cyt)OIfRHBOmu(A>@kRuKM4C0dDxPd z7kNpL_UD0_U<%;!MH_zCAI76UjEBcK#_U*ljE2W3c#MF@*YNm?GY;dwL-=$sXBdQs z2I9#9L}Gu&h>YgGjI7TX<$V~ZeW*q01BVps!~lQbY{R9x7Pkwvxkci26vsy3i?7iN zhtnt3`a>h^GzBfO#q%%WF$f+5;V}S9pL3)izv>J7KD4GcUhPG6_QXmL`lCA|t{eT) zm07hbvtw5{Sg;d{JrBF;+n`Bxxb<`3mW|h;_S|Q3d{8a+Tp&INFn0}L-k_o$G{sg+ zY_-EyCv0_xMQ>~kz}7I{jph7P(KzN2J-&P+LUhDh2S#~&#z#B)qaCr?jw^*0`+?Y+ zUzgjYdfeXC$37Sg2J*f?$NM_ih(7jqqPMLBI!Ar+sSSHS5~DqtJ9?m@cH>N4vDAel ztO&u@m)II9+S^PLZEe06ZEUY5m5cNrPs> zsX;5zxIstJs9_JZm42e3<51DSah$04$#hY-k*jcM6fEjA(g^!c(uJMlQeo@3OV~C% z3+{_R?9auIt@kL zCOm_)Ni$*Jq_wbX+DX)I+EdhO_PMZWMuI?HjA+iyT61RJX3>BvLQ`hOCc8$Y29bQ0L;|-6PP^c92|nyOfc+`h8QFq<;1r*M9-uSt z+k@6{Y0(Vr1O2B-Q+sN8&|jL>5%pU*2#1z+g>5SWstrNg7H78Q%REm>wCfB$1%tpC zFddNFvQGFZvST{qRLU$V)+| zAN?UWwe35OP9`SUD@OFz@cmqVw*_A3ImZhz3A=JV+UFhl2z2G|4je;b6D>hg-Z$cS z1G?COrrCDq0@Djn6C*x@M?YJB43B~E7zB?Y^vMu9cnHJ?6PttkbJ3wc22Eq~^W=L` zd_SGvt%KWX&hbo)h3;U^-xo*?s59?7V7E1PoAJIeb{k;Vp(plxV;lE!c_eti7hrHL zIuagV!Q*RULyGNVoS5(krcs=16h9g{gvnytd%I6LuToPO1OYmUah%F9E0I(#Sc%NHE%l!HrKQaPgZ6kI9Y27vJ(COddN`gG zUtsGq+WJldYD1fw(q>2e?0{Vx+1AlO+WZFK(rSCW&IB~gs)x+wbv|$bJin(}2O&oN zT5xlUbG?__(JK)chvmj5xxi@ zbNU<~;O~xu83*{f5#8n>{VVOx0?@YFbK}($NLzj&5J+1J5C+0QB!~gAKnKjkNdi1% zEl4usB!R|SMsWknN%iA@vq0&Zinrr z__{vyY~1;cY^Ss-?WuqU$aWcl1tfwLkPf7+93br#f>N*;EC*}AW>5y?-lzwflFW9c zMuUO1V;VIg-e@2^>kQj1Xg7~Z6!x+|rGKSuH823#wp2jZaVuD@@5r_-1WUmRunI_f z>w&bn1MC9(!B5~cxD0O9Bf)M-J;@|L6bN)V8&=cc@#henZDk8ap*< z+Pp=})@|Ch>(H@N=dRtl_w4m)@6Y=7>px)N7het@GW4tAUym3ydd#@-6TX==`P-?} zrq7u5-R$q@&YSPz>gMk0FDtjCr_O@ zd+x%;OP8-+yMCkM*6q9Z?mu|+xbo?3Xy?*n{U;g^f|MIVY`}hCnfBeVaf34~N z{^!;c2Or!2e^>w8)RH}3M}~X@V!n|K`esCZD;f6fW!!fm@O#L}?;}J1b7Fsx4E|v< z`bQG}V`cnLWB^Q&BVeW+0&^Gx&Tiz#m z{`z2d{r~JQVPjLPR_)rh#EgBNIt~tXiJSTj8Z;zyK55jbG11ecDe=?11rgM$H8DgG z5k-U%aYP`=NV0}fbu9fuFj-@XP?|DTPNmGG&aUpB-oE~U!OF0RXq859w8ST+rf22k z6&9B+T)cGo%GFGla<<&Dlc2E1#6N^Ybxc@;LPiB)L0k|RL~NbW}YjK3={e zLf-uR^DhL+U;pW!{+T%W*T4Pk-~R32iIxBHAOG>6|M~a7vvt$|-R<8K-=w@eq0_$Y zN&OCQP9Ak^ebS6Gs}kHVFN+JjxyY=zTdEIvRHO-cTA)@u&r>O1=Ej7*&WVY9lN}xX zOL}DVUo1gUfBm!g$f4SYAN}yr4}Tm>ALHR;Jba9Y|MmLvaa?>H7ymFWc9kc}dzil5 z!;GxHhe_lfCf<4vbFV}n{HRFFJxnb3Flz2$RB!KLVz`HiivEi^IO?x|7C*!{g+s@E z+mqjMn0{teqTBz5L+Hy~b@=NX757o9=%3S~Vtz3z;PEH1JHFZO^282s_zXXcIJQ3d z9fyFM3(bmqC5E?t(BOwyIK;vsHu6oDI{N3-XgGwz;ZFh%KU8z*b9i&g*KnA2W>u0a zevlkOB!|aEI{HF#(7-_x{whZk`8q2$`c0}z^|Lt~9)A+MR{Mh=G zw;U4QaxlK-po4=J4!ZDH+1kk0ncC<#DY2@bO;Is_8oT40z@Z%+K0UlS^(#0`#Sbo* zmnQgEEVSSU$-xl(v_P+Tp05vmk*g1TnXQj_m7$A%ouX5}G2x3pi`@xMyUUZ??%ST) z>(G|eVaL{|PC2tG`41dSa4^Eb7*dsIRKCbIguciyguhJHN4`pghe-pEKZ)H5jp5LG z-;UIthqj~*g~PXSaK5rM(Z6DW^n*F*agiAg=8$JOCdKnic%;E22_6=B7<4hO|15SV zHridD(rVw1wC;ztqz^u}K5g>Zl_`>gU&VsB;QJ+UfsYE~f}Z4Bf}UoXgR9a^Ayr96 z#q)T$7!6@BHSqYe*qzjHcX?{_eLK>-9NLoc#nE-?li)D_>e3|NTMH6`?v*44Jj#y` zsLYNFe3E7fe41nqdKPaAsxp~^pX-c@7b-*O%Rh_V$@O=ar#IQVBeUb7%~=DEt;_i4 z+{(1MSC^)G-CB?waIY}Q|3OZI|D*JH|Hnyj{!ij8{!dJnfTuch;4?K`qTuppu{+g% zcX_7c-W@ru4{plte`0OsxbrJAXI)#I?p{%x>USqE+4o*XlJA4$M4yLo2|kaF@xG6> zalV!6IKL-R7XPPV=748^7Q55MuJW8Zd$#AbIJ_~p&&f5pUtd^~J>zO=hWpKeG~ZiU zsXlj7QoQfQC41dBCV4&3CVD+oCwM=KO7MOh9`Ey58Sh*9XR#wgl$Yg;J=+SMe%w&l z_0+QbuP&74%(zmJ<$66Q!{=sNy4S6QG|$_{RL{HG6wiCAWY7CiNuKw^lRO_N;S=(I z09&#|*;b0!wv^UCy0W;->C%D`7xHtbU(U&KxtfvXeJv@|^M*OYqe7SNaZ8=%aXTv2 z<4yz!P4T#^fKyQNpU1`=v1>!2IJjnEy<-cCd!EiO9C=y!n?>a=fm@WqV%J zXL($Y&2+yJP5(p?Ct>OCx0G-ShEw4G0sNR|lUQ!*v3IBWi-TJ&V}IIYnRRlV#r@oB zbHJq)Cgt^|T1CZTmEz9AD8>EKaOI=o(6GuPMa0v>;OMISz}V+mewvp_p0Tgu+&&y1 zIaK@ZqaQx{;g93!V?2C}hmY~_zg|~<8yA*$*Q}Xbv)sUlIL0FF5vPnqTZ|iwF1j?y;{v6i3o*lFCip_wKX|Jh(M(%ugHR z{(*x|achw}bi065s(Jylpv9FW-R6iR%R6pz8V_$zLtQ^e$ zdk#zBurM~{eu*mNaZ$ALX+dP@^Sp?Nm$~85uQQdZH*tP3zi8nQ>k<32{zgW;8RGzAz zW4zR_KNLsOL^X#v$ze2oG2`UgIO4(Le|ecnd1J9Y{U)o z#GCB58i)n$A=W?Uml!|wn-9g|G?8NUL)_;Fx5mREUUG1i9Ii+Xi}WFPOSK`73S&c_ z=Bt%2&|zNXsL?>sAJSuDeu*RR*M!9Uq6$*I`A{5AmmIq8-C^Z03J%jHhx3xdQnT{r zA_F<6Hso=kCgfRuEF5CtAoT}L^qX{Y|G4OwpEY4{AT~Y}a7Zn;NDlq+!|xnc!eNP7 zS+UR%a=%y?Tv@0Msmj+XU*zfFpo@5wsf&7@rc=F%k5#?VMyY;QMZn<$aVVW96C_*LgXtopJgyTQ`oiIBI84P4&gWOg`(9lf$G9{HKPWZ_RTh%_=a_?^Wtf7iQcNMw z6UqP0sNoU~mk-5(bdgye*KzO8ggytiCJz5;W8##PYZIK$uSoE|wkSUM&Vu-$ z2gUJ$kMrUJD>E&DPg2SM6Un(P=AbGAdAEkVI|eS1_~t{gKV4++if_MnXJYSzTa$+U zv@z-1Q)?6FU09Lid3{l0;GNQ>fcpiB{tvSg{2ryn`#nyK^Q*Lwa~shBwB+4t^6n@& zg~R7Vu`gZZ?n-F8cV|-1gIkk_{IoH7;;A($vo9=9b-l4D)%SK$ir>AQWZ(PgNj?vf zs`LLx#sr^7I&$vVc%RB>bbtsrg~I1Uu{T2$>`H98cV|l1gIiO-_-SM6_>-$Mrd?W^ zIrm0My7#U8G~e5qsor;z$^YZY|4rooI&$q;5Q7E~i3Sh`9|an~hvJ6}QL-z!>7Jcw z9S&?w@Au<|tYN2@=T5j>k~Q~Q0XcVex=%%F8u@>!*Bw)e=UqK{b}adS47ooVz=LqO zSTz9ngnS^%GsNOuX$|-6%xZC9Yfkr{Ru}d?SDH8RQhv^yD>+#n*D^A_ZzN@S-n689 z-qMp}YsmXm*= zJg!D&dt3{LPbj*G5`=)DOphCZe*&j6h5JEchb?9LA^W!*#vk5dn00ic!R_RFga6qz zhR_SEG@+N5tI$FsLn{`Ch22@G2*0-=DC$8;K+L0Jzu3wmAKjCDFT>MJH~q6@7YW0A z<0FS^pMCVhM?d^={Ctdu|5owfanR6VYngs<^*m{0o;1FjCzTggYL!=*CvPl?Qr=n= z7J7GqBK$#VaO9(sz?jM+|JWymzWQf5Uizw3H{J6@SA7+D_P#ioDLf7sI&Ld7z`;1~ z@D}4tD~EMP=4zwz;!2(J>N2(B=Hh7O?S&D_`=y~_4@*KK9v1~gKPe1|eU|5^d!Fv8 zeUad*c^T)btBP~ezc=9Ex!=%fdzsaB|P^TI_;?DF!rqWusA%lvM@CKS;0SOAVzo1E4{n!x!zr0^}c|E*FHn%on?kW_(5`*acrZ}%HiB< z6C4c0f+pnlLKPZBwBm75Wa!g^@bIeq(5QDB$Q!AF=+I#_9=him4}I19LUPz^=v?+2 z2NN7j)f`rtLM|^eC~hp)hTK`84tZD{qo^#5QdZ?hguloQk9zeF8ptcPr|!ARQ(yJI zI1YzBhAw4gM(Kw!hqqWcxSU#N@;e8IE6a?En~SvM|FP7QsuWKPqRBa<__zNk@@_N` zbKpA-B=%Lbx9&NpdSAf7dyk=Od71Hx{o75yb69KgJ-5;vd}XOoQL#`*{vS*JuTnhA zkCFAP;jhs^ewKA079|=;K+G@EXk$@++80rbllO(>@Pnc2uHQLKKeoZ-ataRT;cyiW zx8QKUSQAo7O>I@4I`n0ZI^y*|Y9KK|>erEh+7};!V_CxI2Sc~rWyXQ{VKja5mV?>n z{0ei>)g>l!Z#}uU7A;8D#K>9@O#~W9lvM+Xf2)DSDAjKw6`Ge3A=(%33&~-(q5BWN zaWG2`&ZpLxy)Ue=1YLu}?NS4Jl#YM<*TO-EAE?8S8b~VIL%aqJBwF=zbcFg%M7ZW< zc-Z^nXqNEZW$3=A%rs#CcJrtso6XaXZ7@5ZUSsjTu-p=Oo%-fGrABIE^l&hsgOFcm z^Y8s>maRE1| z%eh->f`cjeNdfvp4z&mAXb;J7i9>rZz$F$gQF_&@Fs0xn8J^b3Vi z_1^d~TLkPfcG**A?zextCp%N60f<6Hbt zmehIw_S8OyH|F&^xvb!;3&pw9F6U)CU(L$$x|W*dbv*$+#FXK2lNyAISaR$b^8P3w z>kz_#0zD$QI{&|i2Jl|^Ay>rgPHOx^S!(P3+X|W|L|4?y;ic z-lwyQMx9M7oO(Vbf9{2(Jhw~nxt^DeldtTJ%d0dJ`hd_h4 z5`_*C0iQ7VgaQS6M6lE$Kp>jU`(k5(NM4;M_LeNN`6+8*hvO*=2AoJJ9ev7DGUc?X zX!aRnq02c#f#-Q`zUKv1zQ@IAxI}_*_=Ex_eWid`aE|AdAo?uu18^}*jJllQ5Ld1l zws)uIn}gf5vyN=hx*gr5^*^yer#QV<8+LxRD(uqA$gr!+L&I+@4T-E+92j+Hk-zHh zLf_c?3%qp?O1+E^i#^Pba$U`jGo8(k)1A$a-UlB!RQv0rA3plwkK^XQLp)qyez=t2 z5O19yv=iRW53bA)eka!J6ldi8uu2_zc|}y%wPj&pHImy1URn=KccT*axNF zx<@5mrpE>Drphc=V`ZANsWR2Y{OElEhtZc3>LiqFhVI>I-8aL z*62bntc(r4vOGHU`qFT;T4ng1g~5^c76e2;Eb&u6D)!M;7J3<ph3haZ!U=ly}c+j?B0TqhzBKs z(T|G#)s;oQ`lmVMm}nqXN$4N(uEr;EuBONDgY(%u8784lQn_}>zMa~499)iV)Wg9L zbZ)gF^wJ6~8ib14y(s0Kg<+xhOBLadii4vn3j@?o^Zj&HQUghJ)xMA#h{etL#N=jr z{609BEyi3(tdnfzpc{uDX3!UsgU>0+VU-chT#E*wrVb)nac@C{@?lA6*yEy*$fpHC z>Z)u%&5L9YS=(XNK&p)H#wP}M)8qGn*H!7@p!G04ejmK$kW#Mw0|&j! zu?=rItTZXFEYqV$#iBvPkatF*L4=1s%MXowo~Ka1O7>Gp4dji^UGq}wp{t7ZG(L&- zG(CPFoUQglYBh(k2e;{`|F~K23 zVXXyW?Lp)p>p-eCkSHI+(b@|{Kfd1Nab`6f zmYWsV7aPgFb>#o7EzVafUgWB&fnY64rkWZIe4$aoAp{L1So$oS-a0EcmB z6YbM~=P>QZ&4zh!a6hx!;&*AeCFI5;WA*xG>M)+=YZNc?v|-kDAnDqeH}UY$M4~^0 zt6xWilKU%lRiO&Q)Azw?IGjne&nVY@fgeU6+-87-aqjW;X1BAeEWVeQS%PmaGNDJY zCWbnUXL2nFbr>%)$+J_)`DG1;mKqFp8hITaMcyBwd#()EKYJg%<&gP1hp9hqGR--$ z&fj%JLyzq%yRU#`QrUyKHko8bRABi_F<8U4Y6{-8&Hh&2bY{-IibklI6lDXPlf9Q*8j za3WWHb27;;zueG&-wxx~hqjs~QI9g^o7hid)7FD{_chq{&b z!O=W1`Dl`DX}PJ-z8&VFhqfjS{b^(7uv4pZ$6Q*JJ^NY_Ic;8sPelgx7)fbfw=Joj zcl6}=)Ii*eL3@Zqdq979ps3a!9{N)&;)}1|2S@V7lpm98FDW5gSkE}23 zeP(gqxJ!jO-(AVgcDt69<$XOR)9Xe&br44M2Q9h2Tn7=I`n&$1e5*eMpgs5{d%YJ9 z7KmwwlWVUokMFW?XJ+dIn@gG;TU^xdY(f6`3)y+IE@kApTux;TL_)UbRca94u7f~> zz&}xPEkt!4LIrCfDnj6f25~DO&GWsmuTac7kZikUS4y+JTT8^DrKPP-<`)e*lUX?7 zTx!A0^GW&hFC^r-U$o?TUXnEkT5@bPxql3NqO#quMW985k@L%11O@s?aCHsBO==Np zj9o=y&hF&8yEf&DJ5^TJ`r4GSim(3MU*r7ijXw5HhkjHbx_Y;2+DxtK!F^HFFb5%38IVQ^BS z(IknQGF$9jfmRZd1D**`oG4x+yl~#0E|H>2<1z zb88|aF0KlTyu2bL>e}+am>bLd)i;;=Xl^a>(%)X}X}Yt>J?`!Tw}g9zE(!N?ofGb5 zJ15?)DL!(j_SHu}{I~VP{{MPhgufdXSKf__rT*%QC5(&3Uiv$WJWY2Q7xzjT7X>cy z_j8=%?`1hB+^s2YWQtF3C)M#drRlb$Of_-;4)xoy8S8s=lP2iIdTrR5wd(NmtE0m& ztqhO2x?B-?eQ8is#S(wj?Zv*acNTf;?k)5*-7j^Idr;tNd6?~Nd6?-Oe?OfuT2owy z!!0vMFYHt^(nUFexLny?G2V!|%3hzP&7Oc`-=NpR$? z#Q`yQ&`9rDHBwL0qXIYc<7}5|eKgf2?m-G1YKm)cs7SK+JfZ2nqD=h<4jS)c8+8F_ z5X!S_v|$%ls?b3q!>%t44X;=n5^-l?VD!BOe(DD$KDtLmo~Fuda{e@DAtc|Jpn(=JhUy=<;TrhFUjH5I=$lDYF+51mFm!|%c4SW zE(r^}y+{#$*IF~@ud?bed7g%+>F6KS`aDfU|A?ng;@slz*A!Q?MDH8P_TI-dJyuyc z#J=O;d2EB;|I}JT$oW-z^r%?!&S+)D;)u{Y3zcE_Wj#u9fT~i~qGXb9C%NjLC%77( zqQ6v{-7F7H?(z3)3d!L*9FA&wtp1II#u*L5-|1=&%FD|&=pZrZAd$+u3qr#l zSnE*&RZ@p}p5mc>5$~pbZlQJ%{iV|AZh2_%h`(1;T&d>Zb5zrFO__T9{vEM14sFxS zhlBg^4F=!SYmC7cRvOSCv}h12bP(%0&d^6i%7`Zg!7){G{boG%7iKr@3!}UKnI3&k z=Mnc%>luHqrjQ)2CENS_r0Kb~EVi1%R?R##2>0XbjlO5#aB-y(4T80#tcj89avl~( zD6RD<`HGkqX@2V0CJ#01V6-oE9{Oh*4^w5VXWT=zSNy%2;!3vUVCVazrsulfIB4ho zxJmDJV!hGl%xY86#T7>7wYO_x?v=!#gG4ExQVa4tH!S8=f?v$fI<%lz`a|uhf2Q&> zRmON*9>(~@->)eyXNx{ptsHu-FN+*`4KD*i!cuB5LTB84b zO$@alRk9W&n|&AJLZ}1rVhxJ7_C>Uh{+ZN1B7H3nBmLs<*A!L`DRzEGG`%)dbC`Z; zs}2rEmlNwuUT0UC1K+KQv93p9P0Z7LmGVW7D*R2B3JoMI`Y-Ar)z47@nwOCQx~hl( z_LZY+x(iyz>ipA84+lk3c$tceM@{M&jI9a@lvJs4uC zN1+ZQleH-EF)=@@!{DHdeHE_IJ`W2)dk8i^Rt8%h))a3!_#e{r-dv`U9HznHyPr0i z=AT?^@i!MiixL~7dJ`EH`zkC#`$8F}f2IgERfZ@n zk7|mGIXuNH#V+8Ww)d7Y&6sKq2I|$!b6J<;etu<~-_^yIkXsAPZ`XpDf}iEF7DTQC zNwVq>5wFzfAQ76_SD|X{3q`cPDmcpcBq+lCxTd&}BR;#7VjH+$``I?hVY_w;bz0Mp zZnS)VYHhshg%t@t*A~YG-)1j}2kZs$IG?>BvMfQ*QdoZx_f~(1rY-d^S*O+{&cCoc$?N)}1lD0B z1l})92zZo34Mdu(fnfcGQR)w-z^X{NC|M&DWQuqmU{;g=8=v?ZOqDgoxm?lrLaJ@Z z58D2_%Cw`XUz~7wOWeex8%ba(kl)S$n)%Q*o8bk^;5OGP~_YKrOum;1bKgjhLA=Ettutr7d4xaJK zN{{%6$2G<2Jkjq=TJ5l1+WvcX>W3fL9zW#pmh>-=uFoERc17;Q%L}sRUN6k>x|zc| zjI=+j!+5t3gVY~F(I4a<3;~JW4}I~G7k%cQ82qrNIGHc{pG>P2wM*Z3FZD?WcCbgw z=G-2~Ru>L9w;*@Y<$|0!SKqF~xXwNdH!T^S6$WzrSoDV&>K@+h!w`c05Qr`!wFmEH zpZlKFuhbOB3&en9={D+JhEMmFC3fDwBj4%Zy3*Ds7ZeU*9meE~S$W@IO3!t>%svcP zxT{G{|idGp2{uy>TE{g&f*s&RPoRTQvxy)F8;M ztr`S5KN`g4NI)He)F7@XK?wYU&};(V1A9uukg^<+xQ4YT3m1zc*$dkoOIgtWM11MU zljf4ir;H^tPwR{4p3xP#oYfV2oYRo|tI7Ri$or$<69K}}MndtE5-310{tA2#l$DC% zs~3vk;zeR{=1NhXxV+6F^OF8Q8WxT^u3Iqagtm0XNlnR|)3Im}u|;lY)J2|WRp=1W zAQC?45a+`{D4ftCE`;QJT?o$ix)}H#Sie;CDqSt+WNZ}CmYu?)*(*{bkJ#pi9cZ~I zba(HS%ALd3DYlN^tk^ttn_|OvI~D8amWQmJzdLA+^A7>5o%i^!cG>H@%4MI=N|*iK zD_!?{t#CcyvE1#T`*OEKHO0de(dut;4M#rLw2k^Ha@5+gsPFghh;l!;E!ywM)|lX< zn^j@QH>x5}t&fa4vom_Fmsk30uB`CUU0d#DxW3H8d}En={LQ7V$rX!S zQYuQEQz{BUzH{o$n&2acYCnDSL$x0sq>5Jm8rNXdb4{D5AEQRC`#mOok8FtvI=V>} zdVGU5CL_NxoI&oO~;v zn9L<6Yl8c5_-lOqQB~U3F^8i@Z+y$)(6$)wBU@B~M>naJCpN@JoL(1=J{TT(VU;rK z^2(r?tIPe>*O&QfZ!Y!HS1j=`-&*XJaC?Dk;+-Pr#5;M;Nq2JKk;NFP3GSzf);}lI z8~se%N_8k|^yadt+52}yyB^x6@Q(@I7JD`2Hfn=Utei9(ABLyJ*F-dqwGeS49g`tCw+-ThJz%fozE^CRli9;Ktfq~ecc*QDEtZppW5 zg1hOW^|M5Wv6Z?O+I`VucUn29<{#P`>y8fMb9{p?2puH!-0E1gm}qL|!ozMXRYu%e z929kDp`YqL^)L^Ms5QuPF;%9a!KApvJxp>CTbUO|XHNhP?JV~kphvvFH(c{Xh zIm|n>RpSZ=Z#0O&(`)t0^Q*PggQ!BUEsI2p2@Sg~Yhp_MV;;(Sm|PEQEzHv-^pAvU z4d$N3J?XaDJ*A>1kQ^Q**^hm!Z>IYpdi<`k=vfDLsOP}J<)=+LuM->e0q7vg^Q&~! zgQ!_U`EG5HtceMTd0gbHdzwz|gShq9Pi2ezxfKfG1z{L?1A=LyzvqCqGwtkhGN6RWtnBpMw=t_@O#KVn_bQ@JK6 zkvbGK7}nvKDh+5bdXI#AI?trrTCbFfnm}^6Z{^U`uq*nTJv(D&9@ri`8xHf~;DH7q z*X4vX7)$4NP z-XwXf$&q`K=%~rjP@kkm|4>Q&!#n<7v`^CQXy4?Dng9-MB!>rvCZ_V3343>{rXSd@ znRR%Jes1+TEt3!Xv;<#XW@KLu9s6>`QU}4B7+DLF6BYF)PVNWd9s4@kM^_c)YkY$K z@knYP5q^nxBK(tY)dUrpqRrhD+X)X0PUdP3(++Ic&SYKh_ebU0ptWXi)}sVnS!yEx z*HeR_33)8nqsTf8xfUgkH94wa)_(+1e-UVS8Ww1N92OY&AS^KPZfIcA?V3PxxC4j# z1}BS^gL(=aro&spYomsz^V*K!{Wm0bV9 z+K*5rbq@+tr9xqOs0c~8rwB>9Qxn{*=J3ruL*uyenDKjes=qz3T{o5df0lI(#F{vd z3oGLMuPrhwZ!a(fKV(15Cvrc`9Q21&a(**9NQ_qfI!s0FLzKQMB*OS4INbawC@lVd zP-x=angHFt&FvK1Z|)i!#alVVPCBq%|1E1lW*plPH|O-41h^=6Ao-MPOM&sk?`G_)k)45mnC`MSQsCCw=^-}K>_(J>oBY}5aze~L%1>I zd9X2DY7bTZCc{%-z3H*9#`4ft9e=+jKr?T1Gqv`l+a|~4a@APs)y5v!W*&EBbHc>q z)_r1A=UiTv>V9)!vj3f;WcGqUgOKYm;uCxy>!tosz4qc+pwu4Bq0g$d2g74ei{+uG zG44T4a5Y=By`ENka)rq;wOl=V-%kCA1KZ+QkCMW(57NImyDD?Wl_iv!AX$4+8Ho1aYt=a=S8xl)qle7zvU`(`%lFjB~=S%-1QAlG2X^%rOl z?Eml}@Hg$j->N;hCWSw6PKvo-6I{&Y347@_)2^5s=9KFOvNy@62X-_8`|^{H8smdfoOydvK@UTvG#Tgfsb~{h3TLoBE{E-N}uA*jgy|uUgpP zSaC`3(>X;W&!)?D7zK0CAY82)go%7wM~=@rjH}Ua>mT0LK3KH}{N#&vm-6bU2YM=CMCY(dHacwxUEFE?U^~cvflkI*i1kDQFP0(IA}9n+x187|8XtXb)-- zjrI_M_7GOBJ;?e8tM(9puY9w;uKBQ!XHD>9k?43hPdINX5(&%l#lE7&c0Xk->~uV7 z!51guO2?cul}tHhDE$=| zT2<^0JkLa{ZYVwLDuzyrWjwuva?Zedm( zv`Gm)Zl4u=v}u0O;cg{?`v)!#*fU~zz^)0a0?MYW4cIYrz5n*_H~Mb-ev{AEIh(z< z%-!m_dG0p%P4l+9ZJfWub>sY<&KsP|=B;|`_`+4;=yH0M)eGo4RP%$|3Aa?YG%Q}Sm2zwEtd zTvO@#?!N>TdmStG-g_@7qNwy)&`Tvi8^P0Z`xvf2foOT<%5uA4I<{Mu?^!eI%FWIhteRGnP&98_y;MPGk{7C(=O4gwTluLfCjbF>LHp z`hOnYpV9vf=kSX+{ywaM_hFauYvC!kE(e3`8X^8ZFyMXQ!27^&FO@iU6+23LiUiWW z0-kIjpX)r7$Ci)eGTlaVXr5!)RG)EpA11O$L6aH8kjWH6$YdfY4&I4aV%Yen^plV3 z3go`}Fp}*wc_lpQ(cxg2S0f}pta79^SBTlI;sZ`WeEEe|`3MKsoJg31tu537$ z?J}CfbREy8c}!$cy(crtep8vG;72LM;719B;74(U;HhYMH=~H5p-Ry_3x840sX){3QxFP&hH@ zQ5Y#?G88;~O5gZ`2hWw@VVn7L(FsMfz^Jsw(YmopLIxH?Z!42=I!m1SJw-C$L1M?j zJb~j#j=f|&i{mtz!E|}_o&|Y7Nd^uS3)!$pg8!3nU{ayPpvS?akf|VY=)|Y=l^^b5 z#gV^UwplzA9rtY%8dlVZEgP$(z=LG8wo+$XU@9BP_u#eS=)YaIT0V6=?$;nPtge<= zG*&qgn#*Td46CbHZr@uV7Y^h(35RnWg=1NK(NqRk{5X{*!&%VtBr0$qqW7y%g3pU! zqTjPXQos{`a?oR6O30Lg5<2lIeeREYSPmYxSUzz)9&le|c&}Pw)>!Fe-2yD8y;RNw z7Gu{3JZLai1}sJ_n8*+aAK|&26t)!4}P89E=CW_nA92 z;E9_}&|^6*WKvEGo%obqgNNUJmLB^74UPHAEpZ{?N0UX2wILP~6jxp#bfsPc&V&Ho@j{MJK97JCQv1Q-7QC+@v zfjt}<&HI%T-S?SvmVX32mM}x6B&^VhPYJmFihn7V9Q)mM6LCs>G^|#l3m)`w2GLsP zZUGE}3=Cqn6XQSKAkXogm>B3s#k)~w83@DWYYEf+og)k9AHL5;Z2u=BcHp##9Xutn z4VnCuz{AR)6pO*bM$)ACsIpdix1rh@pZ^V8%RJ1Xhk-cT1p$2&|J{i(yN?3BpwJ2W ztpe_4kw8za&<^%bAba7!RlIQE`aN~v2Fy5cgC99?L#95Zm*C+C-^Is&bK5|kkQ@!K zmELZscDdVF;ilhO>WMQ5ybFRhlI3FyeH47WkKz^HM-lDL|0)dlgAa62h++Q&-~HeT zJU+K~@P1{-_kC{1^M7K`3wmtN3x4z|!MB5z|MXjQ{8zX2lyS+C$Xcgcurmkyiyk^K z{~L7`DlGeReJLZEidhEn3>d`gIB)J-yblxa!o)i;MQ+k>crLJa;pFj_Bldc2>!^5P zEAo5F5d=G5x z&;^nHa?u7#|8IGsZmuwG@cPzQr8>X|*OQw_WGo~cq2}>L} z{V4&rU-_l~f)l^Ee`Yfx+aFu&e667xc0nqNx4gPZSjnv4HansdO+rv%)7<(mN2lRtZ` zr4PI8j<1zpg8BbKQ-vRNK?GlgJ2|umvz12Usi8!?3t|TEg24AM6wm{K_fX>e!5;Vn z>|MOYa}N|>>1&do`%99q;swde?8Q}lqQvyc1@>!6|segK{WcPclPpS2U3_{?}hm}!VT8a{O_NK*LdEW&Q zVLcI!cR~EUhk<7x@Er_1|A2Qtu>a0IPy*P%9_*f52MV8A2FadU1iL-?lzqv*YDB@zS&2~YA{ih28-@|~O16SY=cn<`gf55X3 zc<%$Ad$56LMTR>}h{4<^*1--lmLZ}~3V75i$a$X~4Vuqs4pK?34Tt+M;;^RlkEhcblz}zLRD*pWLMPYUw8>2a``{UV7r<2(aucoo;Z)Gs= z>t-?Q409OwOmb%G?qL5H&S!7jVRcujVU1C;FC^ab~mVHX)Y1q7s zB5ZMVHMTq8A$GyN9lI^*!%XPYOpJ_9*$e#3Sh0yxhI;rK%HfQt4`(4c^Pk5M* zU+^*?)$lPJ(egDJ*7i3Vyc1|RpaXxa>V@d`8HC>LF;d>{HVxPAGLQI_GT|Qh*lKKk zP!qPwqZ?C~4r6=ykFaB`XV_WlE9@HaC$-y_f37ez`(?ek$&cFzM*rMHG5F>%UH=~^ zSbATcv(h9sNR zk?`R8KkYABy|vYG9^qKVHt=XwRRT^^rGVd3A#`jj6FPO2IyiTg*t_%;b6k6i*zSFW zOwWM=y7yqdjqhMSC15C@96X#y3?I%RMhs_wQi+j6$)F@+)ZkqDe;(eS50WiaL(+}# zEDl`w)Bd9MTid%XLtKl52YlMSDuHcNrI6QBE)=zuiSTD3>net4QN(fWD`dO%7yR#M zF_cFM7|tVwjKH%P$pWPjBSuo-c_hNKNce=1^Aa`Wv~dd(?Z5B`?*ics=dNqNy;*Xt zfYwkYWH(ic>|4r3!nQI;X=jPRsk@lx+*`zz_Z6_+2J)F6gZXr?;XJBhB$pB}l1mC1 z%_b^G(+SGaWMcSeBKU{{AF-s!p}B-ya1R@{Ajf?df9IViz2@F_@8y}M&$2lousIR8 zrQA^fY);ZyB6R95;>&sq?Ogf`Y+VQQneM}Rbgz+Is$w*U959vxydxdBOEMvBECIa4 z!FfcJB8ERDBwwtCWb1Jcmww})Bfqq}>DkRU&Z-eo{wELWEdHAh7T7rtVdlLau7y;WO~C=khNOXW&w-YrepP3jdqM%<>?6oCkFkOJ~`1Z%8BrK8_&-J&JtKfX2f}%CQh~_((8#m`liQp&D}e zY%7xNzWgtTGd52guJ|_#4S>a1K_&%QjFj;=4{~_VgGA!NJicTEvO!~6ESJey76h4~ zWZ)h+2Z{jxqJ;AZ0R|LAR*nTw!bbuq5yNu{xh((>Yr(^=EB_Llrp*X01vZNGfyG!r zCIxaaPBh>_wzE8_PzKo?F|Zhc<7l>>1kVQHENCW`qIe3sg0Lg#|1=c1STHGgDiG%{ z6y=yNHDUxbJeU4*pRb1GYqlckuB$%_PBEs1=Y#7V^&yi2*&N8EywAnVW^;Oh2My#& zfyD?#8|>> zhn-h{5}sr}7M%&XFV?$phrd5YB&d5LY^| z7|6xA1B-EnObT?xKqh7;nJa#QbC?j=1p)^18W_woPinxlhfVO58?X;onsQW5j~JHI zBZubFubwKnhi$VS9FMRj#V3@t5*_&V4h+Ht&tl+t5ImCtEC%nZmBU_)6Ms5IZ_gc3knFb|mmZFs%|XEYU+HQ98HE32 zF&KC^==*+BIdp?M*}sjF2)_0gh<}hW<27z~C(x8`$0&)-voI$Yh9Ks%HBit4_goScPtmL*DR^xIZN#S zgyk6cm@NvKvK54lgU064KNR4>bJ=#}qJHB$S9R`)>$cchIrKPrUTUmRKn5W|tFtIr zw=XByd@LiFGRq)hfI);n2ErS59;DvP+1>|+x8xno$LqC?yW#~+?)Q`?3z(rxgB~%& zp%ctc=nG%m!_w_YzWK&?9_#IfJvPNdF9+P=bgHp3;7m(d*oBS)rPe@pg#LI6bU{KF z#7sEMwE?h)f%i~~p#y>oSqz$w{VaPR`un^fdHX#hdj!l-T!S7{okONsfRnfnLNsaFA*+7G@9cYNo&^u}n& z0!~|qYHVG|(z>cRtf4SvNk>Y;!M+%{4>LU40Pe%I8V!P3+ZTEbAp<^07AM}s081e-MfpFw>#BjR_ zs|be)%Lw6|nhwR(AS1QCH4!^mk&bC+rDIO<8CaGwQ>`&DWpAe<`COM*l2*4zqF#?% zf@z;?ymh}E7zFHL48jh^kRxOupnqWm_AX|77x2sjBa$%&++mCo$sQv`a>s~K_M>wN z_BYkK;<5GB8QAvRJnTY3A;t+U!2%VPSdv@qvOMSVU8PP%r>iCTnswqFoqESC<3`6! zi)K+ep+%TRZsVtt+wD^*9h?+OCp($i#Ym!d+a%ibP!ef9q(nxqbpi{-o>K!+*us0M zm|8(Twk)X}JE5$>j1>{v)dwEv&%Z-dKV#bE)}N0?-yZM)O~Ee ze>=9ytsmPW9>@0C&tNB*FR+W0FR<%`e=oUh@z-iS)89858~?J+-0-Kp*7`pjBI7S)04xT%v1#eU*9ht7~k{FK%!&-f7!id3%?4>5V@B{A&}z`B!Gbxs;j>zh8x6 z0WBEj-jAtBCb32K&#@KE&#`snudyvw2-|6j)b<;p6-V^ZXD4;h=CgOvjtjTZ-pktP zu*OYv;<^?(tEGu9-qJu)A*CF zXZ01sMEsaV4C%FHq&M2xS5)(aP5*rk_Os{ERl@e{hI8nFbLcJl-{;U@NQ~^yC&u(= z!#QM4_j9wyQBLN`|9=IS+|Mr*!r?@jzwgpq)dl}EAvxMu~ zUCegxDVp`*(^qKY*Iz&h8Ymzs2lIe`Wr5PbM=~k8KM_2{!FkLjBv>^cIc!;t?2qh6 zwpTTOv%N|FoTDQj<(kE`@TkSr4z`U|0)BIaz_GPl=+s`y2M>1g?qXZ_-Xf-FUm@LR zpn&Q(2uyA;pBO%r4csT47&({%J`zdM199LZh8)v3mymGfd?dg<90m_pZv4v8q`cwY zb{n=giEreS%c}+K#!8`mbA=FikVrbq2WOd-tsAgGkN#OUNc9`ao8^MK#Zm)k0%V4u$ilC{om51UY&@2_EJW5-p#Pgqv3* z{=oyt?t<38c$a7|`PaPq9Q0G~35gG?MYP6BM_XW0{I)W&Bc2z-nN(i^*JU7|EG`aJcxK_Z~p9X zj{aP5*{4UKlTjnGZm4#o0uN%fluPU(qa^CYGfIUH(*Arq=b>Dd>j-3%#`mymcFc-XiGIUG2M_@{3EEIiG4COq%kCAyPc z<7nAXEhaTpN*FEWQf~X~{vhz60-EMOGvU*6*+nl#9Z&mpINr*GJt@dS5Sl8TY+B1@ zw(obfoFJPcavaR%Nk+45WjG6Z44I%O(U8SazR%%I27m`Y;6Dm#)S!<|bibEPY~Ng( z^&s8=9`+qVq9eC{lpJ9`ksSAL6KfUJNQ|LR$`bmdD9{&!chRU}@&1_{A~x_<5ty|~^K=s^Mg;fm)^=*m%NM#Qi) zBWh5_jP7@0#`ex7VdTw3D{{XufP55l|+x;g&t zD1qG^SNmfiY}c(HUAMERUAG4}I$tQSae;miH=X8k599U{PwVaiFB(Cvr&0i5?JgV*2J1lBt1*&(k5b^MRJrwwZ+{5l4_MY;uY-L_=*ka#j zY*FB}tuSPg;}AB+aR?vb@*{`rc+mrM2{|tW4{O&T>9)hjedEm^eb%rZD^`XyxF4>n z@q%tn-wVyUK$b3E(7kOn`( znFC9t9Ayh5hHV8=gL4VFEKotRHEWSf{V?)ctM#S-GS)=ElHdlf-Bq=ULk-nIhnp(G zkGB>@T<*(>x;>h#w3>^GOxiewjngIJ zqYQE6&|E@tRTbpCdM%Q#KlHm|#f_O@HO64DTF^tk4Hfr-)FA_~p`k2pXJ>BQse$CE zyQ8tQdl1kC8A_XW$Fm0k)aPtqA!L6pWFDl?&4YcPSo+R#hbf|4=mc4=9HYv@N9NL> zt|}_XWz|~9vmX3BNbUNVaz3pyT-CoROtqvgM)h80(t_sPB=znjxQ`OL7>2`V_aJP> zec^5eIWPzj+=FQgc?@bG^BEzC2YCmlCnlkaY11HJ4!$9imR@1w1b5}=T>8ZwQ$emP z*8Ztjx_?@!ai|odI)WX6vCdd3U!z^3v+sGr1YB74-c6%3JRSt^;M3|rSCk!gWIytUIk-& zhGVdcFn^mt247r|h^1sCVoeDtYdRv54tED9TH5>GfAq_zf!w7yCg$U+; zyz`L-cZE>H7>|fb&XkqXVbUyII%yQ{HlZKkH?A8VHa3^O@Wxbr_g=VWDgxWynS`CJ z%fPgYvM_d91{NEaiPb7o)jI-{k9YYdUF-Hv(CzhrJqY(W!t4$N%sf&&W*q4}rXT4wr#@F;DzMwUqAwoXSf7FIF3rbIWE5bG z*a9pNx)3saOIK8Q zQH)V)6nm5u#T_9;I}BS!i$+YNo#xa_e@taG3R~2eiY+V8!&YXOVkhD%FssmNOsc5E z0_BaWvC@a1r8(Z)pCha~U*u4xRmLyTtK=7&*6{MJ?{V@74_GAz*DzY#vw-j5NP>UT3tH-nz%@|4Ef$_vW zs?NN@Mc%f9YXez5yOoU2lTq}xD{-`D?L?bK{S zq=LIu)I#kldeO~FcClt9Z%#o+E>;Qie@;GzCDme!lug)H-%jj=Yd>~fJc8-lPtLPq z%`Bi(-z?=4U#%5bJ=-ENpV{YP_UMG0*~BGJld&53guXd9oE_cx)FLc^Q&86yi49m{Iu*7l<3va|!{kpLguEUtR;}kH6r@$ay0fYE{ zz9s=Jzhm*|X9lLfs+$`9yvx$yhl51De*%B_uXD6JUtj*4J!o;SzP)XK>5U%$!YdQu z*_W1%r=F9@`J8 z4;i7=$Mw;M(>iGD`8#O$C2e%@$}M#K8t@0r>*%7^RdhxB61slpJi4WG<}V%nlYbf+ z9{b(g`0%eJ)47xg3?!@)!xT*z=F*2@qDc(nzQ8cr7nlm+zpw>n2wQA~u;qGa;p)3+ z`8sX1X5&q?e)A2azV$lVv0Vf0-FX!q+IkuLuXdsfA>Gxz3=W`0vWrk`WIBMe<-A{8)>9TRl zZlib>G?RTw8%e$u4TON|2iC#0_pQR}>MSGf-?NB$P-~u8UuB-sP;Q>qP->A`Uu2o} zARm-7hY(}qLS*wF4>u1XlD-Bao0@$`A#y*bGF+!^?1DyUqS!vVEUkm)n%iRIQQS!J zE^i?FRy`p3*VYk(>h4*GKB%>dXsEG_d01_c&{$!S+*oRn+E`?n@i5;i<6#ac%POaZo7{Y{Zrc+icQ(oA}a4)Z? zdDYa}_|(->{OYU8fsIw9;O0uAvZca0sw5xhs*}7%@(4 zMvTixk*$W-@0^>&uWWV2Gq&b|gB)sN8`rj=-rk|6nkNP}=G<2Po{M#su)Kf``u2c_ zUYrXS5~KTafQ8{a5I9&*0y(ZH7Cc0g$n#*+i1bn7Z+dG-Jk>n)@P0uNITkl}qC^e)&$5@e9v_?dZf{ybrxz{pqeAxJ!EVbE#674ES+OHX zx?k&8*#XMuvcoQ;(hJcolDow<5{rf^DY>aa3Oi#?ybkD->cM$XE>|{+?}()U`-p^n ztWa{;IG(-nrA7_-*u?aC(c*eMY4P3e^n^}#dSb`NgzSNl+JguC{pyfUf<3L>H-3}v zA%8C4Cm(e_71J!eRSLP3hANpg@ECkoirZP_EbPs9bR5ESIcapanP}*P37%yzcm^h7 z*pn7D;7*I_bEC)gxH96qo$Aoye2lXY0w?`dG@ws>Vjo;jNkUw|d=`tce z64NBRURER12Nq%uEQAbwEv&9WSAIXRm=VatJW6J`z6d7yyuulbr%mX%8}Ju7BWl2f z5!2_)jO&px%@qp9vz=OlC&BzJnUbiipzj>;Y-g>FKjJWTMX>_?< zULyw<;tD;b?pDxKO6xB0up7khamM>{W7+aAJmEe!y#EB>yK#c-iIf#JAYtL$Bd$lx zj_($;6S~Cg#LkZi@mHuK-sUApuwyfFRzHM1H(vkUXFc()Vx9A-_qLcuw=)$rZkli> z1h5b<=;!n%_2zlAhBG|*kMX-|efi=a#I{~MfrJY~nnGZ&FV_V&>O_VzJ-JiE9ao?U!5 z-!7r+V?qwVX>kwY%^Q*XX9tkqDvhs$ms!n(E|!jkEQ@OL-C0)ad#t`X@Jv%h=#{o2 z<(p;;n$QsO8DuSm7?qTC(InEp+his(L19WjrAHy-W_hUjr zV8z1qOOV^jb>9Xp+&vwwa&9PLoa)|9wWP31KuCwq1fyPj z{+GkNjnDpEnEPoFj0sW%cfukia8`B97A2A4y7&i8c95Piz4Sq~W0$k2b zZQ-*pY}r5}wyrq~+X26Su(%YvlTnHZV@t8H&`MwcW$Vj4^N!tj%g}6gNipbfPPFQk z#Zmk5*&pv(WJS?O@R^?&%^R_Zb{sK^b{Wx)_8GYy9Xfm`B63JK^y5NKsw!`NvH4@s zm|9yJwydrITU}a#?aHjeZpYPN+|YZNx1wGp()HnrR9WqwJaNVO5>fH3Dq+6SJwc9D zyT5@RTm6X_z>K8dSZ#o2< z*O`E+)MaBTWu@4{>{@Jh`~yrg>>)-{v|vKl4pmQapIWd(=cXw8<|B#RhRbQTb+@zG zHOBe$O7kLexn-$UDWSr&m|SI0NUPB)w5`2e=umsJ(7EPXfk*A70{?sG^TX;srdQq= z))$Fk_hJ4o&V?Dg8dFPXz_u$}v2(s%n67IdMiLLHa(Uxw64ul*H`?e{1$E#^5T*B0 z7^zb`(z;zQ*1W|i!KleBS?{4$+U) zO`jcg3s=bcd>bL(lebm8W_{;m5mO!;kj( zepF3x2E{P{r^6Y*^T*)NKhL)xTk1NFZE&2ycH6(ejzJFLJmpWd>(*%1ZS#L`&^P&Q zyNTh?`z`f;I7Za@=Xt93H&^JIUum+gesP<7@pA*-+1KWRQ!mM)W6xQV!_WBA15ahL zeNQ|;s!}+EbT|We|FMt;4D;#1F!?y9B6-$iutz*%(m&`ER|{$YIM_#vbN%)#W$ZbUe@12HacLw1)wrdVJg z!4(+h`4GdL;0*X<7zTSV82K%RS$ujJ_gpN;e>E zLMJq0>~2KV=yU67Jn? zOsM^ckQI0^*}nwof(N}D>d0K@FtReaf~<-9zgjWK-&)z*zq1g@UYg0go|w1?Od0uv zjvM$#jOqo)4C^Z6hIFD62JXfu_1{iS>C?_m?YmW!)_J=ut>sQ->O-BH)cd-1skJ)w zsg=6*sTI2QDIXHD-ntN(f(N}*E0Ce4IgN+59o&__3K5Z^y$HdtMzV4)~k=dmmo876Gl-r@3nb)e5pZ8F&DF41eSze7n zWnQ^qRbDYD->^D2?;}ElEenzPzQxG!y2Dv45h6N>6#wEq2 z#-$~N#%0C1CS}E0Cgnw$9}yy{FGQAm7b7F^K)SpY(XJgubnR=1u5bFIjRorq8>;j< zmE$u-6^4yao#Oh*t|{F_udEJh-@I1Kz@ldJu+m1;sPc!#36%{-=~efPv#Kgh^QuZr z3##(W3aYZq3MTAj!UE*Ux@-0W!`Sv6 zo#_0O&h#6n^CAc6lB8~$OJ+OOBOmx+Nh86(qTVW``o2ZPy*jh_`!!}M56aCm9u!$* z-_NnkevodNeLvYMr!K)N_gHQlGNg6iK|Mh@*LAx3xQ5#zfufcYc= zdx`@N7ez_x2&W{sg;7#kLda=NA>{PN4+&9$Pm+KQQVy>~oFiKh|JV^EICTvPE}DJk zaGm>_r{gwdXC6AhwMpvWa`PJPa5g5ZtLC~jm9xFtaV}R3Ob&QlZ`R)&EEZ=^l*CRY zcnF~;wFgm?+XAR5tpPS47P0{kWbi;gxDwePz;_1^Bk@rUBtC8Wo#Q3$Yk`*gB;Qck z&m*R^@tB46JbrZzPx7#wCvPhSHit7YoQDAm>rVj&6$=b10@zO|u%96K{Q&rVKbz!M zUz?N`#Yco_z(^@O7a`WZ6^OrQGwet~Z_k0NNOsiZd#BT!*W%0WlcGDyK7n~!i-S$^ zeFtt`m4mPu&y1CD+`93c7@o~agFF1eMVwVGvt&y zig>cVT)O9I66|9s$>F1cl&ArPO>D0hExyYGxQiPjsoj;4+$v|Jw7M`;TRtX4UpF7w zfCu|+%aH`;UbjvAkoV>bNU=r#C&hNgTc6!7Q=Ugd`&>2Bnw<5@YMrbhBg1Geb8_g! zc~BAbz-Hqd`yS|9%$PaA-?)D zB-^k7d9K@weAk^r0qb@D6||A|E>KX7?KRj$P5Qdiqfyt5{W zDSzn=U9_(BurV1kYS4)l+bdzkcS+cZ9b$G;yO^EaCT6F!f?7T%#Q1DJqJsy)rlqj2 zwjTMc*^L5MpZP0v^_^eC*HFF+Un_nRzAj)Wcuzu`|LMXyAFcb<3d5#SUvg)Tx7~P{ zO#Fk8>-S8+4IOvjMh-b}WBLT#_-+9=p;N#~>JV^}+i(dvDXkw9VuA<8wna#^ekpQW z{TcFGvGdokWv9N0UV7_w+!Df*#3h2qNlSdj5>`ib$8OJRj5<<&KkiapdHn76lnCOu z*vIBA-3_`f#UWFWbr=EWFu(=o0IZ?Q-afI@-ae_FXP4Z@w@Ys2+ogO^hzXpQy+sX) z*DitXqP4#TE#Ce%dg1Y>i3>Czr7koZPgmm%XQ{~t^3;O*OVwi9%a`PpWp8arhHuYd za1VkIW`C0=4wdTa#=T^xtR4lX~boy$vZv-^k;3undf zO-s6JG4ffw`ipSYtxpqF4o{}3UKz=ruRl;QpVCu0pWjuxz`4FkCAzFqtuQ%fUqe{( z%`V?q!XR`GOcFyl(?(&6N9GF18Ms9ZlY%IWs80YP+j>f8rw9Pg6$j1o_DslNJXo&Qq`#S{(MSp?L7CQnt8FQ)hh~=`G@O# zQ?=XOr&mTjuko;->h=XF>ByK?w^)q(`}Vt+h>v>?zNN)qf`}wpTwOuE)qHq07`W zvBTUgxy{@)rS(HXT;P+^g(}dOqcR$cE$GU?7Bv-N%j)l`tf*|5cPOtByPe#OF(O)3 zoc%i&1iH1ai&}8q724&GL@-eWVc-PTQ_VTbaG(biP*fx98Be2IX18S0k%4?89SWZhG|E1 zU}XPpRiRt|LQir3nqYqWff#P%oZ;pShdk>#JO z_AfYI6B_cCme5yNI1}xk-WRHHf2xkgVen>AJ&W)hP8*opC4x5Jjh|B!|a*Rgv|@@#g_Yz zU|ZZLvBQ!l*hSupdAC^KFE*t9w8q-{ztm}FU+%RvdUJxW_u{hn_7g3c=3`ygD-&j( z=SRtkQ^U4?$A%;UhX(xu_6?=_?;5NM+}ht8ytTJCoIiw$=U(c7XYl$|(w3ddV8Cy>WuZ+~ zMFIy3cz|IZa0b#540Ct_4B~TO5MKj>K-eN+5X%kG!Zo^R#roUmv&}cr=53m@{Ncc! z%jo$2bLh<9>_O|q0c3D`53)YD9kDN}BgvJG$X8<>O1i!dWqeS{@E$}!7RDbqkQ?lx zh~W(EA&0?ufnlUCFwFd041)~DJYWziz#vq$(4qy`(eg!C(b^@K(1vB_k@||$XveA( zXz!ZC=+I~T(Wwo)(dA9sk@gmKWU_4o{6q0G#M`wRdF)+zVHo^2b_1jVUM_k!m_1^uzU$3e^5wg*FvOqbTPVoaT&UNeI>ee zXEV|K83nn;))w+kUGN!29NEnBc3+QI20;N|3(0kS2S3 zKF9gIrGr<*|}ENsS91MBUgIm{ThSvZjBLnhsL;TtH$I9g>=Eg%_ED^ z74V>;u@c?UR!90q2a$pGd8ALjiFECZ{|E~)01+)zUwCZ zLfgmb&CNj9S2vU%FRn*>KhsF^eR?&+@5$wYfSF5Gfm4_21BW%50(&%D1KPDZ1DmvZ z10QJh1=ZZ_3#`1=|3M)G@NnnQVx)Oy8PYws3YlwcL4-Spk+sn!WJS39Z%c;7&!+Z_ zZ;c%JU+TL^-{^R`zP#h__3T!N?-Q-4fN9NypvTwJL#8zH!zQm(DMvK!D|>G=DBEr| zhc({nP}XU8hF9M13@^Rir7XJhVIjjE3(?(!i;?!pWyt9CDntYhLV;WiS@#kmn(6#$ zO|kmXio^QaTp)aF;^gwuz}@SquEKBnZcxyac6ivt&A9L}t<yRNf^ADm(+W+ zJh}Bwb#lGV{iJH0hLjS$`jkBVhU838n!&@Q6obaZ4+;JnS%~xw01G<26cLZCM2u5g z5bNS0#MZoom^!+@(@crqQOVpd$hJ-|NkXp~D_P)#nFp{r-qysFdEj$>}|} z^Dzu=XbU~@t+J!yLU9Y>^aq5@rW!59kq6i9<=aI>@^8Y>oSbU z>HsF!rkk7pK)<-4%CMrKz__9?)1;y>*|f4C&a5gg+N?S^(yS&s-27hFhlI?4Pa1*; z>%B{6=YRVHn~}rO14wY@0&=)|_YZq*!ar?|?A|ho&d=y<-$@&Ra+o5E?<07mby@i5 zw3#RiS_~6Q>J4+sDou(?^G%D()6I&@6U>XtqAf~F!!65-!z{}SLo6%vgDk6aKO|(n zbpbL0Hi&Z&&aUOiVb4b7cwiqAA3KM{=WhQgyk`9!|1S3}*Ub4To9Z{gw2K&|ixa!4 zu9LKSEiNFQKB~ zLqZl?79i6-i~gRymm$aP8=$**4{|xo5w4~2OMv4hO zDWczwlhR=;&U;ArsJKTAdQfhe)RYb!E{2ra9183!fRgdhmzq`YP0gwEq~_JQQwuBI zsKw>3l+uz930bKxKvp}{5L;ah3Gr{v>pw#t8+Ra|Eys{zmj+TCH2jzMN!mX=E{R^c zXnRdc%#;H{04zL*{03Wjbu}fhDW8zk9z#i-X%T+PTk*w50JhMJi&Y8 zcgc0y^KQ_vi&Z=XDst+1(&}=^>?HyR3xfQS4_I>t?&HcxZFXU%HOZKn^-heOIw>Q+ zMnW&Fl+cUIKO|%gjFb%jZP-C(O}cs!bQiBe0jtze*y;l)5?Dy|=39To>@fc^dOz!{ z=o69`5tqFuL-fM?739=L8NVW*?bGQ`jU04i#CPM|1!pc2c51Vjoz~>Y&TMdG<hs1!1b%0DDT01r06i+PY2b6&276iZk96}oieA5qKq{5xUQNtCko8cN@&|6A5J z;`cfGIA0c=lsqfa^5{!7i!KYYDGhaU?Bc`D2_Lc+e7nRBJ~z3|ft%9ez)fv-;6TL8 zHv2x`Hm{b?E~w(OipoDEL zfioMpcG>pwy7m4(49uxnTa6q4SnJi&I@alfFRxalYD~$CZnZ z%{*Ls?u*W~S6+?my*mBqxNgtTMZ2r=LdB47YOrMo&TsPS%vBO53 z(n=SmH8C7A>zVwVIwmi#hGkz^$+9ah|Bw&~&VUUZM6y`rdj+QYJOWdDl%%qJJbV7S z(b9!m#_AXCn(SS&cV=eAfzelMuXR7&V0mw9hai3ESb$H%?KF0Q)jhp5`?f17(VcgL z5<84NliMs@Qkx0VjE5v~)&q(t_a0S{Uv1-1SYg92F8h!Wc>{15nEz#}^Psy}<#9AN ze5GH`P#@m>w%ebPMxJRuf|GTSV<5SmlK60r61C)2m7Te|Ag5a z?ytYH@@g>$~^W_rJ z=x$$j*VRbUL!(rK8baQU3U2YG3h$C*rKuGsGYcy&#uQat3n?zw@+&F5?Oj%^<6d5< z=UP$lAtA;})gPo7_Qdb~_x~1{J0HOOUt0xpaEpp+N$>nM*~9bqB~GedQcf?>_j$UA zDt*05z?s_a_P^LV>$obHsBO=NO{a8scW=78d(++B-EB|;qGESJL1)Cs4CFA| zdys!@#Jd>w6Vi>|v%L@Rgd8HgRriTx!8lRNm}WAH2X5PlUqbHm&$0pDZ#BYQr%dB* zp4z3FKJ?DkzZYJhaVNb*X{4rHW@Jm5_|UBi$twfZ(x-3K${xO6FTeMC!++@}G7$Vu zSc2b&Le}CuEJy#{f&P0Dde8bn!ccLSu;-5x!SvTeIv$u*BY<0<4nk%=AY6uiCdk<11Z$8S9y!s$Zs`q}*f9V|N zU>D{9&q0x;=)YUhe>a}RJ_PJRD87w8_#vUDOcIXRUxYslsN#XZCFcWzs_r1Eh9~WtRxayNP`W*n^Pp3>m~L!WayM#Si-+Jb@k0VayY901J!5joU#AiniR&7uAtrG37TEL zps~XLm-_m^A8OlzzN_vF{;qO7&hjwZSyi7-)GmU{LM^`t=^5 zyU6#Kc1OTZt>r=AHP?oG)7TpJRef*x7qw#%pH+J!->F}XoYJ}x`B-;2a@1fne8}ig z#0}b$s6N_6)Fs+f^hM(re+ev=n1P04Ffic&Q!_3wx8ea?7csE&R|MM#9k5C=1+#2B zFfMTeqZ*%I1`7gy=(Pub)ma+$MSFGRC#}s<)0(?uK586^{iuFE?v>`1xMw;y;>Prc zV@HffV+M>L#9cLh9DmW|S;AS*O~+C%n2x8NHXBbnWgNh?zO(3Ggn}zzzk;EY}i)C z4ldf9h&Oq`-xB+SoRz`f&j5VFt-&L~?U!?Az*qaC@Q>D2ac?b}l3$s%roS-m&VFvR zI(O1=YyO1(!Gg#7XA18cT`C+jyH;?;VzB6(XeQ zD)*^fQ}MWUd-(&4o~kj^&9(Q957pf=J6(U-rnl~l?bU`u_I*t|9s8O#IrTNIakf zJu)cteP~?b6g?y7iBDnan0s~2U8jXjBX&y{4Owk&yKa4`{fyJe_Jhu6J7yTz8uv3@ z%RJ9^c6yy#+~(ce-t2v$b)nCt<^}&DaKSNn$gw~OGNCx!?@g8DfJ`|7$X1boY+X&r zG&6%VN7o;TKEdy!!xE;#l5)oBx#jnK%Iinmnik!3T(Wq;cI%Q}$AjI6u^(=$`-$bN zJdgJ*@jkJ<&G+QeMSiE3%=bIn+30_6alLpd!$7CZhMw6k&rr2|g&4 zlY}BQH7L|KhCEBhUzsieACvs!redSB$HUXh?*M{oH!KJ|u)aR%(Aw&tBdaTekF6*VKC!Gc_*8dE@R`n%;In@T++~=6 zjtpX^7b6|_d<%G)p`4!!YD9#fURo9!l(nEi+YD+=T)$M>2fr!uN_v(VUU)A)wc!SC zDEE4|t~uhid-E#4JzLuXcW-SB-m|49WZ$NW&;uJv!w#)24m-N4Fzk3wLDV?b)-dBWptUeB6b zB1fx&GOp*w*PKeo?%WkxyM9f;>K!dX+jrF>b1DztwY@lE&z8c-eH-&553b9NII=n? z;@FDph!e}RBTjW^ho4?DSHH&)ik>qOJ!cL(1C+Bf{%GK2{ngCN108}Qzq%!4KP^|# zc(X!_HnGygZfu3K_du&(;_0gJs+~E>ojr*~8|FtX-dz^8WnV$`w%xfgJ9cKr?B13Y zvv+f5%z^b8(TCThM;~389(7`Qdeo__Kp`WQ0})eGL9~1X1TY4jq~LWUcTr1MTN(YD~pewp($KHWMzF~t5f)v9`C}AmXMax zn#8rSrDePQiZ&ex$=SL$B4fv{sI*<%qEq&6j!8POAvW>wnz)3cE92si_r%4WTpAa5 zs(Y@$PYioxZ_Lo1mWhqtma-#*OF_V^qh$Gtvzj+<9>r7oN` zH9c_1HgNSe*PO;xzVkCWB31@0%-Lbvw0y5?#kSqPg*$f!=Iq`UoVjmvX!?Qm;i-q$ zL?j;18-H1{mrps``! zCR+QxP1X(jH#$}vT<=zVc#UWN(N#V<$9w#;PA>D$IK3p`KLnvz3&{+W$HjyUH4t*8 zjgWKbJ5QnSJh>Mc#2G3%J;+8*Jmq2C|42e)^8-b-rT5gW>Tm1O({Gw&`CfCZx9AP; z)I3$STn@dq`i8@+4LT36F_5Q0@3ZK6j-lr{guZV-`o8_=sAS(DE5pt)4$gJY1tgYCN@z7r$k`S?Q3{M5 z*G%@jV_jl-J*Y|dQf{l##r5rSM@}zM-E?xP&eD_1j9N}EHETTGZB>1y%eMS%r$fp4 z#g4`2+vf^mSSU|(k-^|RoJa3_6us|E55i9LUE6T~eai(#vT=}^an&drPse*+>8AHW z+9mHLtkd4h`Gmewk9K=xo@H^{r%ZPssZOPTQG?{Qi}PiVUTjg?*4w7BvbSBgv$xH# zxwp-v@j|P4?ZsyEnoED_dlJ?G&chX)hhsPod(iuCLEp0hea{;7o+~d?$kJO>YTG?# z-i8mHQl&rnG&6sRn8p5*b_twTqr1K|jWvJlmacm*szCX6X{q$>J*5)YZ&u2j?5|bW zeXT)dLtmptPhX>US6`!E>$OJxmcR8B8OQ*xhf}y7_Tc{eCS*Zta2|U0<5`H)zZnEs zI833|jxq9-d|?;Q0$$|=5Yi6?DJvgPb#?w?>~H?oF1hW~$71Tc+5<;Y^7e z==0CrEtEa{g;ZmiILuIO~hRW4e{H;64Kzfly?Q6yO-Gr<{|J}V4W25(5cmWy2 z4LlEUhcFgCA{?2(DwqJ=(&6a+{Xs&{9hA)+@ZLu=uyNJ>=HsjSF)m2+Mlu zj~_-!4o${LU7kpkIrSt(_Rx5m{O(5?@;m=lALif?=3q0{;!0c(cn*p*V=XoyhpD`Z zK6D6s0mcX=ZIUp@0W()P-oFq4{F0s^rr-n$YBr#)YYe6|9dNc#fnZx1$Z!_>)#A?o z=m#&U!4G~?S3c0C&%FBolcTu4K z&PD&7K8ijZdjO)}5lRR!;C&G6?4B@>*BOKbZ9q!Q98{zXK}TK_%y9$CO+y+YbVZ=h zkRMi>@WD+p;SVp&#h-mQm%IlSQo~>^bqj3fYCEol6_|sLHP{1!9H{CDo&!Puor(TC z@ecBjaY9077{oLoUcVUxmBA4>sW!mJVg|zOh9JqM0}8yVpdo-J8Na;2Ra^)n@lwrF zIZoK9$PI%k{P0{=c>0T)*b7hvN z|L^?y|9T*ZDUQVmn2|qlFlr)yPz6CI1rYz;_aKbyfe&mrI3b7|8v&6!tinx+tAd;` zCd>~z`zO;cp;NABLQP70XHTB5vY7-0;PWmbTpVjg~kHP=4_y5$qwrFoS^E? z1Il!MP>K}?g$y~6D^>y7T1}8?)&=P$1|YSX29jG$Kw_^ch#xlt@eAf4Hedmg!xkVt zYVlR>p~XAJXBJZ`FU+54yfYiqoVK{D`PFJz^M}<tP>YfTl~e^#Do_XcY8{YUU;wh6G>}9wWe){wZGa8YX7kB*MYwTdN>9(BUaF&v4Nfm zI~Z7Uf}SG}==unOPM8#ECMkhht|ll~=!0UTF(|Z~f&6kykXw&OqjuPW%pp6FK4TBk z*Bn6hrsG$IJC5&^#~h!lKXDk>p0Xd+dFwc&Gwpms_p3{v?oZdtdhnM(UyT{m4Ov0Y zfDLGd>|kQb3C1=&VC*3bv>+KUh*JffOdZfDF#@$Z{5WW~2Bjr-pt#x*6t+5n{C+$x zb_!dzuDbnD7;yinJmUURZOrY7))Uuzx>IgLdT%}Z^{2hA=zsO@HTdd%#_(@}fhsd- z>#+b$2N9YsJ6IWVf)%nD8)p%)@|Oj(NOhp4=!0IqDQHz%gT{OZP+RN_sy%L?ywMYs zc6)*1aUW2)?E6{yhVL7-Vc%z3_kHi_Kk*(ic;SED;BDYV!;kbcv=2eYjXwk(Hu+m% zsKN{eS}b6$!3uWRo8zd%0ZvBT;A|@bPM&gL7oq`{@rGcMX#s{M*qT;{Y_1vET(>W% zul4_>x+Cz1%8{V&%9n!Qsr83UY7GaE>E90;GJG0x&1fpT*Z5WB3DZ|m2hCnZ?KOWD zxy#~j0S(7stj+?qDlFiF48TL3{rCAGA8ax6a+U*ke@$?Tq=8L}HJIf&1Fg~%^ymBk z)Lk6(O>1T7SBo7{ZV^U+dfx!-jS0nb5D`io5UnE!QD!m_hUc~DzBKTS zum!gyk6-qA0beaE!lz9a#J)4?N_?ZYKJ}H(!So583z>I}Ze(6F8_7CraWD6P^=SS! z+q(tp>_-b$IF9Bob-I_^_Lc^#d|SS`5_U{ z2*p{d;(0AY2oC(srGC1l(?8o6N4>YIPkd#%IDN`+ZT6(z!Mrhpi~0Q)H}cL|{!_f) zX1H{V{ZQFzr=hZ?F2kiAuK$#@x!oyhcDq}!(0w$2uD~4Gpd0$VKv610qVJ5yb1A9F zLNXM1u_r|g&j%?%k{vQ3Pb&xwar^0)K>y^H6Z6KtGG)r5Is2LM>cS@mM@nv)UM}sm z8YtgyH(0&N@kY%Gmm4*mZUfb=9)nd2J#SSsc@CE~c#V|QdEG9WE3lSl0xxkY1PkH$ z1ANaxg$iwZ39CJ-bcQXAF@9X?NJTl-#Kx*_8kD|0Ohlc#yR?Et6 zn(wK)YTpxyQQjm~|I%iQ{!+CBRk7kXW9Xz=c@tMwVEuJ*lIS?N1eUg0}ZI#*yf zGY^=9FpMA1%>WrZGxt$ap;!!iA>{a=OkLth`$Q-0Xd6QKRqWg*ARx=U4h)YbXoouPX@{s4fn; zSy>b?R6bYWC`AQ-Ax4PgVSps`{@EN9ynm1Z&w?=HSro3H^@>8%^*S=IYt1yCRXUp7 zFYHfe&n-IK38y1e$?|a{#qc!I^8VI z`?^(CD#R~BT2UTe$@>95NS8K}t!xmh(=;D)sjG>-vd*(i{~KwcIzkjFJl zWMUyZ)oAy^0T zRLZ*?LLQ?3A8lYDcUzgs=rT6S^ELCRk2XuN+}*CpIk4VT_WUvj*FJD)W$~+ zRL>Q73o}9(Ck2uSd5Ye9w33kFCJMQ=m`a9LvXI+b*~#dBe#))=%B)v*8cLklV6DG% znXBu{R^RAF^bU}6tc3_R%5*B`pa|om@4SJ2<60L3xwe{#+}OcNhE8yhYsclN z=ML!#AKYcGxoM+=eOHfXXzk*_tn3BR)seNCEuNM2-S*`>m%9|6TI!W^rpq_u{NjML z3$66z%Zq}Ot~P}vTyF@Czfl_&H&8uS;LpbZ(NxNF^xmVT7{4C-4_XMh&`lw|>ln#} z-Av@_X%=$foCM|gY0Y`Njv6bi-fL~qyw%yibd67P(z4J}|HaAk?OG~3OcrfiVqSly z%dYHPhf6_kn@7&Y7Vpf<3w_i2n*39)*9RuwsHGRAu@+(pd4jbtRDs^N3HbxQ z@1Mf+_a`<`$O&W+r_L~u6ITSt{!7Xno6qY@FFk2y*l^g+EqAwDbkx>>T=(^{HKwbJ z7iq6t)27|k+h*K!q1CeLVvBw8XjEPZPHIW1_mGV zPIfpHQDm?$zgBhkibka^eNCE6uPiWFbalRIZC{gB`LzbS;{H0v{DB(h+`%fB?3sckEDh_R1oMyca17^R{}S8-Uyb|$8N`-D=tX;}WaCXXvi6QJrTexr*Zg5!nc|zq zhKV<oBogVpLiH)?e73TvaLfg01A!77XLo0Zlj zL*=$b!)0^zBbt!0Y^*_C4=0hs?8AB3jtpWWGKh7%(1&9mM9)PkG6+`EF+PuR!6PZ| ziiau^*$;FyqQ^|kz3w@9Sd0dSY2L|7kRM)^DtY5khV+TiJoz2Bi&R#RlxTL1lO(;XJHE@3#`?Vd*~f!MOk2dW8WQ1T(3B z$we)DAQMOAf}YX#tL;Pf~&rpOj?$KIy1Ae>O8P{o-t?@iojz=4-j9(5K_R;*VeYN)5cF zOP_xeE_>)@wET{#Sfve<@hYn(6V#SX{G~yx#gmwWotT5Qn1f|li;K4)13(T_hdHRY zOi0ln_Cef5FZh~;Apz#GM1UwyAjk=Og1WRl7%3Tp4ITpaHI#u=b0Jt_Bk=vMz4)_F z_EN*&oTaaP^^iIB)kpT=Cx7|f(}4=xKGNr^7uVw+tb=u!gQZvpZO9;+_TV`XWKqS) z0CM{=_6Y99k0A$mOCh1S*B=0aEMDmOok5Au8ni`>z*tHH9OdO8L{%7ywRmBVp1`kh zL$Md%4Us$0qz1uE`Z8Eap9LG46JR%22a$zsz_qZn3v-YDw*lv%;wXCH^LP#f{cj@r z->CcOgP$OSct67+nD9ObZe$Qb$RH$HOhJKNA2hkufW{{W&Uo=rq&PQJ<7IKZ@_g`A zQTW3zC5gwNC~+TDB!@v=asV{vYSSXjKdyz=RTv-FV%b4_cH}@QedxiFL4@G*`(u2M zDP$1SgxI0awgN_Ch8~*+^N>LZBZH7)QUXO5DbQsV20L~h2;*XgYF-MQ=jVjScuDvV zA)${TEINr7cs>E)xmu1atPMF-J+6Z?Tn9NPu?OT5K0DSx5Y~YAI3X^`VQi5>n13aN zhGY1j{s*FgV^IM%Fb6BVMlJ#z+&_VR zuG(-NG_1$?yD>h_K@!$LB(4KG)_@o0!08zwHZPHXyu<#BFaPof4IGCGN&zK{l0b?^E>hWJ{L=IKH1M`ochsihxk;s7pZ(fpf~+kYNV#!MGYwf-#ujz7*_$I=PDrjM)|A6wDNoD?@BK)_EULKeV_p9 zql%z0qV%^wNrwsKjF1NzvVi&wk3lA+Y|jDmUfduR$_o-nLLizi4#L&aAk-`ef=d-a zV7)T%?@|T+V`{*6NgeoZVQbK+1_+I7fas*g4~f?r)6yT+U&(({d#d~RtfOMu5h?mKMSd$Wn zcB+E#YK*r{6NCu`bZZfCUif`ywZKG@ImL9@>iV)s=o|wtAWuV zQjF{BV0>80L{1xma6evEa~nC?1KL;Fr?j_pI}j zs|SuJ4Z!KJAvo>FLw)<^2-G#1z)+b9EEJf2_oX;0vw))}JJ=g>VH=(RSb9r>ahM|L zCToC3fgUK=&_J;Txmb@S$ZkOvci0xBF583bko~mcJ^LxOr}huDU)c}qe01*9`|f_$ z;D_fC+IP>rCf_}Gn11)%V)nyh^BjS;DifHZ@3q5yZ&%!R_QalynKL1t*c;<+#t$wo zl3*902xc)_V327DT4iRS)?^LJUG|``9+}ty7m&T=`c2`M+iSJ^ZjZE|xen{S_PlB^ z?R$#$Dd2$Vbl?v2kAa&krUTbnehOG+_1S-=)n~ui0(~VWu#%zv_I%#xx%_d@m5%)w zL2B$kH{=6<>=E+xQUvEvq??H}Fv+z9{VIE8QZ7F$ZCr+GY1XxYPbm0ZpC?_UJi1MHnFf zy>AFIfJo#a(MqhyVt63JToi(x<-yNi^Ot)R?W;qY)wE@i(>s$ok5`6^eW&!+22AK2 zpg+~Q7IfF}5xtK#6@J3>Ma(YC=W#RKX@%YMxFrr#vF(m8qFbC^MJ;lA9Whs6hBvFEK;C!Ml5lzPl!C~b$$!^~9H>`8;m6z*d|QJTWF6<43We@5EoV890ty0ft|NlGIN{${eo? z^#vw!t)w4hc&Lvg1e^3n#5!N3X9S$`DULeqR-e4rsVj54!?xT__LuWlINdF1bG=*8 zvb=$(t9ks%;#ZdiO+aiktIysnaAo~YK~dQfR1JW}kWcsf_ZWD)Y9kdcg4u#i`E++?yzgyG>r1?Jle40!r$Y-M{(JPnR!2RZCYjtBD8&^xMUmL3ax>g6`+0(;sH0)5p`(=ucAS z3S0%T7Lfl$5;B>BekYfZJEasdf*j=b0#@>yI${hJlI?^@4D2AY}4P&XU7wL*yWEmvbb+i5I% zaFM;n);dqi6=ihqw!EmQx{TEHqU55Y^!WKzak1;_BV(`EN5u@(L`C1MjE)*EjgGui z6caI;7ZW~~9TWaAGd65IEjH}&9|F&L6bPo0S8>BZ&c?uDJT4h5&` zoO1@MJu+@pc&84R`X${i3P>2ur^k)u1jRng42~I3N2Sdc_;OIbMc_Q7Vh(T~E>~jg zMr058zI_becaN<_2Cb*Sg6u$HCI>JXb9ad!S*{>(qy30bjdW)lR&U!DG*p)%S ze#>GL9XfM!OcpOF)@$2es<&XI#H9M463dc1MRxh4`HtE5b6qkXWVxk1%J4{eoH|!P zr;uj}gpA-kTr5Zafxc@$de2>5=z&)ve?Z@{=>U4MGwfv587YR&lbYO3$Bd;)4%%p^ z?RB*Z-{J4&u_ZFxa${z){@KJJ!NOT$9()|DQ$3U7$=|(k= zQ%%dCq?i{!O|~kSNSv#wC_Dp#>)`^{;$h@4J1_?u(0{K+2C;l2_5q;(Z9j%*Audo! z6ZXZ_-jtw}463td+%OV}zHX!Bcg@Yf{%Vjd?Q)8T#>Kh-#q;~*of9Mf@HuhsJo~Xk7V-7a7A^*TyoH>WkjyYI} z{#mcc?cD%f!#jy)>bL9c6o(HxNZ*)t zkva3tTW;T{0L4uof>c(#3sLWU7pmFvHdK4TyHK6^e`+KH*I_BvALd{q=3u4=pbP7u z1?ymj0n}g)O3{PoU&CI2VJ4FDoRh>&h%tmuD6#oZ=*@GTuoSnPa97ZujMPw_EY_EM zw$WJX(I;c*4fAyZLKFq;ktb@(C7FJ*mI+tM& z2CjwrUC00qV;=(gzpN|h!w0b!;0_@X&sjV zKBXu~e{NP4oxZ6d{tPrEZ-b81RWOo14W_b(z(Q^p?v`u;8~M#(J6or4Eo{fngO$i2 zI+x(Q;~dnY|18Bh$UBYv;}YISc>`mQpbtk6JaZp|CvXv05T@XZETbi;F&lvqyB0X` zC_xY&{LB*-g7uR8@Lopr1;~hxftYya2ohUpQ6T)Kl}rtVgn!{*8iv0;v97094x|hP=g$*Xg|*T zNqlyUACCDC!2EkYL=XNH835+M?A>ez!G=DZ2Yt8@MHZwO#6X3SA55s6;D@~r`N$vk zu`$Cdb}qmJH1C0p|2eSoKLvLFCx2=Q&cPzAff`(gMOcFwScCDscn?QEK0D^$?E$hE zWDw>rvH#*d@`um=`hQg*L=iQ^A0*N5i(+2*(C>4A1ceba(C0g%$4^1_upV_EzohFc z6Dt4{$7i5&O#i87T!*#mF#kL8`LPD$&tm*OjDHK`-^2KrfAiKw3_|Ju zV*kq?1n}>9QS2xN91j6rlqAX!*+USj4##vJHHP|t`t*nD*5ElHtbr`7fds69Fl2## z0~jCYzz*lY9CKjw0^`5M-jivJk7JO-SW+l4)W7V55N`bQ%orF^rPfDJgJ*?}pO9Vq4azh8)2#tCFI z7mx!O@*EG48>kUppxoyL>JwgIp5g`8cf7#M z?>r!<`G8#K1BPLKpx)yLmdE_SHiby)9Y1h=;RkNu`@su*pZJ0Atsn?a34`!cQ4o0~ z3Zf6h{uGGe7$o(PgXl4Vya6-Fny`SR4J!zHumfKR2XG~F0$TwWu-5SaOZz-vUd<0o zI|YDxTo4$q2m#YAAz-;D4D64EffFNfy%z@Fufo6hKbA34tN@N@=IdfqJnrsMf3f zQf}A$p}11}o5D8TFAB$WrxmX0y;FFk_d@Zb?nBidhPO1p^s*LM97p!F&k(G)(ZG76 zF<7lN0jt%fU^!c$E6WIG7~38_-;C$-z&%%Q^x{6)AL6NpcPU$PgQL3;*aS=dFpF3C zVwA1+QLkLY^^#9WeyQoyOp_ z(G;9knS%X$Qs0S{GXg}Sz zntk_NWBJ{4ne`8k4x69uZMMJMn(e@4ksUbC7MO@LfCKUmcl0{wH6R#&hU0jm@SjI3 zGJT8GVS68DG4Dm7tJqUty8Hw8SdF_*nfk+a#ij#Rb=KF++w8BHu6Dj)yvOaF@g?_@ zCXYQ1n0)cwWbw(r$NE!1r`;$27KhJ%^BuqXG&+9ss(1R~QRnp2eYU_tgaMq;?|C72 z2t=eFj<`RDi$H<^9gNzqyKKp&A(yLuR1ys0x@hx}#>Rsmc z&1<&68X1f$7v=!*eh4A&V;IS+L>BTPm5aQ~5G2pDWEdW0YO>r-v)~;}aFx0e6{vnT zB${?CAjRf@cb@Am_e!5F&Wq^l9ecu7+3$^9Zr>l(;qWqgfy>LNTDMn`mF{oCOFiC& z6nlOMD)gKVDDeF3SK#@@cecPzfCBEAg8)L_MWE+PK<}HzK<;HTlc#x{j zFqm(|btTh5{7kZs>fzW>qut?gwwr@8+}HRQ1uXNf3+wRch;DJ)96#UfLPDMUL_!5J zpJMN+*aDxIQMo>E!gGAyg=G7Dq-Xg|2W0tt_Ma_qzCXro1aP&~RgXq|J(`B#+LpoWSOwipU24Me&us ztCLH7PNo+4K1s>(f0mRPFd3g7Fcp&)@G3Gj;B8o{|NGz+|Bv((|LK6)0w*2{cr%js zI1i8FF@8F-hdlIM*bj2Of{6^&v68+9QS8CiU^!N0D!8ZE3BUBfM{h-Xh;>I&towr4 z^uX%KqNw7qhQyrUWoc&1-buGIebM6w z#63->$4tftMZJg#j(i;v67e=PH2i&VSl9>pY=H-^0jz~bamaws|6jx$oUTCsUx(fY z-?C$}6EbB_uSzFwQq;eyVHs z`b@{H{tV~z$EofqPm{b7C*ytMU&Q#uzK#fpej64T^*%T->O;_MffoaLf!_Nba+qs5 z$UjQ({6jVRZ}dHT@SS^x0qk1MNVcvOB5PNxFfUzU$k*I$EnB_VRlA_s&myHM%q5~W zA;7OHH`1lNF43lRRjPT(l~l`ur^$BN6Nygg&*NNEUPgN)zK-xpcpK^+_deJs_5*#k zzz63c68HXb9xkHqJC1d*A9Ju1bFdY2unFJ$H*LbR2wQl`@-1=Aom4*k*oJv`N$Q1cQbP35GS3@unruPUNN#@ zw;D^;4jO;{RvVd=jc%F|YyC}pS4BEF_N027EiDhyThbk_)ptlG#;CWK3txm_=pZi=0S6zxc!cDL3_NlJa&ef+H6a*H`-F-uC=MlR}F8RS6clk zK)vH5U3>lqx_-?^I<0Iv(4^>-zj^*=Ka0FCzO(f>3fE)Czd6{8IoO0bSl#;X9(>2D zfA0cl+>QLV_iGdVbfYhf#{#TozG zy%?Xp2fq&j_n_-`AOkpn4B#aCAnZv=zQK+@T#Q8Zt1|{&H{tNQ<|yFM=O<;_7o)6u zwLnYla+ki+#VZD~XTVtY0KS!>&sSOr_NtwDlU@t%QY`={twwO1tpWU8Jb`OrM=j#!Sh(6$u&KX49aJb39oWB|wTEW$bTVOJ?60=KN`cR7gPs2K6OtHI!W*PPY*t~(Fy zZn&`a-CRkPJDqZhBLng>gP<&R3BCR?(3jl>M)DiLL~$jUDKEu0#m+xziwxi>?t`DjK7`A77U2f=A&f8(kNeEn3&2MlCS{2AgcgIz zgaxzSgg1x!WIV6J^BO_v$wML%Pe4R$6eJ|BgPi19P?9p2k?UsU0}y^5PU=x zB=8J`GBN-fgAjN!@++1nvNb;2<98xeh$DwQ@f8;9w0j zVGUN`XHniBWKS4Bx)<5cb$s?A?Ee@={(;YK`V<+!6!st>2T(_E{cjHfl}H08dT@U1 ze~=_xph@r|NA%T6=|Lgz%xA#F1$1@Mlfp7pDVZ^);kU<%v z!cmKHJdaS=9Pu3}Oz(lh{1V7)Ex;PAT8s0)9pfLu+B=QUkMr*}fc)nU=KdkZe};K~ ziO>G_-}o|L2@%J*GwcBkIQCr-!ir)*{mUMh@aN1}lu;fiT=;Mag_8(4iGcs%ELE?@ zwZ9eD-hP~a{0s=bi23iw{EuJ{2+qIB6GHSb2O5|ICCq{BCuBc3hMC@vndhJ3FB~Xl z6b1F~82elrW5)f+|8aqPkG)a2&f)CvXgx@#iq=5o!|k z2K5Q`69xG1f%=^a)M-{=e#-%@Q(V9{!42$B<_P!=7(qmb5yZ8rAg)UVVPrCVms2L}z8QaBC~c&h=ctxf}V+anxlV z;2c3cMSb8#7RLPz|M~%0*bC%f%Yf!IZvKLxU?7+Zl6)MXh!6Fs;F z{#g~rp@@AtvUYgX3)z@xBnJqkbAn(o_AAx%fIu5B2=vSYflY`-_wfV&X;eS|PyR9f zuL7_6rv-lUy~o&7A|Ucm9K`=Y{&YOcAjvX7G)#3!r2%hh1Dg2*R3WPo?Rg1h=TqyQNVTt%Fg>@1y z6n0BKS2!&>p)ephuJ}}9OyP^nU3pL(Q~=fUc%bo!CTQ-~2F)$Hpt(U0G}q{Z#%zHW zK0gg(TOfC^LH^)?zTa64C67!7S)HvJ>vszm&S?`m?_0xI!54a&qLVts5>K`2q{cN{ zWgcku$lll3A~&jWMDC78pZt*Kxcm+EPpVhdKodP~wqc*L_u_eC6DIh;B1wGI zG*|i`TDkm?QKQnJ!D7{Z{nhGydb>0)>z&cMs5hd0M(>UOFRzEGxCnna|fdW!hzU+!((;j&{)GAg#}IkMV>#@~0W@WV6Hq zY+9|rdVvjCHrj$koh_Kv+Jeb!fhp#|7LQcUwDx(U{|{s!uR>VJ`!Fu@DngJviIAn- z57VZOgjjRj2=o-Z>=P_?&Ld9wgiE^C5ywLP{dU#HyKNSkZ?{@zz1eb`?Rtx|_G>I3 z*e|#I;n;2iP7Cb7vCaYPs~o|$!V#>?9Ko{G5zJ=`EYSNq;C6=_A+P)i84pGOA4MVe zVwlOJSWa>;PKXS}t5B}Tny_7nbP_lf;wODLFkEG?Z=%k2&n(&|w-SrB&ULmc9NQhc z?bo?>*d1|ivm0?=X#eFm^QmzD3(r0T zqvyk4Z^QBEJ(C#8trQkAl*&V{r^}HGsroD@6K(ho#&}BYj0jTM6cVkwCNRZhxo@6z zr&onzi^l@D`EJX->RtEvR=f85mb-oMEpq?no#*kxBggZXTb3s{XL^D|h9}seY-S6O z-V$&jW4xd8%IGlN zCE@WVtsxmU^8<^VYy9gx%Y8ciioCZ5=6YX7-ts0O-S@L!s_!@N6yF~n$-dy4vGgy#v4P(` zV*-D<#RPzJtUoxS>}Crbn8+ubhbJNb=HO}?#?D6eP=Nlo7`;b1g&eNr!}os$hOOoL z>}!gxgu3%wguEpCi0oYVfXr$?&-Bh9 z=d^txj%g2qos*{n-4Z_gdB%P7_Kx}K?h^$rz7gQ$7Y+`y^$TlZW<3mHEzacN7|y}I zQjA@Ny$|)c2fq;e@bT4nMF&4w(xJf6(x%5z-)t#VHs4t`r`}g1xjNJ+vLenZur$-z zqo~~5p`b0mI)4}4B5yR%D*KbaUHTUv$K>x`E(t%~-QvK-Jr+-*6F+>$U=0MR^_(Qf?Zs$cobl~x2UX1U;Gkp+%c z>0ob@3U;&g1n1#a;=gsUuLSdtbFc<$V8(xTp#N)Lig!V-K@YMGJ@`&8Qm|8!Wbe?R zCT}z0jNWW7NZ;rs<+(ms*F=-mEUD=fQ0q#Xa(b4+3Gqw>UGq&ZPZuYaz{^LJH4Vzk@+HPTa;-0zbVP<2pn>m_oU@n&9%*}cfUy?D8x!ccT9Iwapz3^X-pawsM8uS9z!n^|i19fQV2bcqK2mZr7BFFC~Wc$4; zS$=0orcWHm@JTr7epkrT{C3>M{)&mDcbG)|CcKAB@I+4I%T$lT7kQ1X z7y~OX21aol;PkuE|I;x3s*a-X&)_+T%b0_49UcIDkf4vT4#a1ufghj-{t`9tkDLb} zLxwMuN$0r^sr_w5N`D8E?DKq*c`=K}{}+q8{K0s#-!PusLwFDWVIrklOh)Az6Ti+X zjDbZMgR@pm#NWIhWA7N|Ae=|;uOj#F!TW)>AGAFlBKEI{Y`($T6W?S0!7mf@4<`Hv zS^;LV(|`m4#Ka&Blr)WGydz0VGPd3KG?e%@gx6S`~_#B zkKD(?g9!Nq_5XeN574&7$65SAJJcnZGxpUlBN*tZem567Tt^nc-LjD2qGaee<0{GZS9{LL50{kO=DsjcG7XBh^rbq2`EoxCxlV51LUR9h8D*&=J~smf0x_)jw5NSQ1=D24m*f-rMO<*E*oUeIHzXAGF? zTd%=5eJj-Zw(uY9Q2$zM^ROH=w#bD!a$$^I=x4wiDwTPm-5~x;yHotT_K^5j?S+z` zwO2@f)ZQd{s=ZJ4JMELQ-)di#e64*~@=)ig1?`7cez6dswZQFvgoUE!X|5rxl8FDZO#`my|nrcab_nY>WH0nIDM zOz(^tGdOI;4ENYDL(E<=+-%1THrc=8W&AGC_kjQ41^>YhK0ts5Fe2t_%lpI2OYoC> zgy@NLn)oZnV%didb@HFvPgnfZZl=ZL>k`md#%En>MF3uG`$ye8=`n ztqazF>BIjqK4Ha7_t`P?t&YrmJrJHlY2E>wMiyF5~*=oi`hv zael+-EvJjdC!Ft^9CrHEV!so!-s!|_*1Iv=6&}oXu@|!$^QW zK7*!jdM~s%oW??R;5x7t35;hxrTWuW+Kr$iW@Nf3q0(y~|Pm zbJzS;JRqhM)!KBZ+Lphk(pz?GS%m7k;$*$$`8lRzxuv!vS+&l?>8+kKQv3b7ljjFb zPh1l^E%9)8W8#P5bxD7P)+FJ{vLqHznZ$g7S4AT80B#it%ykN`AAX9050#F3s{sB7 z^v_kpcc??{R}UKabf{5{_BL7Ywl%nk*VP8Aj910!jFx4X%q=dk87Qc9?#XNNoSxm~ z-<&x+q%LE5L}mK^sIv6;qDs<#4=>JOA;sw|uqX|yK&3Ko;8~o)+=1H^EExTOORjs_ zsC$wBi&gMGpnbFn_4hQ~`)tLvf15IGn{G_&Tb)EJrui!_YKYRBTbp7uP?c-dU0&+c zT3YAXP}J^URWKM*lD8x>KX-R@PVV*StlZxsvvXN!b`A^5$zuLFnan3QgL&nqGmpG< z<~{|tKYo*pe!w`miX5Cl{G9$lXz#@(*sf00|J``*p<9<$cH0UTcX-RsYYS5!oR(nF z-I!_7T36&yUtR59QQ6{GSl%C!T{;?(Ra#NwYLlZseaN&yQ_&1e4Uxy&~` zhk0dWGmnfc<~{|tKYW>n{Q%?e68hjo<3#Mc+fjdaWBx%O>i?M{S}{|Dmdv!^&F^=U z4EF}9^mfJQv`tSlZD`H6t!OTHDQsx;&Z_GPO0F3RkFDMm6;*X1DzfT_h^Pvz&|b!Z z;)|JoLLu`_%4c55dCcQ=eu%;TfXnckW$+)6gCofQ9_Vj_{)T?sf5m0-a$KU1%~7HG zbBt-$Y)4W5u&+YRmFhWh+9k8njdKPItW#!IJH_<1d4~25`T2LR4)X4LE7-I1 zNubws7Uj;pJG#zw8xUKYW1w@jP;H7-L{p7yO4w{iV=fI3M-L z64d`#C#7qJDotNuLe0yagf+|j(&B=exOn1-OE4*EY zPx!bDe(UWtgZa31F>kL{=H=7GJpJpLdq6F73#?(TQ{2Nb;YI~~DCA&Y$3*|HM*RQP zUkv@_sQ)*>f7m2Tb(^%QYNHjEZg3aouM3vTSQ{rzSe>aIxw6D4aCxJJ*Rp;)m!;#5 zj*E{w+b{ao$!0!twI5=x&V9_)wS&2Ov@sXYY0TNH`4t~yEM7zo4z-~VdQkrlApf%_ zA}k%+_Wuv$q>$_Ktmcin^QGJ9jd>DQiRpGmf?D)YrKN#nJi)Yren-tiengu+k4>w z4PpPA%Z>ku___XX+z$U`FKS@;AnC`Ue+D(+Mbv;-;6L0Hk{#BJvAV5J7PrjF?3Ndq z+)5y$TNR{#Ylx?F>kwb_y+8Qs*YO3a?=V@7Q%qj#4W^{C3vJrMqJ)phAFjn1 zTrvl95THL3`ETC{|7QpKAGyza6Zt=d8u&coe;Xdab?D#18k8TR2EW52!_V;S#UmBc zcwj)P_gzTkK|CowtS9-0D@gqCXOek{FH-rG@#Swaq0%LI52r96<0zA;9)86Jj6rT6 z7#YR?=rsR3KPh_{fcqu&&M%na7rIzzZ9`& zpT-=B%kW>I9r!+;!T1>SKkmZ+fUX&I4ZbGQ`Ud{PcbIzsKL9ttiT;s+-yua^Z%Xt$ z44wnp@nS#G-}uiT06Y-75AJ}EU%?%NhOqy4ZbbiM?3Epc|AcY(QvW9U9yxda4F1C- z#Qqrm!xQ)qPcir5N6dYIw%l(-!atyok9>s~ueE_6r~`YzPei}tm$cpM(o z2F$-g+A1*i^3eAw7;RR z?F32zL>M}eYYUf`*F2D3?Elj+{;CdQ{GYofa2wnMj{)a$pxU4GL8kl%?k7J2&Y|EukZ)dt$vW|w zygiS}0g)3lJwO160a>5|wBj%N!3Y=!8^A7b1e^ue0Ox}|0-O@(y1W1rZpa_la;^l| zI!>Q!>6fp;6rrtT1FU&W-j>INTsyn~mx~lo1R6jW{))@RC|CtHgT3G=I1g?B&IjQX zxTq)oaUKZg%y2E^^f||fYw7*hU{YwSSpah$Q^8ga?+N`vejpa)fJ)GUzv=^Xz!Jdq z$yTr*oB)>)`TKBz9?)|{{5M|xg`aXc;Cv`fpNpN-{tVv89rXMZOcUDLsQOcpWiBA~;%i#E|gWh&%9YpruLiAUW^G~4p4dVWTe!y>i z2hjJtFZhiAIed|iBuwz091~o}lTK6MEW%?3x_GQi8&jEpmL4$;bF>A0qU4YMK`vyo zc}!dmnt3n8-MqiWL%cu43+b77IsGc$fSEYE=m(iM={uPV^sVSNJr;dQU&=hAhq8Eb zOP=xXVwTJ&3QY8_GLv~*%EagKB-Ls4S6~Th9y39$Z>mbn1Q_A9u_4-ILrlj5?Fr-k ztCqt1Q(D0LOV^E3067V41|bYnjLUk7XtQ1B+(iEsHMEO^ZR9>lX9H zS1raRZ(D4Xy=1XZ?t;Z>`Lh=9%b&7%ta#Moh4c_s5ZZ6fG0wnJ{gqhR zX0bEV!^+RPBVe8@)0u)fa&cp7A@qOnMg1R)`<)?tx*s5>2R_R5A0K_X0)lv# z0^&ty{jz0G`Iagi^R81l?A5Av(6d)#pT{h%-R@&LJKWalZFSqDzsdEi!3MWGhO69u zGah$iCX2y*4`wnOD?Vd&8N-=a-DZY2)1QJlBL~p`)d%|FnE!xN|3`7S-jBun2h2~o z8LmNZM_JOjXfNK$s0h)Kh!nX4VR_1XLdw;41T|=F37oFG(ZAndt>0YZRldtim-}on zU+QzrV$A1#iv>PETF&)lR>Qu`a)v*%=m}tET>;FbBY+vVW2NgUn5%&P^+w$rg1VQ} zPC?C;hT0QcNfy%OL=`%nY)r>eTzGFJ2Z{D1#>sDs&y;SADbidURi(Q!qRDVsc&F)D z=%B@dkWuS-!E0@21s${-47z4NBj|~JPcXCV3}Lp@Lz#6;D6?!1W#&zx%oLbR!R6o= zoaVod!t?IXzmbLe-npoGz?mF)0GV?1W|l4;$g$(?%JLCy&WMy>mzFGDk&>geB&k$? zG@;gbURz!-QKkNM`A@IUfUcNXD#zZms5wr>`QXg}uW>@2dNO@;2lHTfZO`Cgh?MNJQY>8j&(ipeZy)N#Idv)9+_sV$YQW4Lb%Ho-Q zDZZGn6e~`b#xhG_J_W-#d=i0iz~z98AG#;XQ1?~f`zOJEyxvhRrA-yaw5GyIu&m5q zGFlR)Jhw1Kb1*+wzc07cq%*tDsx`CCt|5Jfb4}_(xAK&Y@HtL;6{g(tEJ$T;`KeeD zJcT*tCo{YJBxaqT$gJ`cn8g%-_@f^%4nNL7-CKyQc}sJ6V^qP(=hwy>nrDZ6O4TUxFDtzTAGVp>pBYn@d+-65rN z&^4}nnMYLFey@nK4?V-n{&9~eWiC-g%rPdP*~jHF+xQ%29iPptruYW^aEI#$jKLGt z=mY3)Yr(T0&|cXA{cc?Q_u~F%p90P7)1%%#8(w>lr>MC*M838oPPJ@$x^_Wpfnnyf zD)Z!~7Tegy8BXE#i(P~3_P7Vsz3&!K``k6KnmGrTGsmzJte#)QY$6MobyPmHn&J!e z!w1}0EJqGdd+tK~8==3d3;NJrFa!60hj9OKScW=>)v0aRjGBg=1vP{I;RG+Hy2;(;#<87E7GWK|wt>?}9evt%I(xOg=j7S)x1(n>bMUES_Wo5^#lM_c2bD6b zkP>W>x}{jy3D<2EUxN>Lg5;%<;@HJ03gKh9jYz^BLo3yN#-_WvL z`dr6+5z{lD#|*3nnW0TDGqmf%Oy>4iaK7vj?0?%Z{#HYOG4$s_e*n4fT8#P!bzt=x z_%9pbKWsz&vlsREJ~;~Bt4RTS%*k($EBWjWAaJkQ`b(IK!RRaAKp$+z{(C=A_wJ29Z zAF&s2hdyFYd;|JN@m-)NQ3Ibr4SW&x_d6moxS~XQ@92@v+jgXNIfyhaXOPQi=aOn`n~A;*0s<_?8Uor z4<2=B!kf@P1^u(Q2M=wBcQ6OwI_l5&F#q5-pOik5kn9KQB>upHL?8H*;Dc=9e}K-t zjUaA+g$ojf3j)l2xWIUl6O1o+mX%?|C|?Q{pv-aywFn!3=@_!jdozC-*^q5mUvf1c#m$?YF} zcrt3ZNOl0YD0x})Ux7FC*MtYc`5#}rVgUWmd61k3R*JsQM*NBA;lEu){%>LY{|E6y zpYwn85vvAtm7pnkhW!5t&A-8O@ZvT8#cN2=%XLXDuuTRt00LuQqJ6*P1{!{0k}izD zrfoO|?#D3*`&{x_Jcon)2V%T=AomWRVf;Np4v=ph#IBCGl@PB4AH(O`!0G>2b4vU5 zEf2Z|AR4rS&ERc-51)h)SL-`)py|%Q69j{JkPE87>;A(syx#yg|KTt=4c-ACfCu0) z;G71o;(viAPPh@jO#bv+Y`IwyTn#=26T*KG!hhhQz9c)qwZj)gf^<*<8bKEr2Auz} z5^Mr{z?6DJ_%EV|Da?- zOu-24Fvd^0ws7t622mgbl!6A(0S3SbSc+k=22RITW( z-vyXwvkV$*p|%aVK7dhh3ZB;$WZ@1D7vG}a|9{J| z`b5#0p34Q&Gf6!CD9)zu#AWogxPcyt+v&b|27NA`Lw6-(bVss^K9Ovuk0kr)1Ib%- zTXK_b$v&j(@_*13MaH|N#Q5ixnBXipE@i?)s!X^a{>Z*bm;vX#C=fFgV>bqdcx|AD zb{Wtgn%4BAng@L&4WmcW6nHH8^r^IpK9)AqhpJt4TXlfmQyrn3YD?+5+B&+bwu|0T zKSr0;uh0dJdvsd!SN;htCVW$ii4JKo@jouMIpp=(E<>G;yey3zE$UIyJXD5h%$we*f*3tcko;axBq;+->Ez&~TO zoPWw_li-BW0l_h&bHXD=9|;c_Kb6^I%w%^MGx@E?Oktx5Q&?rn6voY%{1Wq5Fblbf zrHw3cnz6&F2Z#7C?F95Se4z)XYV;oyW4dMTOxLV}>9Tbqowv#1owh0CpR}nL9J6T? zy=mPmbI5v@_@MQuWS{kF**(@fdx@Q3OiWQxXX)aw|g_qHgBfU>c!NjV2J-0SDg9-5PuYEjd)yQ zBx3#nwr6mfIT@}Cmtj(}HQIobNS<;3ici!rx5Yc!-yzE$fhT?+Q0_ohC z3XS2YM(vrA9eTat1BP8;3yr6Tt}|^3J#5w#dds{%^cS<*FlJH{#*C`Mm_bD-(*ru? zAxs-+ar}uKd>e>+-m$pMg8sQI=;z|zKiHRz`Zt|NTQXE>U8XUu%yi-{$@CX4OplVx zO-oiBPRWtZNGj3nNvzT7h;K1yjq5XRikW9t7rn}&Dtft@9GD*yJQW zwaQLp=D;*Nff;4TF@x+_rk5SVbf@6_z=s$I@1;$6P@Mm>uN3z@%TafMbtS0%N@QtK zsTPftTF|UgH~!3$K$)JR82R>sRF!G@xf=Dkr8?Ewbp~Zw(@hF91}$>a7h7kh?XXEp zyKI}1_QX0RjadM*loV#1n#2rK6PbQm0@DM!98a(>d>)5>Kn~tQ+;5bj_O66}HELfl zUWM!aDy;ihrAouq#x$eak=I@2E1F&rA=_M*s8m;)rB+#7q+L=}t)E}eVv?0N!#p*2 z)G9G&i%ne4d7IdrZ>(donMG_iGmFb)#_{RQFd>!cC!{dFDZWNOe3FEIK>r^@%=@6f zwHEQ$qwZ;dK9~yz8q(HTr{ibvT0Zet%fha7M| z*iPtgfac0()SfMrl!j{yn3!#aMe1CfXY*r{*_-@_?0s=zcOYLSj>!q3$S8Z9@7uaVR}=1o`7Ql z#{Vgd0nP{8(umr38fwor_#f@4Jv&hQcBA&`;ZsA8lxlhmsIteFO1nMz1zjOB*&Xq6 zY3&(GiETw{F|9S)Vbj|61Dj_V`8KUK@oGF_>e=vxiAOy%_NZk>UX@r$zl`bo6*E2m zLOiWI$tTz!-!8zh0DZ6%@vleRFLUrD{M=tEPANpssu8N`04(WBShRkMx|{n7(5R(|2yb zDsDAcO|BAmM#`DC=OpjrcyJauI8cY$7xAw|>|=;~ZXfyp+MR>A|2qq{#{%4c1sMyJ zD0KnWpO|k&@$=m&W+a#&F)vOSGB-=&Kc`gQYgUu8+wcrkr=f8T`@uIg>;@jG+w?O{ z>mH_M)4{atTbPzZBhz%Md&RYUj75z9o#_9y&|eDu`TgjF0gM4?x6Fh80PVsttbe&2 z_16j!MXZog*m4639=D;uaZmDJ7DhfxlldM?3Pdi8>%d2-mMB<`9#XVe_(;KQ z9#b}%%~VVWnW}jYQ?=}1s#a}G)q0Zi*cT6=4>lqH%b~vz`m>PtzB%XvXxBlzbQ$We zm8ic^4~1>Q9DF>Zkyc(9Tgg{Js>h# z{Ya?4oQZXpGFjb4OkRI3Q!pH43da3Raf+kZ54L06uY&&A4AkGy=RBzPQPh7+aStBa zxzJ95cGNc1|2t8C@5B6q0|GKXs6?jwb;)F(6&daGB!j(Cq_;PRboMlm*6vxPv3n<} z?Rr3J+ZbPUGZRWTFp>IlCZjpV#9H&2M0=9G7z3OSwgmCdgZ{t>>QBVph`g7t!5Dyc zDzu|_;~xBe+=D-aXE5GG{g1lT;4L1hpOGc$X?0RQWlD;t+(`apG|8PTBFTvkk~zMT z1V^ut@E9Y}VQ>J?f^3H$vVjSdR%1@astGtBY#jRYp+5+H&Vy<~>=hfJ5A9TFM<0a# z5v&7o95wh`sKL*o{=5MH;WFkQTpVzI@6{v8bHxA)=zh(h%p zhQSYx!h1N#c*5Oa*90q|zYzMv(C0j;X2f2x8Tz}Be`rS?Mg4mU@t;Hdm!SU+^sk}* zy@~lBx8OhAM(z6{!Dtl%Wt{xCN z7AIm&EQ)0duOSu}Cm8VGBeCD(m$|tYoCh-L4bcs}26Ld_hjHHm{VM3^A3*%a5dT@| zL)-1$SN@AWK1Smq^8Y3LkH_#ozJ=}+@I64fQD{xP{u$dpR30 z+QNAtoJRy^ApSOt`&#sU5#s0OK*gTL9Gth&|H!u^@@#n*@k3klE9gU0_6Owu7wA0W zw7{RQZGXq>=Kvp1Oacr+7-#|rl6?lh?3tY(Kmq0uEzkVx@0sC+aK>ia>!-K(C z^F{7mk#}3<-1I)40YdCAJs`v?i;oce1`ST{FYphi_y0o+{~rKwFl6W#1_Aalz=s1y z_JH#qI2-_1_XmI&kO|5_6X*uRfb$<#fK6aGI0DW9u7c$%X|B5c7EE{$&++Qn63a>YVDIeqoL}t@s_g%!CKT^#PZE%mI0+ z{UNsRPr?+TtDr@gRfnH&;PuOVIAZGuVxXCcoR&bR4mxen>4nBHH0DENDR$2_$n#e8 z^?t;93jKW*dY>WYACU8ZU+Qu_#Kp~N-vDoeOOr4uG*wXlD=H%&fRDPLke8Qz;)Xaw z;OQh`*UyDkIXtZfL^vHfeb5<3))yeR%aGf3(AWW=J1N8Hm>8Pi09GB|jRk&oDont% zp|2$~vAnT5A6v9VszQG$YSWLhrkK~_M4!n7&?n+Jcr2Opo@_DQkgcI>a?|LFTo=79 zKa(!W&!Y=nx)QysrB83`+0i)zUpi$JO~;JW z=!i)HycxWeownWbh- zyvU467J?DHo^8&=!{$t8a1v&ML#-)dw}HM3Y<+js-)=aaIbi(`vSYT#WJhh^ zlN+)7P9A=X!Vnm+WAgoWOs)rX*)iEpJ0{^^X7C@Z;XmNg;}bvlA3?b60O#@gj2oX$ zII7SaE{3$n&7QV<_|j(2DBgO{6#g2IT;WQOQqj13t$2xhi|it|Zn*`n!wU0U7b(tm zS+6wga!`4|<*G`*>tmH}SEkb8%9Qa9G(?w>m262KSnGgSJ0MpS#eSE_Y-?^d7geNm&u`wR6ZZ>HYh!_?}%nY7lMDOYVPr`g>Z1=>V{tpw<#&9)S9ce=2QI51Y%8$1&DpD{nGFfI; zM7CrgyjZ?JtXiojv{|Jiq+6{mc$P+U&{D02z-`*KfoHU<1Mh2B1pTX39)xd33u0;| zflOK)z*K-zk^clgAqQU~@3#^EC1{^WL+zb`+B+R}Uo!4})<(4)~r8yZRU zR36t_S(FLu3tcFa+O zjMz`~(_^3OrpGevv>2w57R}VsqL^x0BvVO?nBW^9JP#GY%>hE)i@NVX{)GRsz5q23 zUN1qL=I3DDg&Z}SnQKhFxenBs>%(u$2@^GECrIkEGUTf>3zW;#D^-iqnl$oKd$hAs z=INy+uQ5nYI%1HR^pQbA(qDQBNlZIFk!dExGxdZxrkWVbq=_+1h2yJ0TwA1p3L?Olj#Zv3pjNQt_N^{Bnrnp%q8d5uLug4)6uab-cOTuFYeVnJ@1 zG$*G)BR#uQJ2`WIy%8d3l7WxGYhU zSDK}eRa~r`T2!l+SU6oXreH`XB7a;jG;hCtaNaGwpxi%ngL0U5P&U&H$zDl zs0$I~)W*tWRHw_PR23-3S5~P+RkWywmCw`)DqEuCU%E%vx8z+NpWa<8fVso-gY*UVGWJ9@PNPUw^KwY1jPwgTN&zhZ@?lm_w-Kw5x zxK=WC*K(}lR>D+03YpX^AFImcO>i4y@hoz106E`+_*dZ)d<=TCq1%gXTPyTCu>L_e zYOih?%IH?5)NTVx>b9YTE_aIU3g$(1#tFhYGDJb`#S*{i_3~bA-AZn)3ssz3wo4tS zU6VRA{ib5yz@+wdOvRy^sW_E0W#>|+^nSwvr+rZ$NlGpn1es6K;8?r$a8@yxzBeb*ZIEWGC!J}Ml#5LUK!73ZmYm@ z&TOICoDCwA*>8)Chkg|q&17Q3J|;2j#ue;zCTrRPnkP7kak#4<$0O)3LCo`@I{@8| z0n|RjsD0+(8GsQy12BsD2TO4Oc^tLRO4OgL6v$?!CRwd8C5z>bWWL;=OvmHMcs!R3 zm(`K}(ixSKOsFxNiL_@j8J%7xquV{f8|Z^g==X8xFM$5= z%n1)@+B`f1vjFwy80!CJSOa4P>i;#Uzt`jb^JdJy--`R++vP}Sn>uN2H6o2I_N2bq zpHw#|l5|rsDQ#>gg$-jQxBe)}t$RxHs~O3!U_AMyjIV_DNar$v>TD+D*wKt}i2N_= zLmwb^&I4*izH1S4F?2Jb8@~bTFl@mbgzZ=db2pwr*oXNK2QdHO4cz-aEJNZ$QW721 zC&58G;vGOucOVr;BkH=n_`p5*!`&YNh8+)V!LuN17*A$7=3^|KU>)=qLw_EAea2kO zL0AM23i&R9cGgDJpWEO;LDzRb`rr_r0eKV8Kpe*$gp=?ePNVibL%7fo!)R6kdN}dh z;yn(&XEN~`gZvaa@#HC-41b^S8{o&hY|l1qH&3t(`XkUEfIjB|)uXRVHb5USC+vYf zV)Z?WIT+Bifu8Ak=wHI~4{u}r%`5O9uAvRr0lr6wt}4R#*2W3Y8VBDXkP8sxyXe3h zpW)}wd>fnw@P~+N4_JUPKZL&TTnrCvHO3%Pmj&&3#2kuvy`k%T5&3@y{tL8p-i7`x z=zl;YLW(i9eS$mK{s-KV;DS^Q7l!8WLXeNI3&BjhJ_yeNxwsEq&I7p)IDZHXL!a}2 znvnZ)jI$i{bt3W|hCF*CUPtI!z7PKadRowtA{@DUi2otSMTrc#qBlIniU&GIlgGcaS5Ubo1_#Z#u^ErJ^o74Ln;Nwuf{XbC0 z&%FWH4vbND7Q6?LSTIpzTVl&$1GqUDJ|G;Vfg(@`ri1^V|8N%nKjlAgRsMJ2S2XYW z%Q+Z7;N3T1s{ZHv2d)O^n3(Ut`3@ZBz!tay&WlL^xu60xgDx-#ru>K1c+L4Bulxtx z-N*a;;0JWa??}_cGay`xz5-nR&-o8r9sbEA6W)gb-f^|IAut78KD>S zFTDp{Y|Q}ICqaPAMGh!~PCYcHg0^ocI4yK?A z4+frtKpyYpfC}L9VhUX+#1;U}SmZJTohLXq7;x8s17XbUL8Z2c22)R~BGK&N!W;jfia@o)9_>ty>r&k1>1c`OBG9oaTP8 z8*CH6T@+2gRN+71bn|jrnF0=Fazgq`BoE(54cdl?)egEoh%EyBmkfU;moD&2=^Ve7 z&IqQ_TY^qHDVRaWg>$fq@faNzt)VwWJF%MZaoQ(ygLX^4q#d%)X$!oUjqqO9gSGHq zR${TGW%%YN4tyQi%VnPbMLT}PVg509s8pyo1##czaDotBVpDk{)r6&*UQYDLG?-RY2K z2<_KOpgr1|v{R>$w&_&T7TpHgq&uB9==IS$z1g%zZ!xXZUr)>R571Km%QU8cpSQr^ zFaBHu#-9a-z#y0b`X=Fgpx+E}8nMJJ2M65ucE+g;+w*vT#!y5jbXDn)fidkhcAy<5 zzO=n7UT5VoJD=n&N+@hJc)S{EO*kXV`YO#R7z+#nPp2aS~9E-EUVT(_N zGcA7?^;$Afw*?b*S}Y^`XW9sCD}U>bLbqa}{%H0GGkTj*HEA91V`%yn!P&T{A#4LQt_ndz`h+~=@W z+--kC(rN#pq|M=1$utKhX>wrVMtdexZ^uM+pvH~~Ies;pn1}NTPAOM|P-}#t-U&s` z6@W`Gv}v1*m^Qem(@HlJTIS(MW1haWz$=30c_r~?du8#5Jc|Sao|U40k4Bjuj}A$v z`=IP}_eF9oZX4vA+>R(TxZP5yb^l4e%ALtqx-r>uHzp}_W#Up7CR5_VL>xcb;L-vy zzYqP3k*GCdp&tY72zU>{xZd`|yl-D+8uQnu`2p56C(wh2gMw)wD3;eBlqTp2%oTP9 zl*&vGsFk$%x5_p7^~=}!jVM<8u2!n>J*Zsfb3?h<_o-5$FHrU>K{Lz!G+2$M|=W)hAsFb+OItQVkn1p2$PQS)Wto^uB34ZNR= zb`7NPs5eEKI#TthHPwom(p;%NEr3^(8pW?nNfwr-WQ&WEi)HhYY8A2)+mzB122_&c z7po@3ZC8tpyQmfu`&czPmZ?O?Fr}zyrVt&4Rof$&Y;-u2aNNgb*1O30nN;{-(BGDi z9OR+q%R#+?_k)?JdC;cmS@P7Jtx5ISrc{&dNR`<>RF)maFUpD+=4WQeWM>peGSaK$ zQ_`j>CZzVO#H5U>My7003rjwy7MlE(G$e_sgd{ShkOZa>8pq^9W0`DN^aOW9a5;{? zKLM@1(BD*qy0;MZMgignJ@{E0wv7d1swq^Zib6vwEwrJcLU+n94C3V!#PBooQ$?wH zx#GmUa@p9NCWXlC9;L9X`6@wK8{sjWk@{zTsp6Nxl>O3~qF*Xg@K0uP0f|gDAYp=! zG5#;3?~mkSUo6IcP=+y33jYO{!o6TRXu!{^%6L>-u0#dpI+R;(PFdwnlu__x}-!LUR*C1T-2rDUpP<6r(nIZSHUS|&-_P99(hdBBZnz?WHWis3?}E5 z#$>%xnS|pu#@t!-{Q<SjHEV*K*cVSA2fGpb`fBV8(BeFx9?%ME@xG`Tbx#Yf`&)Pv4=*MfUQ9%b zDTPgQppa?a6f`Z20-BR}zRkIOuciv2TVtz?bHk9>p?;;ruI{MBw(h>zrkY8tD?vFZ zWfI#WCblb>;4*S>7-L{-6>8sl-1~0CIA}l)KqFpPKtHb)_ntd(@4XwfSC0$@U|x(L z)~xaAwji%AXY%OsBe$+-a_LMbr;ZYyeMb}DrhSIMa{9Q?yzQ{ito3sNyatg;BNG|d zGZ~W_CSz8~WX#JZIE{T^FLJ&f`b(!_UqrmU*tURbXcx7k_U=OM*9ZLp)Sg4AeP&6> zX_hKE4C|BKkR{m+x{}pkAXyH^lled{na->s0P0&3HN;OrSlT z33XbSP_GfQhwGV$;|Tg-EAqV(nxoTEdqT4lG$8IW#GM1}ROam@rMVxH(kw>GgJ1^Z zsq|t_MknK|PiF#-Js1P)knbhXod?bS9^?SJRWq;#!64Qkm<|799`1iHMD4X0wbwG# zUMo<0t-}59H9}HZt3--xv`JyL1<9>)BiWVVBwmq0GRtd7I6i}T%U0vVFT!AWM!ZFg z_zN%>V=g1fFs>SBPO!NN=LG1_hxQP3yO8S!=$1e?3%Uu=4TG-lO4R>rQU7m1{ka+S zC+bt_cK8oFWiXjV8R*~uX93(n6q1Vr%NBI(<^?$Uy@AgD3NQo=mV+^{aDr9PABFzx z85jr9Zo!{aK{sz4eSi=m*P{=h=?*=+U3dm!FYdn|!1D+AScSu=zmK5CJc4!{RsveU z9Pd2hH0Z|cfCmLl z59rxL%ltSzs8hKAe;OXpS**Wt9{Lw1Img4rmptC9!~8bENf7yX2Rr`T7;Kl3)XVsQ zi$CN4q07wyIRy4iFc11Oq2E4+V*&DAv=REyj^2+sDEKq?6Nvu|;)fP44+&GJ3AYVN zd=uL50fhJ8?YnsWz6ckjDmWP%;-UcUxSxXUH25MIM4#c8KZEX+2XYaw!63$Z$6|Ou z$a@K5&)5wg@(}b-LLYJ3LeumH{Ga#XzkG=Ne+>WS4z%uqd*J@$_5rq^18(~z;GWC* zNf&c1+~752z$56uYjo&u-%WTR51@Z{f|v7vpkEIC9Ox%PKMeXlh}#MJmWb8hBg79) z1$@2$Lkor8t2ro7@%o2Je!})g@C*0@ybz*r(FCp_3-kjBu!m^Ruegv!8^AQ|V^y25 zA3{GB{~Zne0L1SOeLLjV6tU|18&|yC`bbNpbAU_-Cz)Kb^lT@!X0HWN(3c zfUEX@LXG|xoREp9W4NC@0{?gafvdAQI3L6UH~?P|12RDgr~_@F7Yu_1V5%Kk@Om#e z4lV<((&noEU*Qq_gOp9oPT}f*u63UR&VS&3i>t>c=3VGu%hlT4mTL=_4|gz?i#)up z0F9s>^uhNShQ@p-EI~J{hVQWj5${8{oPh2X)Xbk^SUiPaFtHZOSJ>VMcL1l&W$OmG z1}1zDb!_1QyhIaQW5DIY4@84>Xcgk~tKcRyA-Z;ab{{f5F%RP@+VMTw@dWMo29@>K zsJg#ImHY)V{}6xi0H46c&*^hWV78Zp(L%tK zppV1qGX)%8ahQLg3=3Qqbq~C$Yw%(&p-;}BPmc4w=!h_k4vG@suVm70aRKd=l+$+E zI@&7RLYpx!Wux36t(TimYvfnZD*5fSLjD9TlfOla6~Cv2@LNW}TrdaV8>ndzUwkzH zm?{qE;E6U4{|31Cj#JH5yuOH^U67ThQxZ)&EN@Et6`g38vL9_ziKNZaB-)^wO>0$) zXti1;tyF8E4KXga)**;t=Qs-Jlb6fOhbg zIu5&f@PEvpZ;R6{c*_F*gE3C8+B|wgRhf2a=+R~^Yg(`4POEi;Xt`btJeE{ite;DZ z3`%IBK{bsSHq%_gE}CsPM8iglXuxP4%`iGhy+&85%lK>FbYsSA1=EZeuL(37GG0Bl z9KY(L51{{v1L`fD;!e9m+Znfj>~N`pwrn$$rS(P{w9>?smYO-zB6A;FU=dF9EE8ys zWd;pf70{qnInA`Hr+%w8UXN8jughwL-)^;v-)6OkKh5frpwa3JL7f#7RD&u|VafRA z7K~pu$q%OR9}xTd9>~8x{133p2bW-Yzs?Ewooxj)ZmmL#?DT2G-kN4RxY3YfAkB1& zrhcbn>T$}ZE~jE%hf_7L&8eB+;?yl@a+)oycU&f_aoi!QbUY_2cf2nuaeN^va%92+ z2Vl1ad<6;h z_#0A3fGxFQUd*(>AZiSXruv{{UQJ*&zcR2yP!>=lEDmUuDe#{m&h=X;$@1GMo9=g9 zHr4NrY_i|qk|bXyljy@l3Eseq2@^agc<73LK+NZ%eK-M^!ttm%aH)DYoJwGqBl6%kGq5edA~hzx#Fc!3~4yi$}C)-015+AB#79g$56 zStpked{izr_+z=4;6EkNK};MK$Yi1dm?+90_%UIWFB9JL#O<~a>iET4Lm>Ay$)W;!LPA&YsHRJgFoumPbUNS%vhTgzOZcotW(_JMyJsWz7B z)wa@C;~`x&!O~t6E3Gx@(p*z04K-Do+Uh3#s;VA?^2!mT;>v}_1r_T|bIT8yWtCqt z%P4!pEWPw6lk^g;aays~D6NQJk1x;~X5{I3n>K%nI=CO*)6MilrhS_lDSrcVU6?~$ zBWwrwTTb1LR?^<+BF#;H(%2Lsbxp}q)08V!jb)nh#(KSyhEDy0h8c#r_4ACf>eiT~ z*X}hHwB9 z^_v4D?DzA2XFKZ;+RdcC!(M7SJf*TDSjszMrKCMWirR~$puI+u+t#L+)i$7?-a6Yb zrDdg2V)Jg}xaPZ!W13zuj&A&?QFOi5D5?%>w1&}DTEmzMtzj&@OnYCYtmn`;)TLX4 zv$BJH1IA#mllg8hxpyD_L!Y6P_ghPGpNkaq`AJ?M{zG4qWcB4rMsK;K^)_jedwTT} zddBo)yO$e8b?q>W=)B7?tn($q(Dr{Agtlr8LzHf}Vfh4|;w*T7BOx=+GMYwP_6kTA*2L@HlmFfwCSS zpdAJo3v87KG_#*JVB0>OIsXXyv$*d$o4jMLMiS;)NX%RZiJIdj5pzN$d`_H%&d!qH z*<}(q-Yov(gW@}Dfq2i_B%Wg@#eL>8;y&_saht*2vq4r5_iOdsd$szW-9Sta(%xq& zt6B@R1(s9hIWPoWXgAIz_a3MG^I3nf5dUK_x#u!{30!U|{>vQ2cd55{^PG~`5}pBB zk|%D9Ys6)7mpCmN7l(yw#D2jMv7LWiZ03F|wzHWz&eCdZXKFR}!&;5Q46UBybgkY! z)c0}p_E64sV~j;K$Dohbt(3cZ9`~Nn&Rk0FJHh&kRpef4@E_Lei_-=RaaivlcI&;w zc3r4guT2uGwMAmNrb*0K&k)m9OT~EQ9xI2Cg&o&hXvd@o5!qiwpL>~ z&iYpBTCD}zj@HV#<2 zT^3@n(?Rric;Rt`VJxO{FivjUXyC0JY|AE2_;-@rzlFh|rJ#TxBP)0ogqZY}XypiH z--YHz^p-8c3uN1eZZl=BKsO)V)Ge&P*iQbti`;K7x!(bDzk}p{hsi&W7;=HJWKcPP zH~SHg$v!9DgLJUlmvi#x!~345vVTGh{hhD{HUM#{=fSot!Gl@Bc%-~T=yt59AEH}C zSu^&Kd!iYJp5HO@|C8jur^)}%l7HVNT<8_#ws%^vwP%ofG5H{l^Etpt=Ny%MClz=0 zJ|-kz;?JOa0$3k2>BsD0PKo|H^p~w;Js7%u=r*BSM(8=m@SslNK~a?fcawkLg9pW- ztRBFBco6M};3_;kwf~R_`!=jUap%NG9FOO-rA@9;xsM)!8+;z^hu|XI33xIaCTJSU zJ(s$kzK^lUN2}2-I7b`Y%Nmf&^Z|6d9%KE%b=Ds|#qUo%!*5_bhwk%#K)qJHsNrS^ zb@IA1UPm|-!vG-i7Qw&$JricMpNH$fx|E3x^uryr0rlN+oO2;%E<`((GDcrx3_Q&k zK+ExE%KsYWrzj+hOgiAt@Gku658L#6@-Z-LH# zP-`z#o~=;(DyC3KgaW9A4j6=4un<UsE-1+ZB@C&)K&XxI?tUU7) zd+;V6qnTbs`%{|i-(>%~TKNmxw?X+IYW>AC@DymtDRpZeln0Awm} z`x+=eKzRW=U30d00OZ@c*G~8@e&|L~t~B(D(5gkJ4UJxoKg6-eIksAdu?(d(wC!f< zcPCl)A?jGI!?=Pc@dBrikLV#kO|HXGWqtxwuek&l;cn3RANp+RqN166{Ue^xcl3=f z$ooIRi}_HpI1QAdQ%60var|D6KZL?8PD}GC*HY?qHOJp9*J+o>25VIBoJUUf~ryq5Ju*mU~$nbAdR{ApcjOIy3@L%XH|H2*7J>{lG^=8-z>ovq+!csFs^vyWET5>vP`!aDohevgtHn~ch zJb*uTk#oj5#>pA_#vSz0AQemTgxlmo0w*{8o)_86>^T?SiahvDtAjSK5$ z!)vn9=+Cm=5ziGvbGcW~ zj!UzboH7oTW2Omm$Shk9uqI`nMUCvXXvSmdlG`i?@mXfd7OVNP$!djcn6?R@Wj{X4 zoibsKTV?&8EVlVc7TDmj+VZ3s+u1MVW2uw%j;;7CJ+j(qNLD({mgP=MWU2EyS?s(^ z7C7G_bDgisZ0EOS%;ldl;;NM)7o2B>Gp?(G{qNBF6#W~V&Yy_D{|G}jm^mi#9P(xw z<9(+I7fR=>H`|=fPZ!r~Gl`y)jI`h-Vk? z-xk2M(8p5N`#H%fe?M6s5FtwflVo8~j?51#k-5P&G9KI_W5GQ#8ZsoqA#-GU$Z{D7 z*`nzUIi%?dxuod`enrz7@|~tRSgUD-h9Iq`J`e(Qd=^AMi01ZB3U!c1?w^W>I4D zMTuoSv8)L-kriQfvN+sR=11`CLu9PXib|K6Q3Wy_T_H1~8)PuLL;9l!r6+oprYm}h zragLtUQ5&gy~e1E`t?yS>eoblqgNHF)vJVx2nf?^Zt)Ry|2le)XK}kL8~=yx&P?)N z;$f;TEBUu&ar&|_&RXWgyU19Azl`9;%t%a_?v!lyjH&`PODcK3o#I_<1f^~OXxkEPd@-|Ps-Lj z%AUgLNpq0t>E68tfi({~vZrQK;zkoJs0Uh0vI}sAJdO7i0y?5yY&r<#? z#k2$Lh7CpN7vg;o&jS8!jCh7}O{729R=V@tr6Vs;TJxf$IX^`j@^hswzf5ZK>!m8c zLsOnNU9Ti>j$UEzgnnMm4uhPWGX|O2PZ?xne_@c8rPWW%(CVkAL#kFUEk(y$wEdH4 zT`H$9!fj0b*1(cd=D8*08^kh$n0kx#rK7}3T1uRyvBX#EO2ee4G(oCLGo_-mNXklU zq`0J2Q&`fk$t#|vmtDMEKci^7L2BVCgXF>|43Y{y*H6sX>L=tv4rFQd5;Gw~tM_^a z=Onc5W}0=dihc+amDE3sRZxE7>SezjnySpDmcQ*?Rplw=RY6i(6)nY8sZv;#FZop! zl3UdzSyern^vaofsg+Chk}9_7$Csbbk1fBZA5-?JesqaeKdKlCwR%wnTD|BztzJy7 zR_~=;&WRlVY#km9tgodGU^WcZpwG6gp8UUo`DTNWlr>sQ5nfC|qp##P^87_(f@C&i zNqR$xq&Cz^azm#i)(>mq>KAFE>o(~{)*jOfuYFW6tmYHF&?>E7NClKbsa7wvSgRLS zq}6+tdcGf>lTF-$Ze;BPEPzoo`=Oop>)OcsptzmaN z+wtxBTXS|ze=x`emRm(Z5=nxK}$ntI;#GzBmc% z&6LpIQVH&Dl)#=|@$Z=}zTKbX^hmj`zjAjTvi;R^#3bjarRoy;kEzOb=22 zcT&E6UF3Y7jD-&T59o(Bs6)S`pS*7n{TcWV!{q;?CK57gBY`6>5-{Q`ek0-HGn^#e z!+GL4R4wj99pXA;RGeq55Xb3z#D4HTu^V_#?E09s_CObOXf=*)T8&eyR&$B6o}`RB zDC0`Db9?9q=ytJH9#q8uxi{KrqvXD0=#P_o%_Z-cZz^u{ZNz1st2oW`6UVucVm~)k z?B*1U&Fm(zo;@g*F{KE=zzm@n8Yw#a9wD|^o(cfs!NzIl4TRAF>$mV`MJ#I<&S?#uezzro01eTdC`+1>}8{Hx1pmRrnuh z`fguk~6Ksgsv&!gyeEv6qXrw!1}Tu<)1d1?*LF8q(Z=pVrUI7IGw1pT9M zOl|epnlb6H0arR0aomx_wgTwT$0^7$K78aA8Xn!-0q;XtAEVZzY({f6n)CSBAi6D- zw*uW<%9)I2V&Z z;C}i6AE`V;9Z%RUGG{72T0ppXBdJehfveF*(d^wm0;BJ|U*uny@l#v$4sFVY8INB?b( zOA)yFs#|~ZG22hzQ@FLSw(9j4@GblkezPQj@q z=$E6PgMJeF5$OB#Q8)B$(Kkb1^C8E)rNJR4b!gs6FXRWd{{pq8m?X+*-NKB06-Nn- z!y$MU-eG~cas*V4t$YWC7X&~Qq(TwYK_^UyaaaTsupYL1QyxB)5;*ZB`lY#pceJ=q3=@?x@~0-B)*hG7mYhFfuLV_*3( zhw(j5<4xR4&i**6Z{SaSL6!VSb?JVK~R4k=Iw zHP8lq9Djsk&%xPPL`J`Y>R(5*Z=tNaX_7u1osAJ^e&%C3GB^zTHXm5<<-{)_CZ z^%swU@*ucsfCZR=@*z|l$`?{~5kuKBsn1fh>d|QD`28Gvgk#Uf9$Sbfv79clhHkkT zon5rRVKUsacoPreIlYMQ@d@SrWwPFt|MDCt?JIE05722F@s8dUDjo;?4=-9WlyW8F z1?1wfl%vtW@jEC_AHK&hZ90n@pN}W8ly+TBFW7|YF6#IQzQ^6v#AA2`Z_zT}{$H7| za`b!Q9GnH^rtz!NKo_0j7rdD7XqPX@`*mwDBI!dZXyv0*fkp#mY3KNT)aMMj&N`iI ztQUHeI36aBhp3M$^pE>-0WML}d-(L-9O*9VLTR1><$WB5gKz+DX{+te@C{zfN3_eI z@TFd3th|U9^9*hBBx^w)qiIL^{8r(_#XZQU-% zdFKeI7J*MiQ_nN93hTF#Bn?AvY-C3S2m!t8=aGK8!pXOT4^&!-+Z?(fmn|)0GLQPI%Ffe1rEMp-mp3O)j!7<}UoPQ>;}wjwf`4@p_Q8Tl-m~vxhi# z(LZ+3F1O*wY~%c~h4He9Hd#+8*YfFA)YJs+u}t$Re}VfqS)|Xp3z!e{V6Fkbsmv+t zTTZ9yk;@n1QBLvqS#eq9vU5()3~gIE#=4k;jFY{ZXxXWsBHImfWvfxCY%;2m4aUu~ z&ZJY;m<-4&(@{K@`Lf(>0*{4g%nr%|vkNlU>=~JD{;7;v{8vUSILp8g%z){ZTKPLu zkIx;s1bJ{NgnQk%rQpov-hoTKHJF*pJ`;P{VeTbcEkb0YWt^HiL7VIlKIX>GS{U_ zX1g@XESC#LNnDJv2^)5O- zL+kAzrUK#I=Y(TnOvOT(b_FuO;xe~pl_@6`}X3sj7XR7d};SxC9S?YrOEe{)canOTHg<(+V{Uw>C4&QhhOOS1}_~S zQtsCx$^B!g12_QNqM7dT-b!Lw7R)@)-$3U3S<85Ui_8r4lcAt+84OO4{@_gM4K9@K zkV@$YX_U5*PH735Ax(HO^&u;yCSv2(4s=b7vxqZ776rA0dKu zC@I|cPop2CQwOk`{l&aDk9fw3Wt6%Zj5CqmI2-AVcat`}nC65CX-G_vy2MPWNi348 z#44#sY?iXbUMWtTDTRrPB`f4(5l%F;6Nm@-@A|wFu#KUWp z^^rX8`@`;h`XJlIFrLdCFNg9IOBXS<5LANLua;Ny(iri8*T}KKp zS+5k50u(V0VI|Cm5#H;Et^#szwspK$S!^sN#nw_->@0c3K9W-$Dp|#Gl2M#4X~hMS zQd}iT#jTQ1JRq?}b0oTGwL})~mGFXlB(&g$gyj7w!MWV6%7!d{Nj^iX2~F2(o}sLl zN;wymkpjUo80YnL=zH?kjGQZIXqWN`R=YH_lI>= zq^E30p%2=ip7%=Y$i3^yeH+OC8;m6pFD9a^hb$PUz#}f6^VUsgV^@=iFMCBvFhF^ z7F{RAtn*1R>-b8{+n71F;5{@!BePgyx*HAkOxQYDKuR`5dApzyDtWJ|#naOW(&h!w=(IBxHi50VvEHNFf6yxCzF&vsH`ZHFFX8P?6 zu&Z?Pk44kRtg##K0WZd&lRIebWCdMZo6wkGJ5E^#&{Q5!E&Ihi+;>Jh8SQ9vgJTK7elM9Q=p*#?k zSc4OILl~rTqGLcUqcF?HY2=LvoW+y5L1q6&tZ3izV`h?_ps{+0x2F>tVYj1OLzxT5 z$U7--9J*nP$bHdtpFn>#`fJJm*9#XG*k}O6v5`175yxg9-~zBY6KZ&mgKk{GXAjfh z&*IDcMCT_4ypLOc%vQo&hTg1E@=ofyk=Ld3XajT;m(vGVk$0{`pQ1Qy!GG9}K2^(f z5g!BgPO;CJlf4z4%K?wWn-fkH#%~_`bgTnZ;{F|c_94FW4HriAmH)8`@MG33po(bQ zVRYNkt){&B=%!H4C^Q54swY~G2hcx+{!uhfz#UVZVtaauGx`{f=3IR2n2dPgk;DLP zqW&P--7A3)-}ye11hh|qdOqj?_t}=xH0bv8kw(g^ehVj)a>k(~BeV+9n z7ty)|_rv8WuCRR&)RtnBWMF_BaUrzkg6zpnfdr@qD*0&&^wg8wwEC5QNB;`2CS?h_ zqnq#_ZlfPiuLU$)3gJeg>3<)6kmK7vO74Fh|AViSZ0OdUyukJ)xV3$m{TD%PUxzp0 zJxh^~Jn$!y@hX~O5&Inc!%vt%qW>1W#yvjTcW@u$kvguTPV+A?79XG=JVHNw0uSgp z+TazQKY4>=z76lfO`uBukH4`00lYW$9ukwu%C~I)MSuO3`uI-<&&G5Eh5j!E``4Fv zCBG39^KA6H(62|o1pUcpkxF1wzuUwAL@Cd#mv|q@ZwS=!*gQ39(@7j|qF4&Vm<5D95e z40X@}127Zjf{J4$s0?l!?8PTIia&7|{=^lupQ9>1z%%$KRrEX2=$^xQhwW>iw4Z=$ z@W1sJmh4-BS`Xp@!4MBQP!5e8sS|%?kW!6u{J9)|F$xn@{W_eEtz`InXr80^6X(d= zAEwD(#hds72jahz&XCf76`qDiL9M@d0Cc{G30p%j1{H_$hI}D{a;2b`j~7uzt*U2H zIyve9#~FSK7*hI1hJ$ zPMhv=tNU=j|5tL%f6^x3l4Jgjy#KG{{U4EU-z1Jd5yxA^aRWc*ReX?_@j+fB-+rFF z{~2=Kr-qU950m#lNZx;$y#IduhfDO4dx+x#?Q#xx<4#vOjt#zjv zbt-y}^(fcy#~x&?T*8aFfM~~~J&ONuh&XO1j{W#`dx>K=aqOTzw$m?FSv3CcnRIbueZ{a9@ zKk2fLp{qu?LY;AbFy)kKf&XKP|6s)>iS1cVuO|%E?FkcF)f!!Ad=?)xLn#~n1ATNg z=dYD`hGe#QEKPVUow7)80FPy+%rjUZa|~9=EQ9Sb)9{!K8(x+fMz71D@psZ^%*ryb=WM;4u_<{;XbK#culGuzL!b|{0Atv z{~sTEaJltmI^fT3l>nwku*r{GO+L)8h-a}Id5@E!%y+LI^62;TH2(=y-yn5XG*>MVySW8AeHV1q@3SrDRF;Airl}G0(X9y3NI!f zA7&D~81;KF&jd4-2&E2S8>|k&d*HqK#50?JAM;|4;b9>&JRPLp%Ts#11Etd^O4@vq zq{TN2ucb&De5<6+uSu%?x~0-@SjzntNQvKCDfHVX`F74>4D6%0!*Ykz((2wU8N<+R~myur9L=L zYJ$`7TJoeKq)f^}>ZK&4LyGWX@$N_58|lDg(h%V&wRkaAkwH=s6(yxn zNm3k@ErmR%lpj?ixlwJB9W@{sQL`m2YNaGc?UY0oCdWrUE3pw@NemuL3?57j9!yLK z;0?Wkj(Q~gXc9gcQ?3;-mwj$uOF!>-5l=gxX(FcDI5Vk?vy(EsnBsUpDNG2H{De5k zO-PsQgaXM-sFd`CW=SC_OG=m}3GvG%HvTq=jyoffaZgKl?3WTA!(9nHnD9u50AhMR z5g#apX?Pm#kWN27Glbhx(S!uy|Ezy#emMkf0 zxssGtCJAW`5|`F3F=;a;GIgnhr*4x_CaA&5PfB3YUnMY+yAlZy$FHfy0x>;>*0~I( z{#kf1umDIIr59Q=x#tX3P|E9qY(2@zv5<@$dr8Z2m*gCOyq0i@&q=9)$1enlpdh!+!A%=(97{DOIcawv7HhPGAW1zS-Mu|&9syHk@0s4#4=a8D^Lhvv_923N` zlKNOheXMrpf*;5>0g8E#0MKYI&?R}CfE#Hr`TqQ(QD3$#+C~Y^|3n$Qt&(I)at>n9jo~F zGkpF%zK8ZkSPLteSE4s_n5JP{M?Du&-ZXS$C}$9wo*U4oOHKMOyU^PUR4Wr&PVjJO ziX&`~!f_KOBG#PvT$mJwaAPWqeLCDJ3Uu-SJjV&?Ka=Y}CZEUI$vrmOb_Tkw3#kLj zn~iP)PXmQ}7JD058KEruY?3_!7~07PQHGRP?)4I*rCS^KdwI9X@Izx9s;glU^a!?w*?i4KSV(~6hRFcYYQ4Zitb2*9TC7SISe2EXp-+!j-=$^rOfvxghu7L6ZE`rYY&}XZe+Bavb+Qt*jU^L^= z%OX=OL8lgt7P>>Xe1sqKKH1_;e2{nXLEa(5e2X0O264PbeZ0&-ei5bT$l#x$CZ6Jm zPrxIfw9mmQI02Jg=2q8X`#WCDkE~DmJ6_CJ*U*y630Wd%Vm6!`|v^TCEvb4+nlFu?jpNBL$-a2 zI8M?}k8$EWLKKIH_;yO9G?n+U2e!i&P(Auq3_qig-Rxt%!n6UScS*iF0a5AVB#!m?AZzF!tN3q%Hd#(xETvTz(@PfOaH&x}50tw&2WErPSEm(q z>QkrO%b+d=r^J9VTA*o1-SM|K@lLnXH@476H!@b%;XkY)j+Ml*f;g5D#}e9Q5&eTF zFwj|s#zs7k{X}&)z4uuhw7*V{>_Kz~rs!w;tuY>u5#{G{_kbyvIkty+y-$zJF8y;G zUd$%iWF4O6YWl_me&I4npj;W0E01!OQl@HDo9LOWf#B#P{C|jO2I&|5l%R*&?4s9p zXg-oQ&2Q4G$FC1-IO9z5*J-Q)vEou@!?H?SF2`(laOvH|lw_S5(-8wx%4&_S6G!!y z`5KBe<3VB9KwCai%l8Iexbq_mr^mKIaiK$xDEdeg_H z){I}eG5bv_&G}2zX7~`MIzF`L(&c~$#O3W6mzJHdp8W~l<4+sNLSkHKW+wAYY-HTj zRYv)Zlp%{ynQjp){gx@xYndb6R>jh3RW0q)nxxgbTbitgq``Wg)LO5SD(jt6Zhb~d zt)Gx0>yM?-=65Nu;p}TY1-{I?E?kD)r~?nCCSH^uR(LWUbZ36Wzl{;ksGYuyTH`<1 z*h{~yhxFJ5u%0DC+Uygg#Xen{9P;s6%B9Y+UaB42rP6Uw${c4)iQ@_>blfg^j(13o z<2A{2{7^C+IrA#;V$$(q6mOz;)tlVkk2>&Y3J8nXANOUB$@_!E)=!Lm&L-03WFu`Z z{Pu#Y59?V%q|Pl`YTS~g$~_yerC3VctEJeZMG8IoB+p|^a+m~WdTf<6kK>Z!{)i;G zzb}dI|CI#1m;}6-1XsX^c?GTe1Gu#mM7|fyEx{1V&vuyC{ejH!h^NDcl)y`0n!PNg z-rGT{y*;GTCqT-4!==PGUW$CvrNA#=^86|!$FET`{kkRHZ&XtJ7E6-fW{LMZDzUy- zCEE9^9ylqWtmIv3F0A4We2rw$^RU%~+UouSO}__roLQ%{`L0cKJW zXe%W_E>aZaBl*E0k{cW?*}?q%iI5!pmQqQ@i%DW#Oajj+#f2=C=->?!8GKm6gC3NS zpm!xC@HYv;g9-5mKb}_gs$=m!`1c~ni`A2?I15R~i%E%h zm85uIyp|A&i;t0*_*99C&zFe!N(qZ^k&yTS35uU9{&8!>H}-&d$KEfVF>i}!^e^HW z#j0667%w~+Z+xK#(KwpGls^d{5Jrs{6w>ZZ$L&1Y_ z#e;E;=3372Z|B(SQb~K5?oEddsD}zDf}C``k4$pkEb{+s1Bt|o3CqEM$Z?gR9R3DJ zPN?|h#ELH;^v)>~&zw4O&+ZbJ>@ji5nh=M~Jz|$}kJzN&5Zkn$#4d#y8$OIZ9*jM{ z(0MfWWRUKlF^)zbv_LhlOCXQ^jBK90C}6G&VMXLV#U`vPnI=9Z4&qhp!FrAWaVw4# z*WzSxF3uCjqAIa3Y8TtW5wR{@CRPPI#UlT_nB~4K<~cuzMK-fHd>Bi77%P03(;R<$ zF1Nq)NRy!p8hBj}g^&d)`Q%;2x68?WSQq0~Z6?muHsVd16u7EM^shVp7iEBQ4t^`lY8tujEV zbZBLtpPiZXpmO=QG~SCZA@@W(s0#fW=KOW!zl{bmt=U{Gnr+3biDy7~PRW?(lnfh^ z#GoNx^zdS&zMDZco8)C32R%yVUdLejl1A3j(V!IYV)jwCRb{jT^g<)AE6^;2OkO8d z&<1FSa7f<<^qa^(Tgm@e7h~FKD*ByV@H)A;baHa<2ml6kI~}Nir++2%rjJufM1 z6}km{HJ$h3o9P2>_z#`vqwC1wtf!NA4WU0Q3}g)_Rb#N=;%0-t;RIe(YA9O<^DHWL zjKF7(!zMc1c}{>gCw-4w>rkYZXCJ7pnUt}cI&Pr6Y8_BE?`sZ#4P z^k;Mbe=hp-U;!*tP#=q^kHyr-QsP+V4V-*=K1h~Qu*Do;@dTea!FO)(UCNL5A+rEa zWvg5J1?&peIy@e7?0{pElsGmc(gFV`j7x0HI$q`YzHhRm1AXP}%` zppTaEdbFrc*$TH!v6JntDfY762e%vGi_kVl?YSvN9LM9C9MQp!a_}ST`1ku+`|%xK z3i>->3%NgS+ezEjP|x|4H)%0_V1hBg$6Yp4|2rr@;WN2qq6tTVDwX4K2b_j8a2DH6A$A@CiSdyrhV&Z)8gBy2kJPEGKW%LZ_4LL znJk`V94Z~9aqHM0vi~W3KDGUX?PpV{_r8R0;h*p;P+b1F1H=JsqNQVNX*BH{crwZr zP@aRz_f@X#44x1OD(_E&La2dO=!IcW+0i0U+1grAnd2@v1ZUtfJWCGuCjP@Gcmm(^ z|KF(^-S1Gm#`Zb54$6bjt#?qqgB7S)oWKkI=*5)sx*po04^$lEumF~E^fl;hrpUX< zC=ZcgpCy00iZ}5J)$uu1^ds4(ZgtA5Y?c4=7$^_oe$f2}g)v*T4nuhlHlX70hA>Ei zTqwtjXh5T#Blc0gAxbxf#yoQMCFF}M$(c8zw-Y<;FwVz0%6$#*;~nzIe_*BQo)mhL zt$GgR5}b##pnLv=rXlCoJr82WR>k3px8skd`aOoq#xo9sb{tOhyDBFBmVg)wxCNl1QXx&9GxsFfpF?sv%tG3ZfpgifQ zZXi_Xj)K*wv^2UJs7pari8$_$)cnU{)-3X4O(--i*)EBjx5JQev@CiY(SkzQqB_wYXQZ%wLrZ zi|-`e0^dPl4)`%IqxS%plcP>dPh2QJ%!5&9=2wo)Z}|5f8$IcCX}0uc z9ZQhZO^cKo>qNYk3@NuMkW!lpDYj|EYw40a+hNJET_BmZ>m<$gfF#>ql0=)=B*FFv ziMM4X9$w5O)?7Q#zTisk?@k?fQukg=@z@S{Fa>sFjz>H#e5TpnK$>hVrOwV?s_osS zg6D(E973hoF$S+CMe-eUB-e@OADn6>)2Ut3oTf{%(>zIZS|f4%9p@OQixTPdibObl z53Iz)i;2LCiL}Qe<12NSRUo~-7x|m9ti;Wb!^0z14 zyd=*pP;%TOCCfcQGCVTyTM8xFqe>F-V&XjpB*tU5M0u=|aF0C_>VCHbyT2qstceN2 zgP8;$CKw;)5j0Msw>5x%5Xe*<`k)Pz?@{GX&cnZ#dXxWp8%n;nmE_>XWcs*Ey04$4 z`i4rfZ>%Kx@%syYc@pbaF42BX65-b?VSYUS;5#9KzPrTV_ni3pyeK|C--?enYYy;W zeDPp>J-B{S=ELZ13}%WR!t_6sI)Fy@tD%hdi-MSM6HiWnMlu4;B{k4il7pNjA;??e zf`eF35-Cx^i4qZ5)xc5LBZV;z~Xqn;N{{S#PbhOa#9_8R0Gw5&jYu z873i-aS{}nE&-86;ul#fK9QaH9Q+;5h^68hzFnNd&xm8#v*Hl?wK(FzI0Z9f!h>-p zp7R`gH+mCMjKyfiKn(MIsAQj|6_N`XkivV|EfN!}FA;b#p>aHa5$_^_@!sMeA0&S9 zQQ{MyB3|)%;t^jZZt-p695*D6af`%0cB|ONoD%Ekr^PzzE3siMj14}FZ5XpSe4*2k zlKnW1reAwkd;Row7hIlQ)TZ(s40Md|XTuJ{9vgW=pX=hk*}ci7#{vjg1N9 z{z;^((1H$+*GLiMKpOCqIQTx~JsISFnap>y^~F8cTwHQ(#3|Q_^&DPepA#guIniRB zlO|SpF&5eNVwT-2CRww^D08hCWE>K`^sAzm_JQc9{8tR|VT|x$jPYS^M{^CwA5Wtl zpaIGtpZ78#8Di7Pd(aNd!GFjj|1V(9U2G^er53E^uoH_CS1~X471QD{F)mINqoN!! zD5?@oVFw**lnyI> zdpY=@spMaYOG{jU7qdB+RG8xrLNiqGx&X~gNMt`cpEfAMe<&sQszARA{aStlqh2G1 zja<|kErE+olQVef26ZDHNG&RAsDU0%H1p_WTNrHT8SrlrCoxo@Uk+u=rHUC3&<*N2 zuu{sJOI@e3jio4Il-a)$eGcbTk3K~+Z(;3w8yEKuF7zFyB&=3o$BBg#d$$i;2DDn> zr55*ew(^;oob=Z*=ugq%UZt?V5F`3ie#{ceHUw?ugveKl(9J+Mf%hV+>4SCTeU0dI zD4Py)zb@AQQ#2+e7$=~-m_g#0P8>6cW5|^O=FgS^Jw#>B-~iJHVL6{Y#CM*Ex4}*glOft>zYRC0h~r>5lT-TVegyUr`0itT=1+VU{VlMLEVi4j(aVsb$r+)bU_JBf<6Bv6ZhYB&*U5;i=~PxY1f>2)WKrT#Vct8bRD+S|MzmN!*Bv_9s4}{7vXN8 zT1l$Vnc_jV55qM;MxLWiUbe!k@PZWf>0mEVxTo)By~x)+1LZS6MITU_O6MU^$9@W)fv2Z6L%|D>DKAd{} z5nH88x0_5}eq{S!nEV5q;cOXrS~|A&IK0kH=1-X6-&%j6yaxwx13w6d|KM>8VgE*z{6dbddj{nNw#ox|6dnYf=b(HC z<%6hrR2=RQ0LqI=g#xI8X6S-JP;ty5i(Q2Oq5EyppBWrK(rN!m9N&{;ev1q61zFtt zyn35YzXH#~BXAj%51>}A=$-*FU~2@*dzc0)4i5-~Sja%Dh>}#J(M0Cj$@++XJc(gS zI18Qmlyw;{!y5UBLG(Uxyh|s3n@;owdH-vC`elytG^lc40Oft$@&f3Yyt;)R`^t~8 zq6r<*^hPt3BgEsgWYWw)h zk5lINsg>VYE1`3%l=q=L2-R|X;gMoSp8EoE zJdHkD>Q78A35niBe&T%q3OS_!LWjRUTIL0>*Q)^0Bt--hr zwt!lHu@O`Zzrqib+PWP4h8WP27L>=1;pmQ*KUxv!B%+~y3!?z{t(^4rnjT`irH!{sryt;B$Bj)X={Q_tGZk z=^M<3X*W+fPTx36-hVrB?4^I~!jIX353-H6*+SmGi41!KZL^Mycr|gXF3KDNEk1D~L&PHReE zoqjn*QJ1<)&<4iJ8piBO+GIJN&{F(|#rO{kiGxg%)=#G_xp*GMT+S=eYCx-!@U&DB0(E z_Fpx`b~oS>W=!5|%B9(iX#v~0Mocw$e-tlfi2gZ9oAl!e_0TuE=%XEYWNr8$Eu2f6 z=%)?zk2?Ii8vb8J$;*kblosKyrqF{~13?eYVMJw9^O=nNOtdpVZJ$H)k~x=QE-Ra? zDLc%DAzt@eFyH6jyN!&cQ_m7jJL=Pgy7ZQMO`z23g-exwoKzU3;Im{)sbR4c8&ykz zQH$gm^+}HLm}DB`Tp4ebRO6G9Z2Xuc8hs$~#=l6s3I2nzj_YXMiRLb*V5{tz);OTg zwg=j6nSSzbjg}f|FgK9~6Dz6VH&QB1-K5;iS4z!-rPw@53e6MoS~Boj3MAX2QZg)? zB+ar{QY^Qd1OoKg`ibDfb z@w&{FITkS$I+Axd7)qYKrDWUNNrr=~q&fOXies=OIYmhVztQ&qE53de;?0d=58pv?^PMj) zz8l2R2gldvVX^c6Ko_H+*;u9P$-uz~aXKX^`-4UdxPzt$_#(Rkn3lY2yiYE7sVa^+e{}6}&5Rd;5 zkN*(wDfaOJVjCYR*73<=#kv@axN0$r>kyOJQ89{LE(S3?gAsj^B>slzMgBwd!?|OF z4`YZAvzu})k0gDI;(UNcH55W7B=cSz@EDeaqU|4tegbR1lkgu>jaXM=Dbv#I#4^oA zEa)(1sUc#L8Yf09Tro%~6TOsX4l*4UV`OcoLeDXAewBm%K*6+BE=2QGG+K*dNn1I7 zb3AE7UZ*^K`{~`ihm2?J-Z$dx}1i^qKe<(s1Yg+8ua<1F%XBku(UXkKy$)rrA=C5fCDu1;pTok8u~=T zK05t1{Fv{E6a6Z{kC|1#c?oJLV-b2;lsA!WB<}@M46icu(Y385_iMm^p;%n7sW|9> zPCX7l9NomxLmYj?(eJ4n=zTO|Zz=G>o;iGGJDZ}CS@^ovebmZZpBRiMZGf&nALr-Egxete#xA)x)i~+BaEM9IWf|KESOsf<;!REfH?iFU+o_YCjF-K{ zu{VGTY6=q+3b$jFf7{N*;c+HRzfSre>TmTl)4t`c)Ild>kva~d$~#Ti!*C2v!dVSohdC2f;N*kaFtmNpc0$_{ZT*8B z;{+(Jdq5rkVR!@{1*P*OJOfJaId~CXgSX)(d<{1bi!E$yNWgc6|OTIxc}f&TFcGi&8LsPz}hdr)ga)bk(S5DdD!U-t}1HLqKs z8>WNG(p2WAGRD=g8Fqups@2Mdhsb80CSQM(?CLY}=6|t-M)w<}kF!-Cfbt(Mf^H3l z@*R{9qGGufhY$NZ^WC}h2wFQD!7RLQ#6HI zh4D51$A4+E|2+euqC5l2gV3$LP`-omK}z+kfPM6(4 zOYEi#pQ64WrbgbSb$*>J_hHobfbt-=f@(n(gX%J>>--(QgU^WLed^;Ka?Cf#?Or3_ zev$n0DdM&&caa&Mp>5RPBsosI9O1-q5VvAKwXm03 z+cl+o%Y&HU^$Pe6O|GPq-B@*J)z#Gy^DtxAtN#+u2j7vVRI{c)b8> z_^VUF8T!Wwa^1tU$wB(Y0rLKR#Ich&wv*>>AzR%@p1Yp5Sxc6?imZ2nTz5HfEG3Rb zTtpV|?p(exo1=}1=n~ZHIcF1;_iI z`+wgLhwd+TjH_cjuFc+St>3JwS+#1;U2E+nLW}&0Av$L0*ias4%HxHOKRtoxU=sIF z#7D-{Hbu0}80;8@9fh2E7^(&2AM&v@kGzzUwHk;w4WK6}Jxl53N~Tev39YyK)&JIB z$hHJ#2sO^4OL>eK2`tFG*`npb*}W)Fd&<*^ks^fhM4{CKtyHx7pp%W44CDOc`D+e6 zdL>@mKpx^6mi|CSjqw?h{=L19mNN7zO{N@dhw*p53R55IVj%w6pEk*2{K~{P`UnT~+|ctuF95yH=!K#egC5U- zY|e;eqcUJVR!$pK?^85wO_99rTljy1)DmrV&icZ?8qJvJB2+$cK{B*{N zcHA4}t+QHq>NE;>ZSH*2{#Ll^aNh&D7&mA{e+_zbOsE61mMLjpbEdt#PBbMn;QbhE ziZbN63;Nn3QrAF)>GIo`dTm8FeFxD+-;G=g&tKrVwSfk~!rw4P_!*{%c81*lV95Os zhTQ*PI8Qhma{q(jeqnETSJ)YTC+x_>*ej5W*@6CYv}Rb~f0ncX(~Vw`2;KQRlJ`Tg zDTIB3P1Hpn`x$SHq94~ zrbWWev_#mLR*JSJ`-G**Jz-(;iY@C8$irBXhpFYZFGgFjgfG-H5ab7w!+!gSvcBx346OhVQUvGtXUUhWtT22>~e&e-6&yf zJ4>{&tq=w_yM&$%_di&FBlO9^7=R(`bJn6i2aVC_W;@}B5C`GV6*@W+8iOx*g9o^h zi*a@`5ROi!!rr;9uyuA2*3Rz2%Gpm?kc%;Mjus})spL2Y2m|L4LeFWs&~aKOv>dk! z4TsA@)8VDiA`hcY9!7^eOa=NgIB%gdebI$};EE4I2n6wW0QiD8xPy}$v7bBn2M=A* z*29Qgi@7lOv=yeFF2dN;M;Li_6b9sC^gR=Wu18;?V$`FJccL@(cVgostX2`>oaQ;yo#$!!I3g?dombQov zz;YkqjjkJ8N8YzZ+cE(Cj?Ddo)P;5z?g8m)zyS;*+@~bE;Z)ryP&al`f_HK;qH8}K zdj$E8*%Yh_2RqHlvEko27}1_tG3Or2xikIA#X&f9=I;PPl6HLN!Ire2unk1N6Z)Oe z??wz5qAJwFv^YSI#%n`IW>AZwBS+y_kr*Axj*;E@ECUMIZ#osc21h%Aec13Hd6)^D zJCAauLw5-2z&OBrJeFB_v321+JF3>IEAd|l`r&*B9!dNYEjU1(PNWU`6u<~)VlYdj z6DQz237kMlXvC+I^T>mVH1u*D{xEiaj$IPFfLx4nPgsA>p9GP-?y58;<@I6f#^3hA z=!cmpEeJc=V}ek*vairoq6D9ckFn%a{wC1%r$OTN<$!bZwF#tQVu_K2;Rax|x-JU!N#t-bpXTvCL3H#P@%u__sPY3cb1JOv0qKYDD2W;uU z)|$iM*f9nWwL@641jFNg+Y)HL#UIXnp}ACj}h3Rd;u_$PCbH3Eu`Xx zvGcGgTo9_r(cEm#eKfB{>CgB6R8=B+QPgu6T*#N*T`8{(x~Ayrp{b_m<-uS@3x+}= zjD*oZH8aU;$%#y)PNwLR3&D<=HVlB?Ow1_U6izl}6uFOO?01s=Ut=frFpOXLOrjkq zFOR?$fvL0sbBQO){Id?B+9Glv?6m0)`(ncizF(uR zLn*WWVEREJZ7`M`6y?-Zbd_=~0!3prDCbvL51U{MY=b)30lQ%@9D?I;2Hw*k|6xM# z;?4v5R57<#s{a zdNK8|l5s#OFT3&Oc}w{YuvIiq!v#>ZuESku`ZtQ^Gx!+3gm2*${0+#DzdMPJc=37`I(4}=Q299?o)#+0HhsW@k}ys66${{83zYX|bIwK4oeq;>A}D!? z@6r7Xp29=83a5Zyfk#K5{1Lx}N^Dn8yuB4Ws)_415ZA305eylzs3f70jz(W{5;;Wr z1w@x)sEMhRu?#O+i{?J8dVtp`zwXoW{7uE9mF%8^Qg)?lDP3C8ZiHG`4}23xISd)1 z%&A#h+Rufuc%uuc z`G-E((HlF`v7;w;@We$tBbru8B>L}#)<86dVcA4Hr<7J(&mPJz4S&+S@~kU1C@8DY z4x^*A|5BKt!gK(X>BvC-?h6@^&bZN&@s!A^IscH%+L%Q8Y&>IW9Cmc4U7~51NR-0) zE0n#0Il3#g)tMISgm-o%a~DWy{qaqIRwK1XTR|BC6(6iZbDjoM11+XGI!w8=nX2(R zRg?LRI@3}WHMDd%y8&f0LC*p`8}yt|b*FsoC{G}xdRH_f(M+Hh_r`N`&>2a-V+I+U za;J4JHu>!rBx=dB{nFbo54@sa*m!tKV0%KRWriKVp*A*eEhU8dy z_JXiN(~i=+kZ17}fvSEYK(&)-uNEeJ)#8MYTAJ`uA1FN33x%8dRN>7YS?KEy7CY zjIh-CNLXn9E-c8!Sdxd?Kso25Q-o%oDRp2*-GdUcD0z?0(2*%fAfNjisR@4rZP8v| zU-;^q2p3nvpt;b7`PzQvDxOJ~v6G)h>Skv%i(CrnL;2_w@f!oYN~&@>m@Ypa)r9>SfOfL!eq6IiP|wHj7+wy z-T+r^@-SuS7jfPk^m^J8>p(Dc06*{oH#_29Z~&I!2rEbC-cIBnoX9^o8+CADo!bdb=OCf(94SO_eVJ%cW_|U@>0wEL_ohlspVM7~#cT+UW5(Q=WOuc<9}d18UFT z-srjUnG;)E{ZlXmF0C;t&Z4BSDLoi*veIwTAj0VN0Iiyn^x63%Sx_}j{l_}8EOLkIMOScA|- zaDE6?QwH&t!90pKQIg8BIC*ymUUPyNc8-ZLVFDQcdV!ZGlE? zzCS3O_tS9r0`eZS*tdq`9$*{#kw7k{AEqWkI66U;(;r(r(RJc)YjjP~Rq{VtQExTb zCO|Tz0;d;sZi|5!jAscLf{&;vCoz&o{Sd z#CD?{(CdUP?Ks4RzilY58M+3PRr9T8vmTlm&=>l_0O0f@7Y0!$gSiL~#SUc==5Pl( zoi7)|2;T35^N-@9w3NZ<1jqftIk16zOfnxwq1O#t{OQu}{LRuoVUFnr3^5wLI5&qk z>)~3>dUtWo)m zV~PFK>8_O7lQP>=Ra|PD*T6LEDrGBy1yBwv;H}PDUT=VE*aBOj9(Kb%I1DG@f(q+0 zu;Yq7`3aouEDd{d5}$Lxp%>iaLPVX7ALQc)Xxq}BCKKp`|5IBj+j^*h?XU~>!a<-o z&B1}9cNX4*Yj6je;2C@dUm1}DaU-Cl(O*y(PpQ17i|qGCbfKO7&~`)H25l3{t+SA0 zmcts@2(_>S_JE>$3{Js$xB@rf9=s19!bk80d;>qh8=&chGy{Kv{0!csabM$`-+ahN zZ`WTaxd$cYHv?tugA;f`2MC2&P@5qtRzWHDIk9zi~24x+>I%rvYp}|&*Z=!Xy)0(c$6BhPo>bHcs^~u@SpjI zU#KcpcY+q@(HPY%XysR6h|rgTR_oPbjx5Kl)&$3{{UZs(uAMGOXB`-iL1WCj?al@J|(Vu zMsDhT?6^y0bAxud!eD)onC>ifa*9~yICXM_!Q&tuYA=zp@|!5iud!~UgVd0b+C+(z za+ktvm*j5#4@Z9rwjP9KA{t?8A^U7n%Yi4 zqbKF-L;7$4iusg&1g@++540G~a>~4wA>=&T|7!LaC4)8=MuFl@N*+SdS9EWI;^L>_ z0PKW1;=5Y>w1(KO8ap;&$2#J=)%fX3qPue1W(iT?BI=}!I+@RaUc$gSo3qa(a-YUP zHkrLAV0963<{0WiSxxjd2a(Nd#k-a6bQKhBWoX#~%J8EMM@xh{=htJ1GDgFavew9ZX-zqXap$+CZ{fiZ^FKFHrIj z3QF0JgEG8THS4}zf6lP7X7GcL|{9^?1_AsJmzS&3T?R_8#=%kY) z=*MMsFfCMsRxv$k6}@^dwe^JjK+F9factkB3~!3QGVCftlQKk(1Lg8G6b92*2GJ&Y zzr%?gY4}JA^^ru(oxooFN(tUOg?AS*?b$-hU8dGv zHIFtS*zlJ9=qSVBQkVf_U@#1XzQBma`86q>9vVh?AkTlmjuh-j!j1&&;7$XyxCeu? zN0A|j;L|XU4x#0{Q5RkCk|0{RBVOHsUKWV{Ta*z{8G@DeFM%RZ^!q_NBtru9pe|y` zkG13n~gkh<`vM*XPWcJvY-YTTO)m=~j) zl6h4|VQ)};a2YUqH?KeGug>%g5+Fv6xrHjRpHQJpn&{|KE?9zmR4{=&mFyX z=mkii3xLTrsf@?J+HmNgZEv**;rLL8ELSt zMO!!+>anK9NZ1&f3u{9gVP)t7n0X~o0_=Ab_UePyY9f(3oR68{4~@B~-ha{_y?F;^9B&DDh^_a~W=i!rg( z6KyPwgn^~G(6h1;I#y0X%gR$|SOo|*s}P~0EYi2igaQKAsZ7|H6Ey50aB0GxUjZ9T zcp@44L(y06#fr6Jj@_1U6nwxPoO#b4xDAPXkE*b=R2ODeJb$9CChJ?sKah*jvNa*s zVo9#WUZ~i*5uo@&mzF?E zeb(g&*5!=kynP)Qj~!_TilF3xym{@)dybsW4qMuSIe(k*zJW9O4_6hT>8>VJJQ>)$ z8OYi(i1<<$?ahEn^J6E!c0hah_JAz*8A$`rr-HX*7j5B&zAHGh_JH%Ib4aucV-hGi zAP@d_!4?NTvj%R76DGWGKvU~@5QBLO&Z)xA4CwyY5r`cf>0m*0U?r*98RLW4Q3>Qb zMMG~sAI5&g6tafn?sGi)|IWi?bKYdCE}TO;vTeuT?yi`J9k%GQ1dRL>`d;Y!5QF-m zPc?G^ zCvuW_4v42c;s((1@XMGA2BGuV_B-dmhOT_4)1K2n5IRZ|dSQzby2=_PGs>&vf3!NY z{-hgP6iGxVn$R6MHB*+Bi+n0|lCIHm(c?FSL|@t@(}@cJC(nrB{R}=ILGEJ_`yb)B zpRfrVqM289A*_$xOeA3@SODSs<#Db!k1gVe)G&2(^1~}*s*^o=y z10QU zpy-W(@t|l;f$1;{NU>)}TNjb+=MUdHE7)9_~w32uPQuw3oP7~RU#0Io= zr(#1f$CSZRP|8~c)$l*dd5rf?!v#=uZ^1+O5I%!%;b-^*a6cj2fCtdQen;>%C;$FB zA8CUQkzh;iL7Cd0psYhs)<7uF0{tC+A+~%)T=$At=4a;3zY*vD#ca6c zTS$Ml$~UsnphRmeYf}ECv%DeVdyUin#QgmS9Q9iq^eYrTCvo){r?^fQ;tUOP7=`Ve zv5KRW9Ka$_)IS>;a~7&GwQt7;all8*kZxwEo(u3Wmf+q5ym$}I4>DAJ|l+t zh{56+1JhIL<9%Y8`_#v6qP6STahdPg&yx>2&0mMHYCEN>q)`^5spLIo!c-+ zxXg2S`fr{I6>==vL}hKn3u2k)!jaEBsn-C~UtNh(BFTUBpq5j}e`Mf}{n5$8J%va%?bt>RUW(7Og_LFvx>p&n|0ZvtsQJh?lCR)nwI;W%S%fRLh&jErOb-m`HY_Rdvh-jL;Eetu&UJia$w|Y zz6=$x%>_pG=1W#zV(v`ZB$Gayi5;25{k^dxjmS2YXg8VYHxWDHu_F#2=}x;uQ{xfT zNEo#hLS1yjFS^j`o#`K)(C_$fl-JwPse}?x%AOBdkPa!32nmcI@${1(#N0jb(KyDi zSnP2>cXE^Y>$dBJ*XZ1RLQz3wEH(3(NHc2 zlVK<*`l-NZ+B{tfXUo-&^J{<(UT6TuU_meC_b0HU6LxfTcf-qEX>}KJQcBK2LCL-NfNOYx;*Z+BTq!#49-e zbQr42v`n3P*I>%1$y5_MKzsi7<$WJD?s-trq+B|nkDd{FX6RW_7CZEu$+38%>Ced8 zm3Ly$P%>8o*<%#7R!j?4al~nQ*w>VxZ%A~Zz1U066=%r~hRn=h%u1nd$54^#H z_uRBubD*UzoHew`b?C9K#6UQy8Vd(ib77}yEo@b}|3TGNv{h>-tkgOQ3$+Mgu9hTB z)v|=K>TqGCI$ap5a1RC}oj#+l9@+{9FrV^|L4N=`NnGM1^qKxcdjs^rjn~fLsHY z*5qNV3^au$_Xb($>k11!17W6TEKKyw$+ff-!5`{cb|XAWmo*^cETh z`Q$q$3KjjuOpEvB7|=lm0K*gAFi=`p-i@6|D#p!Z%ZV5Zu9uzez2Sb78J&q9)9YHH3+=mM~^L zOdDfeVPK-mx)uYWZEDP#67In;WetR>lTb19Vp7%tA|M6mai&E~m=^$7BcuD=wER2v zU<3J}BJ>BKuiT3jYDSy^-saQ+TYInuOV;9;lLIm}L7yB@8!#}{A>X4Tw9NH|nx&yo zvE&4nR$xz%=nno60*rE&{b4i(DJ6KS!yYDlCV!Ghzy@+bBhl~6;c@78wZIpd{(7)= z0$Z@+eG4<{z>M6Fsg`JCrX}>vwS=yP7He#@g<4xWjt!1v%Za#8N!Z$e3lr3K&=nZ% zZ2Pj$ND4HMN@ZlWq8^kS3~O=}3efL`Ry0KjYKt#|E3X~E8hb1)r~~p(CSYX2^Eb!^ z=~}4^O=}IIW}}4$4&_LLD$eW7$(%XR*^!E*JzP5SItDT*6r-?XF_l}5P4_rC`s84g zbvb$Hq@WvSO*>EoB?sipYdhX+%juN-lPP&9BP)IbmVA(oHTe%)RiSFnpyEWQa={Ka z?C`{~y>VWpA-!$Ejg!$HUg3O}M&;(?WHTt>1_W+$Ec&d4QSvbT(2M5~e)5OkaAG$* z%;j&TO538zV}Zy&;X1sh!{-_lQN>wsF7RM*@m8nu7+~6CM*wZYvvWlt#s@lq5ASuO zqb2cq4$eM_eb->u6^=z;$-}rXk8z+KIByV#_<$>~9ni8t(*k=;cx}LYI_Ro5>!M9H zb7^28g#Z->RqCWOc67xzlmR9LM^@IPgt#;4(jMKq^H~=AjHTn1W7}!$`i*m71G$)F z7y1L|4g@b=D>)#0?6PviebF`GeQi#q_Ext&x`EIMx3(AQ{+^Nk8dBL-yuGy*Mz9KAJj+{bsTMc8>cTyRjjZ=*gF= zM5moMzJMJL*kegE8eu-ezEJ7Rx!;yGiq}+Y^W=h4wrH~L4Vlmn212enc>+E167-Y7 z_6&lYEE|Id(jNULG8nAG&b#C_upxo({R3!H%ISk0&e+oyS2m`ec~p8!S!3Atfd5ra zUS~jG=nvVD2ZLcKP~FXwk#X2D89S6D>_jSe9Ca}|l?f_43|qwhr#bE~CQU?==qbMK=YMGQQQz;WgFCG_fV`GJ@?GD1r$v8K%Q*m_4l!vu1 z@Zmj9TrzCWMHnXjTdT4}$Z~`vC4eYpO!hIud1XGc$ zpP=#fQ&IbNku&+0-y>&CLfe9J8&GcbAsq9c+DciQ>y_=>GAg=?=2qANd*H3+d0t$aSL15jU`&1BpN(0{805^pv<#s^Z9BqBtS50YKrQC}4d{F9_YHJR_ z*0ZgKZO{O3wU6=oG+c!1a1Wlsr|>QO22w#6XcHOAS`b8ko6Eg2hxp)s|EHp+uA!-| zqpN3N*v8nz)Xc)t%G$=(&cV^i+11V6)63h(*RQ>QV26%Dox5}m4hap5h>VKq9vc^* zkeHm3+B3aZ?~Kg8S^WnL%+Adlls}|k=`Gn?mc_=9XNRC@X=$(Pn)_E%9=T3kzUd3}7|NO5{|yuPjC_V$Y3yWsd9is!deTt5Kc z@1!_?H^uwIasOz=|9j8@l9V2hrgVWm^nrd#C&*EH!C<<;^ z%vL(XJf$}*q&qBC`ol`4LsZfu)+=42TImzp=oCAYUeQRm*st`9BTC0ONzXW|bdC3v zzHyDtaZBkP_m%GPi2m_`(m_6=hkT}Vk*}0K@*SPzmC{RoRl3Qaf5}k=69SYzpr+2` zMCE_04?H+uT|2DnZeBO_zH$J7k}v9QF0+FIj7 zI1yfi8{tPdlHy6tuGHd7|HGM@eF;}eN>+waM$&!*vU3LwE*MrgYE03DNmFOcnlra_ z;o@b>SFWyHw|?X1n%Zp)m&$0l{{T+W>=XaPC0cx<*(nsSz%B3#90Sk5HSmqkKF2%0 ze2IVj>uWsZ+wZ>n9w+(n)vKTIlV4uH{`J@2@Ri^H_~Xw%|N4u^CjH-CemT!nORg}} zksECcWu2R)+!Np?4+eLAeJC>R^8;}s9@nSLy|uAd<@ptTb{tvI_ekTMei!Se54gQ) za_*Bg6AM0HI(ht$|CDz&wAA6ddU#h4|Jj$``NKPZc;^rQ-|Ne}e(|nf{BQkYzKNDx zX{sYPSsTiFS4+9q-&GzC?(+I*M9)u;bRW^QD{1bHnzXg&*7j~Vye#9`?gg2bw$AN) zZ{wVSAFQ4;DWT~;1tT54$o7)=59WEAfe|y(Ik9F((%Zc#RPfkP^KHQry z_gY=b+OwO|8V;>VKfZfq#(Q-uvTj!`8Sr@Fyqu3G&C37kpCU_*H03H2ZCPz;Aa^>M z%Y(iyZ%%X#`ss97%ExD-hTlKfqvZ0=q_wBErZgO^PCL45WA6)_*Yvruygc*aoCW=! zjV&4Y**`^=wb7KTjkV<#3w^oU(d^H|K2EPsckcMZ`LN^{7bAwx z@2yWcTwC4q?3%UdR~M}8b$9CGjK_tgeP8@j^i+Vr(r^rS6 z8gi|nmfU8lEBD!&yguP!|NX_lfUm9x_xSL3aQ@Br!e^g76SZpZk=U)(yL;@ds7p9j zx;f$Ov~`JBMy*P|l~>;L;Xg$#(bM>|N?%jf8|(aWu&vRnGp=@DUup06*`3ZYA3h4m zzj3?stmE&6t*k#5UA=a1_nnJ(#2%czHSWatO>yUkt&6`ha8=T+e~MhHqyF;-U5#IM z8fyP`)V$4i7aXlWzvbio{C)qZrsqESSDO51?7tFRUVSoh!-|7Z+vhh%@0nU3b9i)Z z%*nxFD~lo zestH&*soRQ@(;-!pkA>BYJ``RzWN-M+0lOpi z_QsZf%8N>&`e>V)>Vrd?Y7Z~#Xg>N--{9t#Z9Ojh>=J$CNALdiAA60dZ1SI3b~9+f z^h;e=6rBsM9DXWvL*B8l>VAjAxAr;^S^rOY%n$J1-L5Kb9@bE~c~x8W#uHtm^FLd7 z9g)rv4X?d2*1z;BSo+L&!t93~W=*)=dH%@jT^A3z99*7#F|?xZx$rgVXCkZqDR)R$ zU)iA|E*?=A7p`flo_V5UdPtgjHc02-jemJ1l~WgUzic;j%18c1W1e=LJna20GX_2A zRy^Qt=-kX(;ic&}A{YKsE^i{uP2T(XF;#Kox`z6`Ct7Cp(!_0(bndu9dPdFr!#j2A zE8ncK-}vVZ|FXl-!Jl;=k@Io4G5ubIj_dPb_=NPQQIr2ECu>Oe^Yy{rQ{0nwQ&n^8 z2kNHlrLp5m>C~=NdUTm4y`zexU*hmL0cnGO>DVXdRZ!M|AG!|c`&~#*#y8=4Y5$5E z{7>1>_ZoRHYIzzhojg#I%h0-rHUj{^F%Z}aq%g#Oe z%B~3+e+4I{{}Gzf^Y_S}{}egXTuUxA*O9Ai4CE#^bGa?RN$v;^lnqfmeymH#{d{Au z$&bqWExulov*Fz2K@CSo7aZOd+ z#irVFO{&l$Mq;FR2xJB#wK zY#llD-o~M$K3Fqk@@J(vGrpLeUHtLiMb0MnUqZ~g)=FP)aWa(+z7BFPHtY}Y{@sC? z?2j6fCO_QVYw4Ahnbjvt`|aI7bKsdBlX9-t6y-NnjV^q#dhFCMW*1C7FZXw8|JR}Lm~Rh9XFqRDoP57FZRwS@8PzA2WbWTLFYDa) znf-57Ps@A1YI@=GrIROoF?Gzeuf~s<_37V578`5IW#k~LEcE172V=S0$4(yX?Dxmf zu;_1&MGbtmKYr5P?I}wyZ%VH@QPKOrp2dAGY%l9~tEMdP{WbH3J)bvo)aMhYPy1@r z_*tL*UF00{56g(V*PH9gI(uWe$IJH5!<~HJ91o59`efvQ4-UsoyuCAN>3g-QHOJSd z9o)O7_xbIsvTm+lHn3^&g4}1*N``zkV)odV!)DImH zo`lT@cP8)Mwk7q{>UBM@l&ws=GkHn+tk%`| zwbnpO?lsf@<%GTYH}845esQZq=NAtkFQ(5fDn`>#&8*>gq8z4-F#TqFKXxHhNuH0w?!WtR2_4s?}pfmDQgq1_MmP4 zF0YrUt9`UVP3^;-n(CjN(9{0vvRRuK4;-wUp1XFq`O+)x{Ab=H_B;rfz5YtKWs6RS zR?RpPUR$&;qG2exhwS>uLmAtmk0;f{o{rrdf9~({#d4wg!6sGJ_jhZkzJFR<^mNoo{8JP^~zoMk@wgI zO#w5f-RfL6hWZ$KA!Jp~+0eCFr@}X+ACIg~JQ7tKi?94$n%3~Rqb=kh_Ns|X=QPEo z2RhoPe=@Q=C~X6GNYCz7KYL{^dFfp+`-NZ8gvTAH72fYsoPRrb{=gfd3p1~VFG;@~ zxjgA&bVZK~-Bn$6Ol7qM^R5v|R(OdJouEjEG<^>xqD{fPjOj)m)+Go4~X@i)#PE; z-4tpoKM*>Lq_%O1G`F569i7KYckf}+$3IWD?=(OLcI_*JLNa8Ruyh$5(Nl&-Cd=^X zL>UzmFJof=E^?BkmMpQZwIM0vyNW&|KAN9B`@z&9 z<=00K-*92bsO=}S$L%|kHR@SlC>oj?2^q3NI%V12HA%~FuTEZZtF_1} z_%kANze{Sp1``b-vBVMe?D!w;2XXWL|gKJKY8rFDt z$jD=RbH|=<=s)S&w#*rKwq%qwtx8?+cv;%g`%BWx@3s~>)l5^)C;neB z7CDXBzfAe=pYIPg+P9J0yxPiLojl~8&~9(`MyI{poly8;OqZZ|T{CKI)5cM=K3G~%@?vIA$%`pD3m#3$S=7{8)J>^UHlbhJUavwfIg|#_9`8v$h{8>3?+h^xXHhPr-)CWieru7_$v-Sq)*oQQX4aC{yR()w&|Mx3>GIp*$kZ4)w0~1OBM`yFuSDiY0>O) zFA8SO`FPNz(x-zaEO^{nOSMr!5$UI_axNq+>v^y zYICo1iz+j&%~;X*-k2o=9_1|<^kJW}(a-xVEPL9gbit$6B4_KX%Vm1%ay{*_-Awnd z{r0TG@OJp&d`JJUuY|;YbTc^T&c(=S7fwa5ICvzkwq|F-{_>i{GqcwvUn#0cy)$@O zdQ;}2%qPhU^FB;oJn3=D(%J8~7CA>x{m*h8_17Dziv|;&-w)XszdGY?_x0rf-_LJ# ziF*F9)4uD04wE49?VuGUe1)o7^w%W+G?Z!S8w{rpxt&*x2nVUM1-8+hYsyUB;H zbz4$>Dq_R31JOHXHAEj6QyYCUe^bo)zUyMIB(ILY8M89=ZrF-`_d-`@-)${_&DT)- zVzrv;v#lCxA0N=w{Qj(o-sd-L&7ZyR;r#H0XZVemJ_F7^^PbQ^%)RD(=$iRQ!?#S@ z7qN4AL*#+%y2xW0TcS@Vu^uFPed48%b?H~Tt<7jPKQ0ui&(;c+$93wej}B|8Ke?)_ z{ozA1!v`PPyI%j7d-&N`K3V%&1F`vj;G88_x-Oe}CS={%qhU2e4useD-xIMreP`tU zg!<^i(X>yaYG{miefVFzA& zXKek#Yxt@sepBY%=`?>5b`+iuS(A4vY(v)Z@apu#5nB@uMAb*LUMHln$DVGx6I#s& z+;@1FH4xVuRK?X}8sgebZI#O}^o&lvv2@=rU4rZW@Jgxt);o96CqAQRJPDjS_Fm_b zf}7nIWM2(knt3UFMcRePRY_-~*LFW0yFUCx{HD<337cEX{R;9Q+b(cL^w`;@BKcS~Jk=LcG*TcnxOTIt+=sq_db zmfk%k{pQ0$}W+;WN^VRI!anC0VoAYq^=w&za#;?1aHM! zBDr+`v4kauj`XNFdMI|oiCxi~Pi>E>Inf&4+0as7@9N=QJ^W|C`9H!RimbHc41PJP z%vw*DI~&P0zHQ~Y&hD}*qO+__NO)bIk@MBO>~YVg44Hd>)QIKR2anzG-hheQ&h(kK z>qPqO14mNk9XXh|=*0f`m1i2`)}PwK)_ z#vnJjDXf#+7#IKRnzWpki~EoNVAi1dcgGK_xH5cH^|{=lh7ymn z*n0`Nm^HSn!*FNqV4#z%XYJsQsMw#k#}E8+bNcuvE3(RN&dXVMeoFq1qoao&*;6p$ z!j8Q0H@4-@zFVEM_~G)ti{77~vE)HX=E|GJeb-!R4b3)`5PL5r=3Prps@jdUAOVhY zXK;Yr71{l#U2y|GuT35Qq%w2CjfDfM&d$u;d3gNbc`!ByeZNRE)twByQ(U5bM@BV3zb*u&1!uJO|+B?YI-P+4N5izg!cJKdL zee$>`8+$LfQJ%H_%-n&y4o}ZN-Z**0#k%nmZ)_MnZ5^tXPM^dX$}h_N)c*V=YMZj|XDmG$wy_@ak(sb$wU^xkl`qTjBAi}Q}w%^iAS-Hegf zxu@k`$@u9_Q;N!-j35V6IBeOy!l5f~wgx$c{KI@=?-jJiM)G2Hj>fXl$M(;ILG9ig z4U72xcx2{_!?9!U?@TVcvL$W(sj9v^8&_l>tzM9Se&w9u*Gp%NxjTL8q^7Zx=R6*Q z9fLlSmUC{&g2C5j&n>(&es)pQ@R`#e=S(Yml09MR{hVpJG zvCpB3)&0-TFVDR~4&?U8vXKw+=1+XocYf*P%$ZB?_no%lR%?*c4AkWUUG+cKP#0TR zhq2q*@XcX2tDjE$yMK4FYgg7`^n7|PY}k#n(Iuyk#a8ZaOxnI-Tl&EzoBEudwYLA| zqE)#!hpZ^LpS5gEQ_rO{AN5?Y^j_NB6}MW0oS~=w*COKowYuuRZEM3C2uuB6jyqd? zciz|Kt81M)eR`)`;?oBm3$EYjR($kaI%>Zx(Nz0ty_VWH_4*ob4w~xzaK_&Bt1Dg(pWF>> z|Ka0+9uGe79dhx0hgo|rg|4VN7FAQ$7~MFjuKTf}ti$NfdXS#1eTc2-bvvBgL)faK zTM?CWueSy{TT}I4<*KUBHmR$Au$#3Q#|^drb;+vDCwE+|pFZ_!cmHFb_^TiL3_kw2 z{j{xDyDnLNI&A%{gAv=u?26czyDjoq@9O9?2^+d!j9iy+rCVkC_0DT^uXU*!bGbGA zR-&r@dQmv-)Xpg4qT&X=ci!#~+D%$_E2y;W zV(_ZTCqg$3KNz-sU}Jb=I=PVey6B^kwQ(oARVSb6xVg`{z|GmM;;T}j@^qCD4{B7! z-Mtzr_s(mpHZ|#~-~PbWfyelEgvMz>Kq@9hdOgtUE zKITOCO<_mlw{$+7ysg8*w7S;tcq#hC-tY1Jp>qe+#M$>W#F>ZMI!9g`S?`v1?P^&A zvF1;&-Y2&UapaJSIDA=M>~GT2+wt14ZMC%Ztdt%>i==1ttUr8GC;a4> zHR7AVy!jArDhe9;TKx5Swla)yk(T zI^|N&q)b}b%$Ck>6Q#TFDCyI2h-@F6BRhomlbxe8WVg6<85*A=BNCJTj7g6Fy+>;7 zo20bpU((xxD_M6fsch5b9!s}ejIG#qK4xA0*{GVG zXCim*J{-2QabLuqhW{2hlKkW(@(;yrb>%`QL%H0?LRNHgk`-b8az&3wxv+QdAB(dK zKA$*r@`ocv&wDUvLiw$JQ`cVUHLLn!>fE}si3@k0>al#!iSCvAkHyp++#k91@Xo01 z2kK&W?)h(#qsW0wVGV?`R&23@fn4cjF4uH$l$9ZVva)-GT;8+Sk7fObel{b2%G094 zvb#gaR$R-Txanf%w7N6t#k-FummWBhu?|-1{G~N z(|^*gWZjK6-8xqogT-AH%rway7do*M4;+tcKS6wU^U3W5j{Juk(Q%~$k zpTl~&C0Dj3ufAFpzv|kmq;(g|Q#PMonpS)CzeSEA2cmrUU#hIbASbnsob~4Rwya0- zmbDQfa!Zf2AFI*|J}vJzt*In$$@Pf^8_taw(QtIo*u(nJQba z1)r|Yoc?e@_R?$9^EaItJ9PJv;iHf48#3kW?m_b}ZOdJ8tukxn&BeV}+?wBe_0^I- z>(3YWtv>nRB8#v=`QCjYd5{YHa)XnRtZiq_dK6E&BdqJ2hM1J^>XL?hvZ43%hs*je zyD}%Y`t+27#v|j#9Bmvm>>W>&@Z>HVrMOzXe-^nZ&S zM-F5bvF0LdSj8{QZ*pKQh<97r5acd*hj#h3F*^C%UGalIs!5ynaCP5hR~BYhpPn;h z_x>p(kJgQwaCXDU>6cdypMPUf0s6V+cP0&7d2?d++V>{pY&`SdBFE7VvxzwuQx|LS z%gxGK5Kl|FyQABmdqaYL*%z7k_5RrW=i5`JKG@j1{L=FNn~#1$@FSZik2$-l zX!7Mnql<6M8L{~Gq=NFhV+O6bHF{9xrO|`_ANJlNx{hqw+RZJN%*-INn3=YinVFfH znVFfHnQei^vSg5qDl;=Pm8;@~&pl(j&b933-?)w0a4U01L~3|`Iag%$^q&d>w^BhI zzIV^UT9hJ_sz)x?&bAw~Ak2RcN{f6LmQ#N*s%U?^M>FhvgF*I8iCN8Hrfp|)qVq^u zl;>1dNWda8kh5WaDd&B?b1t~K<*mB8mY#BRt(y5$K@jo>eD4m~$U(|+E*p`Hb+YdI zXMmsS+o+__yD?eSrxWtFH;2?h&$jAjO;j3JcjsGn)TY}H=O?(HOpfwdj0g)k>lYHY z>K>eZ!QMCbyn}bil7m;}?4JsPkv}9bP(d!%q7rAK8JScM^Um+XyhlHei}SxeNmhO| zD{FOiTsinuw^rt8y+KuLsabnjuJur6n&V_lqWeNXtp6Fe=!jLjsMHJQp}FTRgG!dH z0xDD}eK{{Je}75F?9#M+;OwAU zdViZvMSYD?TS2M$U{b#AL};edyl0x{X}jd06|Mx(ow#zMGn%rgZ}&Qp{*Y#*$=#05afJvMnd}bc}c^SMH#<|F{RYbK8>=9R^67& zI-~yR3iC1FV%u5A0+%JTT%WUgIT0)Bx!Eh4sU=G~36-;dDhOks{F+Kl@vNAd;z=Vt z)yLkgw4cWIZF)V&vggSeKGu7eMMbXuC8WP}PQrU+PBy9Ogi>)~zj|X*r%q3Bv*D;~ zz4?@7mCb@)x%0AWsqY#2lDM;q#igf|b1G;4R1iT+@g{?U;!YU_#myEP$_GOmsNYWS zWO#D=$d0?0IS*gE#VvCF7LVS{6+w^Qv*K~p^D+e)V+yt5{VJUv9qL0?O6<$k{)hYARHPuspDu|+_c#wtuznp?}wUwH5^*9~n{iRK`cP{PSc=a~Z z!PQ3`A`1_=b&ucTa&7&KKeF(&cvc){Abdy2&2~TeLz~>NYLn79xn5&R4DUs##%flu z&TmS%F=hNu^&yTR-Ofe-Ur8b^wo?%oPtcOCoMoWAa%UU;xhIDY&%Zn-di*(;cIOjL zhpJmV!RhBklEP0(=6Os>S6Ck>H|P(_wW;(d^pHDM2E^JlhXq;S{+ofA>!KiL zCU6(U3LR zXe=q`C@!cK$joXMi%sj537j;kA)rw4IJb^ z;8FAVn_tiIv#{yUJrpLd$oBfr6`i!6s=BE?()3h%pzAGn&&W^ewt1k~Et}v!)eQDw z7&%NAW*}NI1JQ^Xi0Vre)TQ^SH|4;NJ*jZyXbiA&1_8UEC-6u*fqbWIgL7+R3unOjS}v9bMA4PzfVFayzu{=XXif9W!=%3dR= zGM-Q{B*4~fk#JyNATTj{!Z9{S;Nr0cJ^?cj5-|dCaXpZd(gIl-HBgXM0Tp>g&{UKM zJ!LW&t4e`|`k$%;`%sU4C?7!nF^%iGS8#3j4T3V}B|#ek8#npEo*nLRc&`I69kPOB zj3&UvY5;ufx*)`<31ZypAjzWwGJJ|4$1e{`g0i3{Bm>$al3*bEr>etevJ7jGhb$^> zks#tO;kx%b1S#MhLFEZ_bk4AClO62aW)6pT83E&7U0~g>1sn&}f#^zNCTNw5)_Y#gDTsf3LFp9fj4rHki!fR!?FbuxOYRM=ut?J=Z08qDG0UD z_~Gkp{@%sc<+)9$|3lO0h&%d8@i(+GQm(4yXIxP(&AFsdoqs{Lsqj3xt9V6vsPwet ziLyEIiSm<@Qzd_*|GS6(snh?};lFioIz$UT2k0R55Cg`lRGo)d}gz@;?!{AOrcCkqX~W4+%#%K^o@{$PhXN88V!Zt||fXMjGG4Y%SmT zx_dl!@ejRY6Bd8XG$!Mcep3FbW=6?b)x64OrP7*H@^$qKa_voX4BC(@<@HJ z{E3=B5xDNB0e{Ru{LE=39o`5T?Aswn@F3(!aX_}RIHc*Te~-1Yd>!oK_0ZEh;-*7T z%4Lg)+*QN4($m^0)r;!cjdLo+tuu->9aHk{-J|5r-afgW&JKlvmKNoahCdOw?WOro z1|k~yLkjYs>|@&?Utm8JNU}kJq8Q|8tA9^6vwRih;C;aNk4opA5v$97fuH2FPXE zig~qtP$GT|O5{bLP*d%Drm^L#1Y4hbA+9lNo<3PC4uNH-%)=XI4C6Y+wNnR1)$>P( zRBFaM6ze7$l^T!NsJ0H4Yjky&X!W)IiNIqwHRcbfF^7TK|9$k3&$IyZB3?uK7!zUUkzB0bOI(QG+8E=_mTlN++8QtES6zs%G-EtEaMc8%|^xv<;*h zc6I-Wzzb^-ik>?W?;{(tImL%JK^1bbdSr7AG6LTl6cs38sMdfUsQN>)ULFH_cQQdfgNy|{YX=m@B2)vO${Cww~ zg!hqyGf{eQBh<3(geJkG&?3e6wM9YUZL_-RlPZ1hoB8GmD@k@mv*E7wqyD~~-JT&s zO)jzHRSp@`MRuigX%^)Ru_op7QD(K1krvG(5ti+JesbEl&BEkNI0iA<^yxmY{z!SCG?TpxO6 zWuEsb8Q#USK^H3xFYTKh+J8D9F1`GW{#?rlFr{g^`=EL1e7kwQ{PkGx{ z&3HRBobYyN9sCo4FEWs*4U~|E_fd%bs74;t!npO{F75+g`o%cj4w0pv3@hm0>``|+ zSFaa)vdA>AFT=W~F~PCDG}2=rGsJ%^J|JQ`%r|-7->2}Dn|sMA7uU*J7nk}mSC^Kd zKN0w05AePFzv&E={}lhsKp+olKeG8pALqVL!@|d2osbcKI4-Asbx6f!sYN^bM1^5) zN1jD>b(&pUUcAddQk3_J@UY-%zu?$;x1j7(_WmVHHr^F;c3$=4_MR=ne4c)we3wS#P`zGubE!#XX|7dk zdYVIDbfWw5fLOmN*Qkg&+sM>K^N7+@rok0+<^gpRmj2BneXVC1*c1s1n}Orj=P+V^E$~YSx^XZ`~V` z9qp^A~kuE9Ueg>Lv`snn(Ub5XeCJcRUrv(>yAQ`_-63Xx~Wn zeP|c``^lr*UMz4Od~i;L^V)eaxzmd>HYdguL)!Y((#l$OO4I8M8>1@Bx_wJ*h8+r= z$4zs6rgd^6=2WvY7L+q9XVsJH#x>)cNB%?*Oh@rPiA1_vKqB3&r6%3%p`(0rVk^zd z`2(9Co@3j6drgq>@|v*p{5c7W!8vk3!W%QYlHcKMUCBY-75D< zm5QKg*^0zj>B`D!xq|vJ#jKX$KM{mdQ9Mp1NH=i@!__(}(qFx_6xU}Ms2-i&L3eA7 zar4@34yLm=`J_%>5jO5VC+<_ZARV7^Qa&&8xJs4RkXDOruYR{dhv|@Ni_HmgquYd7 zL(rs9L)xTRL(`~aMeE?7=z9b~x|>cAf8lqRE;djS7y4<4%X9RkYik=QFWue0`OG6$ z=9&9kQp2~n4Vtg=yA@s#j!eMap#C#5B@Vdz!DvvqO|3@*`GekoXq(xHK&#^@Z)?CP zUq|ksKv!q)pXfylG8pvU7cc{Hwuu5W5Y)ukd0OK9Uknt>FZOJke$K=?{FFnY{V}&r zU{`u(C!0J6`~BiF*K_oC64{SpF-fW&B-Q|K|=0&5vs4D(`fx6y6xxl3$uR zNIti57J2IICh*wH<4-h!edw#eYvWmzdi4KQ=>JPs2vYtH3YzqPHf&0OJ-Z@->2LrX zWAy+IZU^8I#9b6(rXVJz4^m{@L7|`q%1X+hsj2{a8nXYIXi5LD)Di#gpfB>x&G1ju zhtFg)`u`f-i&~0($VdO5d66I`-6p7_fsP>*c5L;7L%ZF9>7YIG2TR~&H341@eGuZ- z1~EQ$kQPt@IUz+*5|IOSF&WSmmjWY6aj=vY0ecKc|6a|QMXAOb6!&8W<0NJ<&fq8M zuHt9R?&IgofRe@&Hg9l&y<2SI&`xvY4@SUxKo>X;X#&p?H4tD_1`%cj5NDMI>0>e= z$0h|z9O9tPDGIvW!eGqvC#u346d?o2#2O?bi;Bc=patH-&$&G&NX|e|*#SMRC2V6b zfjyh_;J{WbIJ{jAn06`y>n;W0*dqtrd&$7JPYMM0OMu7$F%Ul}0@8;BLH@7+s2uqd zfz?r3a5+K;K1UfKh;=iBaqomM(L)d-&kjM_!r*Ns4=(Q7KWqZcKbb~4yfsL4f2p16 z`&2DI=&^EH*aL;y$oq1w(RXEf<8Dcf#9xz`NW3gIn{;0ERPtGovq{Ths|ml;|J}p? z)ad_-I#?f}1=mA#;CF-`LRmIJ1lJCT6gdbHa%>Q$DGdG=^5E{G^S6Va#RrRU=T}DY z-p_Q>0v~DQhTT^wiMp#;9d}!!<@xq4f_vki88ZWbNz&>$)N zu2yF3EwzHA>q-@ASLGWruaG-)E=dpPtw@d+EQ?PU&WXJ#w*99r%H#&vju%}3%S1&I3NS@LjV7BXHU#Q z21q)#1ycFgXGMMTA2l_s)eOz z6{{+jO4Vb*qT9OUCi}=XBD_m(_D?7nI7I=H#1O zC&|4XgED>X-Eu=sZHmWh8QtvnekX9=LyZ{-^u6dc6Oad`Gj4($?p=_FtTtDc z7jo3df2SJheTuPhcoyUqc*nyxe$754d&MlebV)y{Zca0+bxNhQ>!eCs&!9p_Z@YX~ zPoq*_TdnGFV}<&O>N3rVir)!bkb(H2_l`vGpM<%XtRovCpJN9U2_J?c8SZ}zm8HLA z>*>Btws3eF=@fL!&ok+gOF;fMllYctgY3>py{g_3?biNwwYI?;m9~L$_3o}> z&4HEz?a{hCow2Il30$!T0qA>wW*}0qA34aRirBV8nb1Kfm*V`pOi}VY76Wcm!p5E1OQqRq^kK8cFsq)$-zRtJJlg6d2myO0^8W5b2P1 z%HN}S(%rvy$T6a|%QmsQ*)nIK*1U4K#JFiR6LT=B$Y7F;y1NpL2b$weM(Teja7X?S zgq|}Fy?+MwqY!7J0-01T&)y#m;;bJVWkp{$s%ShY*R{QxZ5n(g-Zo__)TL{8iu=#fa&#jaYiu@swff91CAElTRsynpt zpBDC=pF4yY-*ih0Jnm6Yy4j*(u~McVFr8(VG?-vp&>HDnT@~cjQs5iZo$eVu80VID zJlv&p+~2Wo!pjc%hke_Cw|!5CxBWoN?*yKkDItuW;@>2Cif_4Ciwb;38V)f0-F|HQ zhhBlhF9#%f9t_FJUF}jeTdvXd8!s?U=uWfBtB-T2Dvoe(&ItDFj`t5A4D(4m?&p<1 z?(R`L;e`Cd*`;;R#ihH;#ihUXcLFb*g>YJmuPL+??+X|xzg6M$(Y%-bOE>e@w}ZR~ zo*ftGygN>ozSOU5GT)@-Gg4*{*OqIRTbXK8nG@&Slo;vN6&@Ni=o=Js+%+I`+&-Xs z(#pGT+{UYA$kwyF+s>n}?RNri{Ou9c6mQe0NzaNf15mq(>Yw&qw4eHqZhm!wYwzQe zBCNM&q{UW86bz?3)IIxZbz>SzjI)b!EX&hU?Hi*K+&cYZ{03Yj!$)nyQ^w80OC}71 z>n2PBTZYa2ySmN&`r3ad@MWO*CyIjfB!hx^~U>^-8x$|qpGU)GjmEzOXKsb>w_~L+dWb}`)refMobdoPUyzxjcdf! zk84M@4C{q;^%#Wqw*OAxM@R7qeed0$`GYbl()AWv(xbtRRIev@(>-2f-h6wNZ|~)E zA_5C@(ptkO6r7uTRl`eKwbRn-42mKu%xZm#ZCV}joqJ4ke1^2MB1ToRGET^6)t*pF zX&F+B@9Nfw?QQ>^Ab^_mJf0xkLcevjf|6Kkr6FA%rKfnlu$Aim>Os2ee{pSFxg^Lv zeMVfhZ-=nEmxLAY znnZN7b?tuX_FEd>LD&7HlXRlV)v zr2{R$(}yt3V4(NDgr4tQH3|1XP!VUwXo%Ib^u+lGTd9`r9o;c?hm&pKCZ9~>nxIj^ zc~SR-Gm_x}i{uoiS*3i_Da~@tF@rj}Ve@9O0mpX!KEF=xzW6S#f$9$4?!jih*5QWV z>2W01APujN@7_=0d+(nah`DhpV(}auvG8Ow&BT*~y9OQ|V{3W9EmeMxMeD)8>{-A?#3d6 zK@PL{-web=H;EXZpd`juXo(Z|Hq!OJ-nYB?70a>8mt3MbFSu3XpYxamKjm|DyD#c% zbzLS*|FUA7%8FWw%xRr0kyA!_yz^Ft9J4OPth0e7EOQyfEc11Rztc6Gg=OS0C((C~ zqVF8+MGi1ULG-QCkh*R$&^LbCv%CBg6Lan-E`g*^+~lxNyc#|q`Hh|4h}c>@lXlgA zpy;i3TO&~Zx?Y&%nrXDqC7U?j3oeNq=lzmdS7TF{SF?YsRm`GH7hnbh&p`AvV-8^u zIlwH5Xu3c}QFDuqzU1FsJ97R#dN>(4I3n;2h9B^fT|q#__8(Cl)2~uSdLI-m)Zb{> zD!$NnlzD3AD*njUQ|N)K5AQucKh8VRfo!)^gMO#^EWAGY|1RV(Ey!W&Mv+0F|1Z5n zL6Lu#nl2r-Z%u&1`y${NlRt3cK1?0~dk_$_08ts-L!qDzaw=+|rlI(cu8!+IxE<-(F6`|(TAhkwSZ-(8nEqA2ClvGz_*_a zLIiuP4aMLGZ~n+NCE3M31HtY3S2vcfOn?=2=3wo z(cL^CwTBbr_p*cXZw1DTv|w|T4qTWR!1LH9@aEYL-eUW~Q;`K+b$P+w7SWQI0vLyC zf?mAg56ukoZ>j~>pOq@?Kgu^ay_fB9eJ#`H{#Gw>&De#GGSI7hDp|IPM;}O@zXQM8QE=QjiUWqv? zdI>+dbuDyW@16J|T53d4zl+ zZisv_c0Ga30UB^c|L=z!B11M4G664YW6T@9E|I+#x97 zzC~2@O~a&=HSO%|i>f7sD++aGXJmV-7Nn2YjA0&UkbJVNTmC11SumfKVCH%Phh0?Q zg}yfg{cbGQA_e(e7BZ<^fxVC`$?|u$qR_{59i>-s=BAHA9Ncevd4yka3`jj^5s`n& zII(O-Kf7i^r@VettF@_5eW0aDd7!0Qb)>#beY~nzbE+g?dp<8udogD{f#Xgp@J8Po zhJH65Ymtuq$YtIHg~+6e#F@Sn$_u>B(@=PtYGiUJ%G&*kzjNeSH}AAL`{05J%joi9 z)0Em?!~Dh${o2-My}tHxt={$mt%2rTo#VAxx)T-Y`ZI-T`g8f~37oJGzCS%DdcH)g zM;7wAf}^-6mTTwV<)TMEmXmp3ma51-$k8{tmSFCFF4R6^&c{7<%+;@8z&@;^!#cjM z$vm^U#@WbmzqW@1u51xZ; zs2HEoO7`tvYlIKIt(E3_TB9g)r&!0}Qo4!jsc7r)i6G~c0dKGTcGtj)ddJARa@*vV zLaY3aY>T?C6tk|L7~}4)DAU2_NVDU$;pQjH!p)~k))Tm3AN(08Fn>VtZz|qPKK7$* zKi!WS)-9hJ1@^ycmSTU@BrkEJN>gVg*U)(;(IRv>!alh(&@Hdt+qb;TEwnDrDXt~m zKC?5yuDmDOwxc)LqO&K+s=qbRYNS5EdZHr0`ef;P0$2R)fq4BGI*Kpp3=}^KaW<>= z(0*%R+VrlKXV3Evah7|XvLb5@YFf+1dX5w6rop{&Hc3t4PPr92($z#fK7{joO_w zpIeS>c-_Uh^GTn`(cArG{)??D>hqP__M>@5K^-X;2{p0yIfdb_W$8h_wQ+u-Eur2C zoqis673&GyH&Q?_73pmPCFxNPHR)+N1I5>d zZB!pR5758pXWRC0RPf-9Q7N94ZbjAUdQF>w5`F)sY}5F%Wb5qgSf`SN2+x|Zkicf& z!01klvGv;fRrFi=wD((i^>kW$4mR6(oT#>SAFo_b;K4xgZzzfMBnf?I9wq5k6)ovS z>n6&#y?bb$jxuk$dy;SW+N3z==>d7A@m4jj}JY4#El2y%d7DkxxNftDzxo zbkUPu4R5D=rZ*ecxwR7wnR+faAr>CW)`$y!B8oE{8ikh{YAt4$CsN zHm4eLi*KW7Q%ti!V?hgFV|TM)^=O@F$#|t$(Pa60dJ#+zHxmfrJhGrA^nCM}!b=U(S$9cTK5s?bByL&SHDF#o*m+td-fU7kLu=ePU;czm zsraZz75{KVE!R*^9mil>EmzNYB~Qy#32)*3wh+Yl;Lq=I zP!lIF(-X%Z?4s+v$F!&EE*E?0ZC=Ur8vjX86)t_*C|2ma zRTB3(*R*42gEN@Vq-QXlZA)i5eKv*Z)VbvKbOp003+VsHa25uT1$CqE>%jbf>nTd2 z`3fyX&HJqk#qSR8%6fN&2gCp7Jm&j@m(S&gpqR}MQL@PoDJ7jB3L2_^YwG>nIU)7Y z+FazVt2O`Y06VT1agJ=y^PHKVb-6G-9dlhz(;4{tu@7zN|LbuUDu*zKFoQlA&tYU< zr=&>ww1GAnc5e;^#@)WacE}a?KiC34rx}RgE=);bO^_8=1{G=CIUy$nW=dk#78w$83BS5|IN%%o2n@ z&Vl`h4-Nbo2ZmvJB=LdqLqQYbdqp@UJMR12m{jwL15j; z2ke`;fon56@NGE;!dsa^a@$c**nR|*cdRGSW~K!bCOWWYrU!erjo`?;4ICu)f{pSK zFgIWaV+VfF^%V!rNLf%xRRYC)HIS>obtp|bAl9T{Fod6hkBW?@F=uwgiDCgoQdCGJ_wjNj1bQt|D)o=1yV$|%N_=LqP(K*YfBFk0} zg;%Zb3SF_jA#~GbP3WH0MUltm7et=?QlQ630~W|2?0+&TmJQ&>xdq&WcY}-E5pd9A z2Wu+9+HTt2JZ~jfG-1@V8qy0y6m(y$ML6>KeC)^&2&v@JwUGlmia^Cxj z&|lu?MQ(bW5xeKQDDl{7J%Q0-8n8hI;c^f;2GswIx^6l>ky$hCPsm+AL;Bz4^HuK0A|b>u*oMa~DWimU~l z7QgAYAbHn&TKbXu)H(vwgVbP${@)Ebi0{vxImo1fd3QjN_yO=&JO)0x0^n*R19l$j z|5yYYeKU%+{-~4Y^hQ13O$OcnJZC) zve&}8BZgxO#3C1qmSKVjHNL-tjATA}*=fCX^0atv z9pv)ZB-;O;UP|~ajlB44%9W{C->#2vc)qMva zkDC^up;ruIOowG_2lG(yumH)~+&HUZ*@&TB$alSE{j;S)_R`C13kOT%PvD zUkYq@P=fnU-;16*3hyNe=OP39kM=|bizC`bI>>`zuO?Eq)oT7yh*2{vPOHXsz_tJGEZx|I9q2SH$(SydYb-f;(7wR z?Udm0({rN#kHLFMK_-%o{U|uL`Ad=D-nT_ktWOG*MDC>Ps$PyUvswPd1ihtubP~ct=O32$w^LGjJ#t-GZyIxjGFg_@k7r36MrL>Y{Y(5uZ<#{~7DZJOy zGqJ@vAgk6sqOi<5u`J&*rz*?5vNqMMr#{Yjq(0hYyfV^sx;VmYAvetIOxk(^hs_k= zjn@yOA$>}uCwi-rYy}atvbP`xIWstwIS4UxG~W3M0KF$RB3?Ke7?Wca>jZBN4%yl z4e3iH73pO%4e5362GZZ9+bKWQ9H4#G#J2fStKhzytz@?IRVuRc`MSoVDJHI6G1ejV zp^ovT{vMgRUj7BC?h$3NE=kp4j(PQg4)u*b_CrmcwxhM4c9UfucC!T@c1xM-37l~j z{3uCpqDaL3G)m%r0WIlGYIWW2LOqB243mK3 zMC+K$XvehpaF4vO;DA!!fT(I$-_&}0pOPjk@6Kim&*273kMT+?_vvD5x5b?G1g>j_ajv=kQ9dQE;h|NwAqn+n!TC*wLCwwj0mF@k{^M1~zSG4f-V3>?oL>pt zs7Y@^2;vU<-oMaqT`r{}E;rE<*9I7fJCi#|YqPAhXD5UXP7g^74Rk1}HPvfam6Ypw zWfYi1#AaJ11*JJ;dn9=j+rPBZZYDCpFsYMPqXoihdX@^Xe=mst1t|#!I zAUzJl8YCir!1vy(<&?x~GYxTZh>p0rxPiF1$Vhc+hJWw)xCC!E@}SxdRkQpCZMUQ< zgOIQivv}_Un+%5>=K`}#pEBL_@M_i6)Oz`piUxAZP`yIJc%@Rzbg^pWLhgF{$BQ5y zL=eOpdcIZke5Wh$3`Q#zadsHbV4R{S&Yd|(F}uXQV`Ns8qitM9wtPs@Ftc0DDW+8? zAfUl8+NH)k#j?UKN3YbqShYB)f?SwTEm2ThD_YQ7Bat&%E}cG8L{47JAt(J>?|g9< zk;7a>@4bxg-WRG!#9SLCF+WO8oLZtK7BB23CRR8$_nj7GX;_dDFFYx$m3&;uI&4tg z)3aA6)V9+o-l)woUA@^UPqxvwSgby#OrS2mg14@tg1>sIM5tsgUo>wib3Hu?#0&;z zAkL!ioWu9dC-L3;LccUAJdJ{K5mkrIAWVFG31#eFc6W?)t_C+-q%*h(KDaN)p9C}xAsixdb)@6coFAu z0XfVhdahCQokLy7AjT+&!PB(F;Pnke=dFWu4L3M;7hmURPhAt148J6<;dNfd-1eM; zv*8(aKec83Fqu=9vBHb4N!;_HX>4;@87woc8O+mXQdy6$B(e8hh-dHpwQk@noKC^t zkFzk0=VAI9F^7P@uLHjc(sq`HXt~Zns(HAVzW5=_?#u_=%(3_R1p@C0$-3SV*Rs4t zHr2bSY_EJn+e7NQslUKA=TMHnLLynNWJNPx>Wn^m@nRI?>A%96XRd`Z&s_hdE@B^M zun!~XeY>mBgQNd%97F~&O(LpSsEP6$^c4B8cG0K5V%!n;it}*DD_#z-mx96$FU4fc zUP!CxzEILpeW7hkeqm-Q`rOH$?^%!w`;#o- zwC93^ZURvd=ro8M|V-<<7cA7$8y>+?2#u~DHE;eEQ*l)`CdfA-u(GAqiUupum zWDhcF~1892Ae1MdzQ5ZWaU;(LTab{{{e9^eMO zLmXg!gcTf*GJ!ACVTfZs1o6LACDxz-8Ay5$WgIS z2YsNW(8l{vhn>{Qu$NXI4$_g~D1#(0ZxBQ7AOzf-`G9{5H;8Uy2bt}xptOStw09l` zCIz?4{v{12jBvn3fZm=-7ajo*6h8jso9?Lm;woKS*!f1In9ruOm=qrTq_s zFl40%Q;rQ_#=iwjBzJ+n>H*L)VFFcW4p0c>1#+AaNM+%1n=%OyZIlL)9vKiGkp-b~ zIS`tb|3`39;j7>og%1K36kiMcrTARnmf|CU2a5Luo+{oFc&Tth@QwVh1S%{vpo=WV zn1v3^*f)SV-xe^F*ab$)2SCS&3Dg}qK-r%U8uC$s~s<@SS_784j+ae|JAAgG6mgK{DnR};jCwZDsv z>wXlS)q5kdr2kxa)!>oPn&Dl+TZY$#9vWT|ex`px%*7_ zVWbBqwoTw9upJy^_Jg%L6PTHCfuV~K=mbiEdW<|MXQ+UD5#rfeWMb_d#RAB@gBQ7v^5C%gZ8PEw=2K8jkAIiCU-xbP@zsWY5Aq%p6FF9)S zQv9UdW6?#2yCN%&H-xV`T@t?IxFYt*c3J$H)x6|u^BJjkroR&CBLn&A{~h)s2f<8= zH}=Dadn;z7_JF&>QE=Af0$XblFn5>z+b~G&t4^%mC-rpG_sWGm>it+*j+ZVYu;2?F5@=YFSQ?l}bDw$kqAY zlj#b)Ejbo)O?)xzg2-ybY4N{8XQgfjOvv2x9w9$+ACP9r1Yt%f3|VQIzzzsOCKaf{{?*%1b=()p?OGRNnVvyHHi|_vyp>XVdu#V6RZwrM-&QAKbt-Woo8V7q`)Qt{0r;!q| zteO|Ipj44CqtKdkQhqdLT>f-wzwEi>4uy-c%}Q$#b;>t_t5om!RjA$f{*}N2Yv8zr z0=&@k1)&Fz#JmQb(R7Df8Zm(wDswV4;83!_jWu!P9HfDj;OU zEF!wkC?T;^FDtD@r!=!ct2w7eb2PV1VbrGqa#7Y1`@>Mzti?m{hh@~|2~&z>x*3RgAcM4IIksXORa>P>MZ!#+D^E;cnvxF zgml>i$Fx{PC)Jsxq*oZ_WfvRN`kb_p1X(*MzvLgHeO=7H=}C$3p4)}8td}!1B$ne0wN8dwS`Ya; zdUUzF2RAwS$5h*eC6`#mW#pM>=4P6f7NnT86(yLQERHsw%a1f!&ImVMNeVN)6ccK; z7V#^AE!N+in)Ef8l6VnIO*~JdCw)h{{N{P05Y7E+~w$Y$y)194!g7m?;XdIF;pZc{au0 z@@@oLF z*q%vqw>_WWZhI-_R|0zm5}@aNgueH7B8j+>MMc~wr6XR|ZY4czIY4!@lY{5QW!AzjY~>lDJqwLtJU4CvFXGA}$Xz zQJwA;+F7NRNyKtBW*|Cfh!s2w za%O5jX=a>z%kZ!ma~txY$`%#1oLVijgmMFy&>}N`?>w7G$1LYW^K|bFz0|N=wdCYN zg~ZZga$;YxT>N~#Ld;U8a^$&WmGJZNztSfUg1C#%#3l6Jr!j}IfdAK@tHlgN7oNeu zeV7aLJBjfb_6>a#LWdhiqy2&Gjl_YY)g}7honK#ZN&f^Ns<3bMZfhAKjp8T%VITZL;jIK|1=`KCzJ zL}v%9@g(Qzx_SNba)&%t0H#*oAGqyO(f@B8x{M#BhlfH_K{ z`aCUBes41=|IQ)0wA<`EV{Y*s4Y(=F<9c06((0O=vi?<7J(V?GbLlJQc0!k3T)8iX zcpkfu>BGF*?#p=Yyf@R*125+3haN2BkKKNybNEb7A`k1wKD3wNwb6Ul^x#!hkdBP3_=n1A$JOW z=xJmSm#K)@#|)(K_q%BV-ZO6Ue9y7d@jc%G%lE=eM(-urwLZx6Du2)rB7ZOv7yn=< zBk<8%p6f%L^0D{jYRqrPG#FoA*I<13OoQ?E2lZcR6l>6dHK@bqp$uz~k2T0Rf!Ch@ z{~1K!QyP-@KijF?{yjkN@Gr||%YQj{82`(^SNC5L+=VH9Q~?xNWI&5e3{1EL!GRZp z;ewdG5N3fYJfMAC{K)sq5=TGW`K7vW4(f0g%g}%4;T)u)|Bk~PMmW|W;1V*3o0x-m zP9p4K6NNSUY%@4SX9&z2b%1?~I`D4C48|^5kk}&ua{EO<{UARW9pM5yCN}V4VTRaa zN1%}H5Oi@IfJM$w? zp#!_9G|=lS!%;eUU}2B}j*Sw)yIBN;w(w)#fg2Qeu!Ghv%wFs{3Ql_ug8%+~5Px6~ zBbBQ_8<^@;IT>&j@R(ocacFnK?d>me;9-+P?Hp4BZVAnqm;pW zk$`>FqHu^t2pDPk;20ejaMH5@KLay}Z9D>Un+}4;=DlFFWf$0P-2rafex+ROK??RD zdI+zNbLfFRu)jY4A2?`s13&E!5Tn})a`c-)^_K#fV>Fqihbt2 zCiaf^j@V1yM`BO-o{K%;dn{X1-|{S0tI9sKN*A`=0Se)AS3RLxZZXf z=qT&~b^Sx2WXBA$KI|YB$phl4{2)>&1VS|;AlQacph0oqpOgUp1xesPE%}Y_g49R8 zt5UD|Zc9Joeb%?wZJJ)AMC13fYxdOmfbWk zAFu_}P4>Ty_d3oPopAbLc+usH;dR#!2KU@v89a7-qW{YMf#C<&yGEa#ZyHZI4w+8d zUooArn-6e92jVZq0%7PuqA+vED04xKE-yr*7YlQehCqL1@QKj|k4!^wF0uG+UvD#Q z)8X*bYPrjIi#2XjX4^f#njWHmF+T6}!RV^*Ys0&KPYg%>hYer*-8LTgzH0i#^OD(= z`#Fngm$Mc#^8%hY{%roA?LnA5mm^UD_b6%cK!ULl#Mnte1YH?|BXoZIC7b;8%%@Gc zRys~PHoJYYT}+>#_4$3UTp#$(Y!LdyFp>d|4 z{WI;xy^7t&Tx)z@JGKVDwC%<=&H=`##pd`&rU#N9m|RG?Z~Ry4b(1H_7c5@Kow6E_ zK4Seje80`t;N7<019sVd_n!~&6Jv%j%-*r+AW|@MXG-F}7-de#)#Zb1bFrB;M}=>R zzSZ0%4FHF2+)029WwmRdQ<@T&A7RRzLSzN={&OOOF zN_&yE&*n|y4!id;TkI#IHaLC>UGF##{Z-QE@;Gmq+w+WW_t(jbJ>JE2dc2RF4+zFBDKijfj^I4mb=Z%}dO4!`RB&G>M2 zomY4HYLC@b{qDPKdfhM7cDvuM>hye2ywGbjw}t*Zqlx}HsnPpQ;(S0T`iEE?Kb@6) zFJLF%%h5m73NcMJNwZG0sByh+H57W>Xe)Q8(nJ4pexUuSj7Z-@i3yRrV$zc~N91L# z4Jj)a2&^mZ^>450_U*0h^xj;*(ECI~v-kD-2H$(-wZ0<-)qYR1EB#)kRrY%KQo!g`(kD?R?R^s8+j>T7K$;CEmCBEwpf>UxZO(ndcCXe*)l)d!+BxeyV7GK zHYFt|4#s9>t%xct><+Ii>j-JCZVBqDZwOr5SQButxgubwsVwkzRY}lrNpaBQyuzSo z8HGVF(&q!hasCr1X7aF|oxEJiL!R}CQ63DavEEp2B6_aV zUgL0+r{#{S0FQOW;URswG4Wj)$r)`aSq1fpg=H0SRW(I1tqr-+%Ud#{wzj55oo`Et zx?Z0Wb+;lp>OoO*)M!p})RXM_fGG42DfrwyA2T2Ro`1g)_dhIRBg4I%WOR)Hxwlq{ zWoV_L@Tndf)&1>mW}6#)T~}3x1oae0#V*WCNUzUI&#y?&D=SPXugOYmY)pyoZi$cE z*cKCaav>w`YGVw#kC@oul9<@Xc`>o0Ir9P0EaX!f=H5c|4^^|z-tqi}#jNB`ANC$> z;wHB?$unJCqbG2z&q`@mm$UKuHoDV_`oMtB%5X+gNo;CmK}ud>Zgy!_R&jN5dTm2Y zYG+GC%3xba%8`X3DML-6$+xP*lJ1v=CqBv#Pkfv^9}q(!v$^*Xx}ZBXnEP5OhB`%#9rv6D-?#mUL-n%w&~n#yciZL8bg>uS^4{a8~xx+|&?IxP$vNcW?ZT+5Z)l2s9p8z8{^5i+Z|@Nk+4lW3-J0Ew zRt4MK-P1Pt1TxkKF@n}arg*MO$aYwnRb<&$R&Lb0uu5M99jEsvKSC^u?M^ znX8r^w>8Q>>};2Cz|%8)uV1k5?ofuy&X{EDZE0D?TZ;3kn_G*tHmxaE-*~oIYwe>v zoq;FW)a5VI^_IOzn~xFvZn}c&a2(fRb`QdK%>SFP7i8Tw+zWAtLROt;CH*(KNzY9= z=7ra_cx$d2Nf%zR)J(f*XTmt|Y8P~t?%{blDA4|7RHXUwlz9DPMJZZ%lC#?3HJK_0 z&u6Iadzq@f<8_ke#<6(K4e#RS;|{LF8T4TX(1&fq?7MyiW?;;o{kt#+A4LyT4_YwaWL|`?QZRQ%g@>7W;osCMq(iKdO^6x)s`rg zD;uJfE)PX3UHBNOeDYJc%Aqfzst4!iGTskI8u9w^eprtlW?&VLy#>#L*oS)oPGSzf z!i)}rom9Q#rxd@CVas{0#*^}#D$00fA{+e7TGjiRv##?~x{3AE5Sq!;cn9i}d{>Rp z7Ek3zo9IdpZ+j`;|LUc9^M|L>`D<0Q_(F094%Sc@z1eptE=GcfkUv>d^` z2$+8>uFv*Cu#(&f9!kc9I7`xm5@*bW7GLPNfr#I@nUu$bjlAQ8t14~6U(0kNT2Fr> z!$@nQ+Dvu4*HZEQ2`l-r_cVp)Kdlr;z*6zy{OrX#*no4;kG0r?b5D5thLjxC$VkI7Sqa!DCk7+(V(@HUR^q+TjrYPrT!T8S!&0onob$L31lJ(;FC2Rm z9mGp?5SafxfR*4r5Q-BBG1-9xItW>G5K3$&pw6xjI-J^I#D)DA3zWf?R}KRCr65i~ z3?CK>L%pyd^oa<-;JozU8Z1NyQj7Ou$st0ru@@%s8X-|Qeh^;2&nrUQ-l2o|PKYfK zLIWmZ0bIlscqv98NYMvTbP$rvS|G=w3QDYs=pAH1pIs6xIYhykQxN>Pcp-`#1Iq#~ z+`K#=?dU-2@cXC)*C6`@j*m4Ei8T=Lh!8Kley8{7KRy#;@rw{+{22@oJwyj6L<`u6 zI{rK=I5$clK#>PwiZqBZiGehe5GXS9gC@%YFks;XOIB8JVPye#w)w!LRao(LGHm!d z8Fp~e)$TM zMV$j&sNCRa&IdNmLSP;!4n}d(pqC>D+Et36)us#@OI1L9P!-g-s)5=8bx=En3~7Mc zJ!C{Y%++1GF}3g61C7V8^sU<05h!d8qSK z^Qq2Pt=BpeS|4=YYJbswsq;hosm_enBVEvZK%F13#_^rySiu851ReWgyfwJM%a9k` zY=yv)E)F&kGGLLa2qs17r0O(4ze5Z3mg|7-8eP!cj$Y~@dZ{z$q^{}DXb&4qX+JUe zr1Q#POy`5aOWiMjd#Dk;8NK`ZK)s_6y7K{c@+{yc!}7NWnRPJ%=!5+Ac)-U>5Io$) z!8t@0>=Kl~DpwuME3y5lg$hPp`e3-y@Q?mR;~Bl(CewPyOn*`@nSH0;Gy6<^Z2pe= z%KRnugZZf5q}i~+57Rq_zm0DgfzdT1Fq{u?#PPkb2f`nj-IEfAJ}3;GRIr%<_@Ni` z@|Oiyh6*@jpbsjh&Y%zaY2I%7-E^t>H{;coUyZiVz8LPe`D}2;_9MEW_xg|QU+TYd zcx*6kf8X${?M>rf)KP2(v{W zH+bOn z$lx{ouJMG&HPdgd7tN-f&RT%|NlUOjhA(uQ5AYCY27l~72t@}GjUFH#=OR&+0}^mO zVl0Ir!bRq1aG=VhU##wgSGvhN*FxHBhiZoxHZ5+?td@9XOAl#s%|>F=x$3Voq93L>{!64B2fv6|l|jm(M2q8L#yY;65MVBg6zDycCGW+?j-* z>B885FU(Vzl?8{v5a4~a@~kO_D`ZyywpaUBCTyTLrL6Jn%ouR*&tBoSHFufoiM(#N`+18!o~O5azD;cP`oL(W ze+qA;Pln6~1fqY4Vj6f&O8Ox_i-lP~4G(hE2?%^scv1>(c>gTfk zO^>ETIPQ&$_ud|r7P28MmoXSzlGqFzXc`Fa|PALm{TEjs;GzyRxEmBM)Z5nL18_Y#6 zRXVAhDD*MjpA}-iErmf}AD}qx9^8=A9N1Y<=RZ(f<-e=6-2YN( zS-?m^N#L`LqQKWlg@Ny53j!yi<^w`-9cD9UI*wm}{-GStU#MdvkJ`A%lSRVh{$drD zYi-7YXY1^h4wZWv>?{bh-H;XGIglC`v@9_-x-&K>u{F9ly*{EQw<@fyur#!{q#$HV zS#HRg@|@5I#o3`xva>>8re=nY#b<_oV9W=EvXUS1=pQoCKj82A_o^u5ZX=$*(9S_d z(1YCWQDnN@X~2J?#YTQ#t()HFGC%9p1)=WCvl)S%X^BxS$r%Z?2?gopu@$+6(M?4; zQQf8Kk?Si`B92!kN8K+?j(nV(6!{`ODe_HXQsn#C`G9aH@-YGZLpJ({5W=&Y~yw(Khna$Qmo>ED_Y7g3jy8eg88lU9&a znwynSUz8HpSr#8VSiy)nR2dz2ubdJ4D4!AYEQ1mAI+?*3i<=LKAmm*VA&+y>Ka}D5 zi|4fpMzW;kRqr0w7Cy-n@etOchp?n=xM&R+TXdoEX=pQh!IwimlRi! zotc`MQIwOMR$UmAvJksvR#t{2?X3z*zF85HG*S?n_%t&#;Z;gl!dSw5Kor&j*5d=r z-Zv_6|3d@j?}Zd{p_>IA1iph}tr$7BT7zTH3KOvn-F9mI?QSNEn|vLbYlG=k6;UBY zrSUOYg=s0t`FYulobtl3tme{y%;go{89OSyGKMOBG9DKCrajH_OMRK@pYk?gK7Qam z@dE4d9=f2RD)bMSH%?>TIJFGl#ffKPoZKi#4z5>a-M-pTc+CoHrDfgD2JMUJHg&B5 z9;FT8fjKoXQ7M(lNik(vnV}^m`F=(9C7uP{_3f$3L*fQm(7tL8p0{ zhh^mwUzdW8VBhrC$ndzP__(n8^fbTPf*g?;F;JH78JRJ5f!i~IniTbPKIM^MJ}zSz0jA z=jm3}uam7S#^UGWWiomotc44+`5&|Ieq4uLn0>deLjSNC!Tl)f4~vqO2i4fS_8AH{ z@3K;?+~z>d-{fwQy3WUmF&N|>JP;W|?@wSj^=2l~mX)TPE^Wy+=vkFZ?LMBX-}NlR zu;W#VaqC#TY18|d`547oxL%BFfa`Do*I_5F!|eMoH>|3xV<>aY(r7J{`%%*owWn0nrn}z zXbnD3)aid0OIilCWaNpwWH@2+*R ziLQ2G&`0Imc^~D|ALuH_XS`Go{`OSc^T%U8&SNbez*^jbb1;atxS|jJ$NGQHVrV){ zNDb!Cva2kl@E!-r85P9+5OT~3k2N`?9vSfmJ)((wk2uM>jL_9=MnZH!5x=`j3;i z7vd7;(3=#J`hbljyyGP?W0Fh}W6EqnV>(kL$%bSUm90K8TvV=m3skAHaF^ zADF=t?hz9Gm_ow7aFL)XA&TFW3^RR7h1KnwHizSsA-DCEC7;=plc3?0uZZsVC<*l+ znbOKX>gD9WuU3%xepylK+c!m-34F238&H^!b@+YIi*v9T*PsRKuzELM`%&Bna*mMX ztN-*NgpFVhe})cXk_jEe0(20<=pdxfK`63VfjX-hP}vQ^gi{x6xi!F(R|UfO0icI35!7L0$Ec{@{$_?JEc(??5hjexdl+8=)N}T^q zI6l@u#&H}UJyh6D>;ZX**Zv%@8^^bpAjIe^x{u$4Xyf=Ah#I1dC?ImcLZpF{NB|EJ z0e&I?B4hzbQ8++}!U8%>1k4Zzgw8x4pr^(HCg>n6(1loPvV*w+Cm7QffUY|qXoLuY za-s+*6o`Rbtpvz+Aia_xvkuuI1u}<_GsqBfA9;p<{y`GtK1)u^Pf2`Jn3njY2;v`< zKzvLY#9t$?<^c>;SilS&gcV|~&JI>oPB6Dv0ERAnpc5nn8u6l_k}D2MRg$3CCIt#h zr9pl*vPA~u_aP^cOUNDM2{I=ATj7)R55;fNUzDb$#+5;83>#%$tAfjSili+ zMi=9RKFGlUos=yfSkTc)MWB;Pl>(h28PKYg1C4eCP+z8qK1d1Fwj%qLLG7gSAGIsW zGivu$eyY7t`KC6m{89a@@|ec2zdh6w^iU((p!AmxDBhn3u#{s42We(-#f<4O>tnDt z$DPUr&X&Aj?=A$i5OFX|kOm`kQu-B&Ky5}Rv{?1GPM_M0_ImYST01m;Y8}@6u60iH zo8~R8FPcxZK5Bi?daLzS>$%P^&BwZ+F-!%GJ9?mgQxDW`%mdiq_%0I6;Drvr2R(=% zW^O+%PVhG60e43sa6%_#8;w0m>54OEMXEoIYc;+bwrPDc=+T+fAJF}*w@L34b&vjp z?g@i&-64Z_x(~4*Y0U5$^{e5C-n7A81JJu+2-G3$Ai87(+85^m9B_OO%>O=^w*%3G zgkWzL4f^j*I}FE+mzlgV8Z>)l zu+`#){z1#<`sb{k>fNP1(tBk+Z15HPZ)Pme!I)hz1(P#oV06M946uvnKY+6s6VNgL z2VnjV#m`9eGSS$t6s5}rVdnfZfzD#zd;(-YdN5Spxuj{owl6SvVN+@P)T+tyv3aM> zh-t6=u<<&_`^Gz+?iw9;zHM~X<%ZD{*CCTnP8ZC7+n=%ko1<1>b&v)Ydud>{hX$r| z0XMvUUp@*1WA=^2+#f5#1PQXZKSzUoCf11OYm}|NkP*Jz8~u2`<{xMZ>3>%93M@3Us7eNLO*@jY(w*82$Umk0V6mz}oYu+@WkXPs)?PdT*EkJ~NrJz~=zaL{^F&_3G1klnP)p*v_#Lblm`_21|) zO<&^(ZUcDOR-Y3%tZ)YVxd0vgLm-YH$x0v|KT~-KWC~NhXUi~8WNWa$&NSi~O?42v z7w@BVJu-}XF(l6Xw11l2G4DLrgPx`Iy>7LByIk6WwmEf&ZgyB5vEF`1)EfI!(W~w6 zMGrVlgspIy3Rvp;%e%|%k4L8)xGZu9=edA4&OrzZ`NN=)&&kYWB9nuB%SHcCAVzsx zsL1@ZP?z(s9BZNLX&wp};sbS0L`Rt)3QM%#6O`$`-LJrBlXrRGIXxedo+0cai0tLauEn4*<1BYF%~$(CLYUc3My&njh!pp=A=$nIfknZ+ z{#6k@KFy59^d<4_UaJyYJa;8GdR|Crpg&8j^O=aM_L&T+^!@H%;rq+G+;^Hj7w~5% zv$^*TW}Vr*^)Mg(Ln)sBSj9yi*9wulwJJhg zD&4myJTJI2q%^WEs4k{4pgpnHuQ#RAcS~BC@2T_>zme3EfcLRQ0iVJP1HT642mbKO z5B%vf7YJY>-!b>jW}T62%$?{U?o{CU3$@sL(8NP-G$~Ln))_1~UST7-x5!<6OOC(M zn)Gm+6-hC!-SNpj9WhzK%~6Gsb>Wqa4_f zl)I>|FZ40!&k3e=XGFQQrzChcC8h_}#N|blG0I{Kqw15gBRbR4!&he}hwsl$2)~(~ z6!|(SG2#OwG2(N0V#K$g#E7YYxj-=b2h6>X)A9Pz1zjt{=f1VL{{cP7)kS#jL8k;c z-l4^@ui0E=bFHJ&>T)l=WkrFOi*m!AnlfX(YSNMeN|UoA@)L_=GvjKKQexZF;~D)~ z494!9NXFHi=$Kb2(J}91qZuC~q8VRLYXM!*!z}a<*aSlDm z1@s`Ndql|LE_Jq@9VSBSo9*QLYTb2~RQQ><7Kb|2=0|&!sPi^ys0^~LD2s3_EQ<5Z%ufwX z%FSWKWR)g{Wi+M*q<3feq;1OfN;{k5o&F-tCv7a=H)SHqFZoNTU-D$gT*f1@7E-Vd zuof=jI-IIS|A3k2Fg|lUyqrP~4)Bm&D;1bFuAnYh*=;G=wa8JerPb4*y3wCjTpQ+` zUB&QDDNhWJDank&u?OS??KEQd-1vJo<7_Qu$Gf-Su0J}tk&jS-ft?}vCK}fzROLw zti#7VuPw+vttrAIu0GB`yf!V&zbY@rv!W`=8Fy{jl?`TCmmbZqDH~0(D|r{^P&g6k znExfzDSt9}F0b&r=|0xNCCvY`+4lgtpxu}`w&U;oTQPTS+00DVZ4o8?o77ml))@-4 ztft9S_d976F84Id?D3~1EDm*!T*ROUE=&sYYR!&xYATDhX>3ihXc$N_tv`f}BwEyd zU|3as3a6ES4Yn@-7BrVya@;JyeK@8$8{E=x0lY`Tjh%$GDVj28FB>2)5A z)9-v3W7z&F!nkEJ#I$iLz_j7J|6E4!`}qp4!?7xyd#uGRm_65Zqx(Py(TDka8D_68 z+@I8OSQ6jMputwT--y3p4^1k4m!nGDb`Ra~Eq*3`n?h~eH^eyGuS@l^SX1b4G}sVA zU9}=id*EQW*2;&WI(^>)sY`$O=`H!?t-pAhK9{?=4rfbo4X_rs;T){P>^Xq>d--Y{ zdo!K~iP@*+I5Vj`%TKD#C{T(|>2PG7G!;lXZYvdi)I~Yyu(vk-V33jX{wPc9eaZGF zd-7fNcGr7q?OfrlzT=R$`nFNJ`j%-=&Gq1}wHjQtR)Nc0hVs#aVJ+^!Iar5tu(AuU z9p_*%&Os}#L;XSAi*TBW6yy0M`B#NW)>UPulp!id%oTI~u*>%1eiz;4-7om5*`E*B zwLF(-WOO#qLibF)wZ`dwJJplN?NpDyvQ;?(*ysi}YP-N%Z3ociavJAg57xm3oP(7( z2TQRI7OlYyycPY&KJ*{Qun*xpg=Ai1CCT@=NZhb6VGJu#!tPU91MXXJd);#obiU^$ zVRJWF&g^ciivHakP3=4Ny6U$F^i*!1(^tOsK~L!l9@=>k^p#G7fzqk}ascaK6W$90 zcrPr)d!b_$zK;X*XB9et;v?9HaOR)A2ywTVNF;iYkk`D}iy%pSU#c*9zR+cLeqqLG z`@){b@&#Sc_(ixF^<|2b=F4(9)fY<@l%5|~RCxMPQT`Dq$~^=nxx1h&fBU~|#qWbv z_1Bkv!Na+3998MwhkDSDPQjoZOktPnGm6>cl z>#$gSHfA&aY{RMd*>i!`moPrn$uvR5$r=&a$yK6KU#^Nte*P^c`5wfiUxKLgbNrmc zAg;ku{7&pxf#-m%!yXX45AqKYk~ZrAaQp}yKkzR?=#L3;eNKqO7Yg=4aA6;WAof8> zU>}4$?!{2U9tbM4CYZCTfHS*11aeA461NysE)ao!UO_m=Cjg)L1b+jc;Pii4hIQD1 z_d&y293N{i7spS*`Da|geIU2d0X)EcAWzUiyhH~v>jA#~pM&7QJ_sJ{gAgQ|AdW6T zmZAtM6j{(^k^l>45pYNU5P|+7hm8|j*x6wn2P<4g{`wD#um&3OJ}k!?%-fISpTxPx z>kq>5y+-g14!m}oH@N@gJvxX9bPzvh`!5KN`+p9C6&(Z@kwq6G2|`2^=SC0|2oGqX zf3TonfI{yOf!-klsX>}p=F+qh*MB3<{~oM8yayN;2noJ{b3cqdAb9=OuL&{zfMb6q zL~9Di{X>Wnjz8-@WDzMu91%eT(S`5=2jRr|VZon=fE1#N7$UY50^W%Ke*jH%AXIb^ z`l^T~8&D0{L4(E#itYlK4QZKmbH*QLnTMg2-}Y4YCb6fSklXzl_{Lp74Xn z7(a+k@`30K-)}MC{UHv#lM=xDQ4)ARNCEFV>Hh-Sv*V+KFj7X;S;2tH4muW`pz5*! z6oYs{Hl81(as@!5QV=9sg+ROqS%qvu_8`ZQ^TM%?QjHk;AaPLa5eJ1; z5}>dN*)8!$;i$xKg$okD6mCg;S9l`vRq?&}XT>S;iT~%JUZR6~ssiGVR6+cq+J6C) z+3}^A!4|Q{ehho;&#}|x0BcKbFn2>A6f6kTcoERbM&(y73F?hfztuXWr`398eyXmK z`L4QEc1m@>>{r!OvXiP;Wk0DtlpR-nBRi({RrWRBu+Px}J<>yg3=Hb z6ffz4!d$=_$9KZ~?=D0E`m6^)FXM}K;jPC3?lc~7@(}!G8zT12DnaU#dA8iRNvYyH zqdJv01`E|*>Gx>7&>PTvM%|?Sl)6`ERQHtbBi$Ri4|JbW?@>SM-qQcAa~*e&T{Z;G zb4H+X+8ESN8l%0M1K6W)aK*guh56ebKZEgS3cf(Q2dHhFeW;8XPpeZg9cus=;4omkh^D&l~?T zK4S`o=wI}Ym;?2I1?cRx0PVSelOP2=cqo5o-(bxB5jY=FxIR&;EZ@WRIX?zj^S<@- z6nRb$m3izIr#$SGp?Sxykb1+W(&(yHqv>VK4)crV%dF1f`8{XMw%eRCJ!*U0^s3zv z)2DU^Eheq^S%KwlYcSnr4aS@CbfFD4pg$LIK?g(UBoKi9Asjy$yaeJzC_m%nm_Eg6 zvAu~l<$fCOEc77AU;4Ihq{>yVB%MocS%znwi_A_rR9PLfYqB|Pv&jB{^>W93v~|wA zt#-TausY+m&Fa3}X6tchbT9U6@noG<4q(ym0A?#3!DKGrhVI1|#}A>9@7R0)HJ+3F zO5!J9Qp725Q|!=UThX^woBUeOB6C^XapHMPK3c%XOIxICQyyZKo^H7UALJa{&*$c7H;q z!tt4NEbhTT5ArIFlf2IqAkVX;$iqx6=3A+jJeLz(B~CN^m5+o+==Yu+&M+4j4r|4}S;M(j7j*XsRSC1!O z&jq~L2m}%`7KM2$5%YgKg*?n=B_p|9qC>QRtIG|^aT{SF7qw-?DB5#S>)9b(B|0}+~lz(wBG$hSgprjVYT$n{?*`LvV3WXLx?#s;KOM zy^M^2Ax38KyRgi^y~m7KdN8;l&T|1jLf%K9f55zTCljB0XS7esnpm%tqj6p-BMk4<$q=8EAAY{c({#V2KOS>= zhu;gcwQxNfb7wJTUz~#jI0t)MFoWao{5yI$$)+A@vU-UY`?3yG!H!ltnTAGpwTfC_ zy~3&xv#jzc+vJi2mzbh-dRTrzKtNtq7(J&w$}M|Uj8oR37^m#NqFu8mL)>Dvc+BNBelHATEnLQRIElIMAkM*VoP+H+2V1%@gRNj8Yx+biid*U0IZc7aDfQu2u{E)d;Z-Rf0TsEvUS;LME+wtu_Qm~C)K_Mh_+~(z?MX3dSjNSOGAmDZCz8aWo>VmS2A81w!0fdgvsc$h|Cvm5aeazXR;%as%&&lUR#;a1Lhk_iD_3vpoP^eb@uB z7Be{JuWEDvrAN6);ZX^ab3~mf?XV$d{6QLj#C|97zf%*L$yPX3{>||8>s9B1J#|#R!mcKIF5C&1Mh`3%{YDsj*n~5 zhHFrdYcQL^3l5+IIDvaW&NC6lH8v7@djV!qN#cJ?op|3gVsXD|!{KlqkRSN1?mno?Z9adJk{8~xr0;njS16767_+r`Ppf;C1mADTE>u?3u;SyYf zwtn1)fF7u92WDU#KkYan@#nA)VF+{ZEoS09%ud{&@er3M;>2-Onb?j}nXE>wSj~{xopBU=*KnaT8!gk z4b}Al4rRh~>BhG5erIj6Ud4^gfs} zX@78HQU4IYt~8#=DK}BUEj`i4BQY_=D>eZ&5Z021{`K zY#cuc=Rf)aX7Fpc59AK^Aw0lyI7e|02)Y2XiP^mv9Jm*Qk5GS$5bYTmqCTxcl&7f_ z`Dq%H)C`?j>~{={(C-p9{y)9!yl|0y0Zeo7OanXbxBt?OYp@U2oUl<;Sf$DRi%%=%)WOoTum z9e_QB6M~pnA%h7IYD4d^1-Xq(p?~;a+Iw*QSK<6`!RyC+FcGgm;v%}Io9JKg+U=ea zVu@oL&yGEg?&A|7s^9-{5c238WDrS24ELl6VUG$g_F!;=82SfAbPjrmBNB>apm)Fr zBY>MC=g_bc>ksdN9J~jUj$-X${RQCsyWhjPe~j+uC0_eGbT1##e@qghh~vx7{Np~v zaBN{j5aC6*5q5-vE`;FEL;m(4viPZuSRtMW{?axFP*G7pz&Y_-kjeTIwvIPGtoJb9Rt&;Q+B9E)b6A2Em*KAW(@k zBVEW!WCOC38w3s`XOOF0AaEaf&Iy9!oHK&oIerKN#}tnFO$a!?2m$AWFmS#X0nT@# z{|ji~_&VqysI#Dp(bi%GHDh*Abl?D4KQ54DEC8_#9uO_z0g-xM5a~dcBZK(wTabOo zapd9x5V^fzMs#$+Pth^%Dba7-UqpfHlNfM)6a(%L;=uh@0v5cIgat3>06OwapfAG& zMzhE>|Lvuy`fQ+O!vU&ZT%ZuX0A!PSK{}rgq^gh>bVFT8KeC?xx8x4~8L5MOzogFa z{gArG_f6^n-xsOZydR~%@V=M+!!w5d>8%X#y+WSJ0^g_{@I9Ub=t(nyi6j%4OCXqa z&Cv;&YO{io1qV=Fxj-|B2UO$uKsk#a6iWquE7lACQfL?Yp|DhFN@11ISA|VNlM1_q zJ}Vp(`lN7CXhPwx&^v|aLa!A+3BFXE5qhQwLZj$^9wEcZAbd|5gznA(jBspAQ3_a# zP{0;@G3?OG*kWDKjIdA1o(qh8cz_zoKdqf2IHj2{JgHGB@=3i(bV99D?1Ngb*gLgB z@iEn{;%`*=pLLg_q*eqxM7VrV{O`7asH;cFPJh9=KpLQC-lJ}Q*=zPSo}4$M)HMjtMoIS zB{ENR`eYw#ua|wKy;JU?)=~Lkt;_QFv>qzl)_$jOUFWC56J;u7wkh2)=vKL@zf$$O{s#4{dV4gk=$+KKq<2H}y#8~|(*~0oCk#RTs1c|h zHU^c0CZN361Qhqo0c^4N!x^9byJPnC<;MKWPw<=+%1n?V)1<#H>w9lY&X*qUypLUj zg@+wuCGXg#$=$HdR~oV`SG#0Ur+LAwP5Z2Ax9(|^mDCd^oAi$v?>9JNeBSVo@vz}O zlMe>F&3;pNpnutB30j-2Kx2aysI8v^INF#nVG}!J`VY1nw!F+># zyX9KDW!9^0*V?YM*<;sdbKZWr%_IAz_LH<8M=Q?}0XGWy>5C2q|0;QmS>t{p_I#%>le=jgrSs4`-PruPA%?BT^roix!1ZK^sIKf=2_)2=33?T z$G(ydv~q8-DD?)@QaYIYo7r`kz;$>Yg?TFh{X;6|KKyI;S{5tjKW=g{M~0lp(q%o6 zX2rK7$yH)QoUh`l=unLn5ixq*p-Cp4!I_qAfdw{A{uK^&zKt$b-itlT=&QVn>ASrP z=$Cx*y0zXRiMW z?<~J)m2O#~ zEncZ1OMMc9H~Gf}pA3i%dE^%#_T3{s9Gv3Az!sq)R{v%aYvEYG2W~wKFvBeHqSAg zQRbE$)#w!$+2tD@u`VDy;%H!4#Be}F)Hlz_XmE~<0{h5FutsSAW*lqbSv2NeT!)J| z2dB_M94YxH_w2{qvAd3kY^jqYYpQkGSCm`uFDZ7EY%TCotjh_|tjG-4FG`O!%}Gh7 zr6*-OBqo%&#>CfoMZ|Xc2FDBr1jHN+^ozM07#R1(D<}?Jf@8rUI0kGG>wohOYhm_& zxQ^>^7Jb+;oPz`9|75?N_1FW@#6{LONs)m@P1c@TQ{MJUJF$i`clpX9KlS4LP-<>& zv~hY?qGe)6h8-iVz$GlT+A}bDp^s0}N`J4!eSsc{Hv_zqKY4j4gNsiRIQS%jjqe=Z zU@bg|$7lbz4kxh=4&oZ@uEP9Xhvy(-_S&$Jm8@zPCB1EG%!`{1xmy~ng==bDWJ)W& zRr1P$v@=Q~3=<3E%o+KqHetEBP662!?)0n{Z@0`9e$E-Y{2eo{`8#J$c)4bRi(5K4 zxTk^5TwY);+)cth7+i;=I0t(xaQs^I5175yw&62ibO60wf~0GSB4yzsDtrAx3;yyJ z2Z@444~5J+KlP;QP-;vi!z8Rc$ts{U+n!!r>f&0|=;=_f%-g15yDu$&$j7GOgNI!K zINRsp6OkOSL9GAH6UIO5a0+Yj0M5a7%-i`ni_kyxVg~G8K-znyN#in2mZ~0; z1w~72gtHg9N~bLJR*r2A(u!z`)DLWkH>KBQ&|GVZ9PF#>U9GCRJT_B5+_ z?`ly24wj{0ODhH%V)buk@5xJ<|IEQIoP$lc27|Z;eeL)Rx(m-@#QfJf$V%!5MM(8( zWlG6_K6`GzC0|;vqiFmx5822bKjok$VOrjuG5T&DsiyYr`82Dx8avb0B~FGdo1OKW zFF6}Dy>l>X1RLXe>{zY=i@D5xFP*_!+>djx1@D73xCVWgy?Pd-|3C-Oyb5~|F#nWo z;UPs^q)F~(P3H8C#vBRjZTO7hddv8(3R3b|8KvRapG2jt$Tc!uUS)2uY_Sz} z>1J!4o=Y^HuJ@L@OTe7k38s1r!FVpS-w8(xG5=R$4+h=`v-!8D6Q6-E#qn3-`0M`3 zfI0h^N%}rPlCn>M#P8K*j^1s?5xUEs*MEn*u;+GvN$0KMayDDyRm?YMYZ`8>q*6C@ z8fdNGY^b?z$WUYOqk+aM(AOBim#D4)s%G#1GCK!*%dq~j4p-tD^xztFVE$|BNB^=G zdmwNQW;0mIVa(vC*h%CmVG?#ykp!QhGWi|1VD&oY$mMdB&S!TdSlIG#jD+!_3|YN{ z<%-$|I#ty7Z&g*>cT-h$_g7Vw9iXPX4cUx|X4C(27(LJytix5f21{`b7GVuEVGUH_ z_(eE=HjbZs2%m#v4h=oeOnisfiRV>8;yxrtT!yrX;}vrz+bfQ&R+oJ^O)f|9=wC_^ z(7se6qJD9axXOhc63XWviYuL&kyJPdQgX*ZTJ9*w$Q}7#c3>S2;(f3T*I*IWK+_5w zA2ayu_*r`}gW~wnC(!|%M+Y#34glX3V}FmGSdJ_p<`2b)=>uhAJZwM=hV3ZSVIO9# zVFs((a2|*9a2uDx@Gfro`!BgKM<9#?g ze&JT!2eB6&z!A*Ar?CeEbGX+v^dGnJ9E$sxgGVUD=s6qFf6q&F$3%(NTX}*991xW^ z7DVxlCq?#6G!q`i#v=Z>i~i*b?gP1jJs6n7X%DdvVHEd&JV*cV2KQlnXCbQ73y9KBK_d4%PXZ(M70?V-Liy|LcKZMgOn>{eu8H013hcDg+j{y9K)^5E2@H?PrGtNJ*|LpaL;@W#($GOLOr{Vw2`&-ECJB-FO|b8S;Mj!lBMb0+4mNZ|1badVdXRtmFD&rWA4x}Mf0hl%j{l(+ z@4;gH%O+$ua)bp~&LUTtf#p8(9QnWm%-@;*Fau>8`N;y5?`*&{$pK8C zIf3~j*Z%?vDomiPgeWtCB03ovU1kt5!=J;M6?g;LfIE&IIJ1#*qzPHf2AnI9wa7MB z;M|X#KrXQW=WS$^_lOKEcz_He_mO*Z04k1cBuoJlbO5IKX@+xRiZyCvh&@ZT z=!EDTGiu>n-&K;hCzWy+d{Qdo8CR_5d8gRU^H#Bk=Z)e3?<>WPye|}Y^FCKR#`{$9 zGS8^uFwaA!x4ie2rugnC0sl=rMCUqkRT%`XAeWVa|KI#QzPSJaOLP#{_-Ti8Vyi#_ z8qS4<1?x{^SN1RZfn4LdF$>;mr}MnhD&%{vS;hZEqer~ZO8W3g3kHEGMobO zqB|&cNoSk%1)ak(=l(yo&H}uuV|)ASNIW^Q6L)uacXxMp_k?(eKm-XA+}+(NuEh(q zw@|D|DWzUo>hpgOa+}`od;0vINz!oMnKiRzy?f1$%|+eQHg|Q8+rHF2V)v&m-XNp1 z-=1mjbzoY19GK?r|ABd-_QM~04}xJOM`90148D&Q((4#K`X@D9s#cN&_DwjMOG%mQeYn^rN(>>)ftbf9J+~An=Vxyx@YmE;%?KC;yc-(ZK<8`y$ zPESp@JO5&`#f2Gda%Bb^QDnH@o$0M}XS$PNz8HTXEaY&apHa^CW1u4eP^o>?uTlSyPrK26@2MtxyoSwodd{)f?yX;a*Snp!UF&|&Zk5OP)+@Z2#WHv=OMIBgVqa#o(3crbhWW#P2*DnZ7}Ou& zwEifKLl4u%={ag+?x(8J?KBIzlHw_LE-_s0L|l^aa8$PDzKCMIU7^)R+k%@+HwSiD zYzUZYz1Dxkc9riuhvhyi9GCcPb6(_q%w@vomdiZfmris1|F#_yT zDKl9URb#OtqS<yBeM3tkH$Wj)VKOK3z2^pbLd(z{D(qWI$xwq$BXQE2l9Pnc4kK? zZ^}s0T$7rmzdWhXWN|`;C+ds)3-b9oL^_y zL*LHG*Y2Is%(*j$*|*0q+bPk^dNM2wK2SVjkcoV&0Co2z$T!N7|H6y8P%cR)Dm3U& zxg~d3iHFpt!VtyPxv?5cv(oeyWaOI6NiDIQkz8dvoY?3%n9$)mHLl;YJ9ef|d-P)e z*61w(O;INT8=~(9Hpag4Zj5JcjS0-DA%WS~CosFo{3eK8Hnr-~summmq6SadIkmyU(W)4&!SZB-p3*F{ z_M!so=E8D^`uuv=s@y4FrP=*{g;}$Lax+(kW@hXQOV7L(mXY;MU{*Hs&dy=(+1bnm zUpr0as|5HD*~ojb4n#2!jema&-amjGd`AcJ|4wOI)2T^IJ1lwg+gzn*GzTaR)kkXd z)+Fk;SEZXYSL9jMm6h68mejbE6t#Hf7xwyP<&OoW+(%09S)2ySrigoyge+k_(E7z z$@8F?Qsxs|$~@vqm}^`ybDqr8G#n2&9?w_3jlo{Te;dZXu?P9@RMf!ZHgw^T63rPj zq_KWGvB6$X+3xOOVQWXUR()HNL1jy(SxHlYODnCU=P;7X2f4Kev$ry5#(QU z;6Kcjq`^6=)H}z7(>~ilylIBFZ1rfca_MliM*g%Uz0AQZljQzFtJuCO`|zG-m%#2m z51+1aACJ!Uer_En!DAnf_CMV{+nI}38*}n%We$`181rxzYf%&f(HdCS2mQf!`m>P# z%*Qj}C8z;dqCjnnb*X8Q6}M)At3=s+f4PFW;lix(c#V`-&2n=dTI=CBbi%`7@Ufd?|6k5d{mjw1kJ-EQGP}uK$2sA6{o8f80c&8z0OCK4 z@y~?*Jm@0_>{x{wfVGlTyGE5N*BDdjYI|P(N>9nG6~S^T%c7OymL#i3EY8*mT3BM} zJ5gumKEKP_Y2FMwySZx|Z04MFuo-`1XEW<>Tbmio+IEat*^V&F$y^Y{0LS8%F4TbZ z!+)BN`JW+*|6Bde70_P?|6x0FupP3Lw_SsBwwY1JRwr)q79a7r&0*4!o8shyH>3;w z))#7ctgF#=UejS{w|dOPdes^;%ay0iES7&^YQB`2nlHu^-9=!*zd4R|uoK6@TFk+c zY4E?s-s%rv{GBVIzaBLZTaknAMh=Mgv?d*qqj>BOialgTQ3stk;Rk$qK?fql{q`qG zd+y7XciCH^Y`;KI@tV7Wph}Pgd z90Oxmga6e>4p_Af<3}Evc?i$pkKsA^89oJFkRku`s^ojlh`i6*k>^<-ayt{rbvlzO zW_P++(&}`RjM=H_a>ggu${U`%BrrJkN*>;aqRwHatbLFPwGaFo(Hw}@zP92GHKz5OlC5oHLw6{U?z@%p@mrg zBK=L!-v$5aAohV^?D5Eh!!M!^;VL|UkKh4(3=iNAhs^JbkQPlq+_R$!7;BCYn+Kw2r^)u9eyujGM zCer>Edr!WH_K)x%etuhfA&HOu$A1uqt^jBOdk_Pv0S3$vXm zddP1L^f*i(i29;t&SgqYh(`dA$;9)RR1TksSAtd^6Ym3~U>;b?W#VhWR+t-*Pi~f67hpewSOp`%P{G@3q_x?$2@uxv%6- zbAOb*%K1+AGtNudFF9Yz{>=G8j&Yv~8258|#(M})20NGd5B>*c3g5#VH4qjm#LVDD zm?9n~SQiFvT&9irP*37B)ja;+!YZ-fg)L&Qg}q|0gwy#y3di~13m5Uf6|UlcE!@O= zDcr+*A^d>%TzHZ9O!x`+bKx`Y1L04+dn%0osVWouM3sr(2Den1#7F-FvxK&d5;0qN z0JdoDG|`&C8*+d@<;!D+(fr@^GQ@t;DG~clyH4zD?RN1O+Wq3swMWIDY0VXTqP0Zq zbFDRE54E<5J$@KCKckJn>0y0Fz%B0%y>}Z zj`0l1PmJbEer&W%>Xy-ZsgI0yNnbbmK>DiDhccIq@5)>-ekpgx_;U03A)ZpgpAQH$Y=8Vo;m`or6Zess6veC6WHec}+!dtjd~cE`3z{9~IM zshideRw=?ICS1|XM zN22&O_Z+DYT}x%pyVS~`acWgO>DaA&%yCfVsKX4^!}jyl57;l)*k`v{bC2BttzC8( zw6@!QroF}fJFSiIUDi1=jWv!;eYF!)TlGIMchUHXehbcrfkxo=kHx%nO$jf9Qv!{vZ~6 zFcOjfCh+Ml_UL>PtxVTqOz1+4EB9nnu*8vwc-eiS8H&4t3strSR%mPrsMlWa*QUG1 zx5r?m&orZD-m^@Wcr7+v=()jszUKjpxt^CS$Gsj~%<_J1Is^X8m>)A3^<#P?{!Di= zqB;062zf8`KTAfwla3mQ3=Z8$!{41u643b+JvyFZ&pnvrC$T#|Qg&->veJgAY_&BJ z#ab)EDs`8HHW(}no?<*dsLymx;IPH4fVoy<{wr;U{dU?;^E+cV==Ygjf51;xQ-hd! zUobQ23&9(hLYTp17-H}ZV(=&ldr&fvcjln(9hch+Iq)B{r0GPKCSj*IXIF-Y_~x`w z*|o`WN-Gl6)fUI)X-&kG>dlF+HkujPWI7trX)!%)zbEpon7bHrMg zhKPf%brCmQYooq)s*Ps0wXt{$6#gh)O)N8=%=aOfhj{o8neZP(`X$IW%8>tyz}I_9 zlxSZIa?c%cQoPIXOlQBX;P*6O(vXK4US^d>wILUR);EfR>rEfmZfMllw|8y z7Z;h77gkyn6*SuB<#jn`=1zA_%~{})n7!FEKKry+T=wT43Aw*JC+0G{q&#Mml*=q9 z^AzWU+n9&*nERu+<=YGWZP4BTA7XVIa-S(2nwX+Qv!@u)NUJTczu7~ot073ftu9K~ zP?MxtU6rX^Tr}tsS2)i-x^RPMM8OHq@WKZkk%hlGM-?&q z=ptqlQ^+hQ^C$(!1Lom0*1^GA_z%$E2<_GFc>dps+@lB2f4c=V(ydK{U6$OQ4p)gO zQ~YHcTf>#Ao8r~V8q>53>vIiqYD-MhYig{Ls#@)0EBl=yD#l$y%GbIFm3`n5Sa#1n zxcpbAkaA`hTF$IP%b4Y4?%|wp8S`)q>tIgsQOH{A-xSWd0+X2ONiou>N+mp#C4@Uk?3+eb9#wF*1VZpQBvr z995#WQ9WuLvF6rHcM~rk@|P(bh)~Fx8n2Sxm#&%ElcyKcRc;*C*n(QWP1K4$MZ zWrd?#+aX8SwmS}Pt-sj1w=gS@7G~km%*-eA5zYxGMaLo5;HD|a|DnHVD)N8mj}9aM zoeBS89`c`gGE_TPovP-TQt7w@uW+`PME1;JnY6JOg~ZWhm6+jd&9Lbux`9J=Mm~cb zX6^$cmd^dltsSQxv~lRWW9`uUi=|@^Gk5G}rcPbVWHOhq7LOnX+c5`gyO94wf8G%C zUyOfn7V^Kj&|if7XBn4DmMKudQXR@&Vo8~cUAU=>{KOL$hD%3J#0$daXDA2GEmZTF zQ?2bjK1I)I_H-lrS<6gpW*#)Lo^jXMdi1rC^)NHEo(`rlgUOr`#Q^JY1J=ND#D6}< zKNI@Xpx=WWtYtBtK_d?U$_=YhFQYcfYf=K{>ZO02&H z(4P(c>9bJ-fE=`K37$c(LJh)t>;ph9nzRcw0DC1VY_Bo}@7ANh-InCP%Z+?@1#&!h z#`0ZvWQaR%FOjm{)+lGWbx_`H%MwMC&4(3@H$7H>@1bO{mI?J&gH=prGP^``AX)=h z|KphdVT`{|q`wR`7;CT(VIyiFwj&4LgBnR0tKA}l?Y(x&noXGB2 z0NEUiBg+qRxMm;J@Qsi5iWwYPB%ycYh=lIpFU8?|NNMh6G8%iBtokk{H<^uC155ki z0Zqp-0R5>8@C<%A^bz|~j6DZqPuUOsBhWvAI)F3C!7spnxXdTB>)3m7O^FPz>X7~w zE7HB!DrzCT!j7==-(hRzlD7epKwU)t{AC2kS3-3%A{~l zkL2#zlgzy^lD?NulAm>v#Aho>?Cx#i-e<(U2R>uO`;>8b|M`dc(4R2_#~|YWMjx>+ z*@^i_+>?&O|ABVkhlu}G?8CT;9Q+gbA9tXC4<5in_z#~W2Y-Sb{09k={8~V~FV%5V zYL15@{@L3-jNI@t0xla}fI^=to?7>;Jf7%(f3P_NN#-G_}9NzL&4z ze|!u7<9i}0_%QsRp#3Yr$0F&5h6wcKFJ=4;F%dmDWJRC{ECl-i7TG(xvHnFKWW^5D zAR+!KCvp5?-UF{=572)w{s(XUAHy%9|IJ(PN8x9T9b*=U7Wa4P{Ru?cqCWgTNa1H1 zz#c?_Jb<{crvJi($SsBNAQaKcnqw=C9Xte25DwBn31|Ra`1omH94rQ_!Dg@<906y* zRd5G9h5PXxy7dS6|9c?c;OFmQY9h}89)t>71@uG85_&Em7$kuLPy?obe*DWZFdr-f z>%ca!4_e2dc#(dmTbP!I^qO9R*Z=iEUg5Vy9*ju$CHQI*OcS}jCVU4C9hA?YFLI{n zhXdl_2hCWJ4az|i=mw%#%mxdgyArw^F#S8Bb_jmUY4|Nyp>-E7%yUHIr#HUOdtH%3 z^c;NgKQJA5AG+$Wt$+re_p2BZQ?y23Fdx#G4~bL`6DvZWY5`)wpUU}@KSICp=Fn^2 zVtU0}O+WHB)A!sx^ey)T`kHfrUeIkgQcs}wJw4<8O^;#NJrsvi1uy14xF^XtpG|@p zs1Y+15;H)}fiCiX9mGS+oS3RJhbaePe&RVyki+>yuAK8)u95Rfwv+RN>>#|B8T5_p zJbEd+lwQcLr7vZ-(HF7@=&8&pdMtCD9!Wo>d$Qls9l1a0V*%sbl4snH6d3oq0^?o# zAD9WW&5-Y#!UHgY4`PCN7$X)2w#2l(Ie(}}a(-4#<9w%5$ayKO=6oq^>t^x+~mGp9+uBC&Ei~TX>glsC>n_qWT-}LsiBwKieT7`--ertb{{>4jcA=c#Ts=aEh+=blbI=Z?-4 z&L=v3oZC9X+?zVHxi@qcakN9VFpNO5*eI@>Z9+Nx_ z4uS)EsAdBDCc$i=Z-@Nf4mA)?sKs!Rg9oERzu2P&!^Vo9S$NU|vv9g&mcqGhme0Lm zTEV+&+Q`3T+Aem{v`_rJ>9E9Eli3odOcqL>G+8Be++?fNQIkW`hfOX@A2j()X0Pct zvO7)xmfLQMoo!}JZnGJa-2^sHg4tsaj3Z7LIIX_+B>KrqoPP0?qpw|6>9L~$-L4%_s}9<-S*x6gXEV2|}8`CZm)6?Rzd zRNQ8DLUFV8O{ERi&z08N{HC-DFL_#F!xWd{4}LGTWeQ6s@y5b+#VH;>#1{d``+`sd z5Fk#Ey=3W*hZ^1RG^b169(2YljCb5KN$jvkw&Z@dVwpXz)p9#s8s)b+w<~UO?o-<6 zG%Q^2I7elz<5JaC4ja{1I2=%4=J27$V#h}s3!Gl5X$3G(YI7# zolmv$YOf}h6`mbxOFgD)EcO`HT;M)md!E}WojGnhb!WSs)|=sWM{m^qJKbRqrZo-z z%a9jSAM|2slVKistl$q1Mx-B)d?yL{2DlWD{TETPbRt@t4#im0?r1OG*2pl44dDqg ztHUznmxtslEeR@9SrAyOHZP!AbKJj6d#2x@?wIdP{b8TQhC|+)j0U`q8uxjBWYXjN z(y-f)>38`volbwI)fvDvC-WL}@D(m=_u}Bcq`?2k!1GRUDh>DiNfLAjwK=V$h^H ze71Q**mBF-&|Oy5p%<(x!=6}HM*MDCi9d)3^edy79?*TyH&_Rs$HITWxGzBWL;xs@d=&=&ApvzT zh{2hBjK3KArO5lhR(!p_NQRadY0$zVbDC4&!XL}?mzQzuw`Mw0_)uPO}5$bC+srg@7ZQ0{9=)n$V{`7m=Q1l`tNy; zdANgha7h#c-0B=Cga1*1+_w_?Xjhg?(4uk`np+T1MRKV(62@A36|90Z%RfL-C7bU zXe>@tt|`n=t0>6RD#aSkPw?n?K7sDsQE2c-}#~(7aoAVR=7T zh37G|h&*N-nUA-&P2xUod#~U)IF14k~7b!NttrJ)eq>2hp)0ib3654{8Op7a|88??CR?joho7 zPaR!K)Y_#_jh)t7?u-ZTRA`*)njIp6@+;pS!> zhtPlHflb947()I(g8X|X{D(R4A109hO~_FCd^Jj$XGBT!Y$<-O2Pb+?5I=l8S|WIM zinRZ%T!Gh&az(eXW)-K=0d>2P2~C^f9hz3d*EOxCeXn6T$TTbmK>r)m(Z1ohC%`{XSb%vUs@*r8-LaYNB`?vF|)<4oCjHd8j5`ET}P z9g4g_kqaTh&t-FHB=qzKhI!l?H z_9V997+i_7yLsB_!Pr`G7q;xKY6wg(V;OtbAJG+{s&s-;&Gk=rJDMr#K7)Saz zYB7%e!xF6jap(_2zjr=r05I>>7<=Is=wsaR2QdDl&_4nFGtj?)9QYFUL0m-+d;>Y~ zO%BO_EKX7%%aH`?Qp9dq5bu@`ac<+}cdHr<5e_F&v2l74{tgNK9*uoP|x zg}5O##EqFNHipLI=A;TO4zQ;?@iY9==g|Hi#zt`ri1ab{H5;Liv8NwG4a7thvz;LwiQ7Vt&e>{Z;@Qg_8IrK$*DQY?JTIBG6N)tYa4SbLY+_07c zoNRu9IOZ1!G3TY zTm+(8kcZ%F_#m%g(J*)eZ|g8b{=>I#{?A(vLgYh;fP0EuAJ5q3^wDMXg)D#*2m}cr z4^)9x&i=L9q^(=){4DZYD*9zMdq7FFup1D}CA;L}Mk zZTKHrsDaRc_aH>xFN%qx3FgLun3O*;@mTtkmkH0Mgnr`I(YL%dddcmj=iKS^j5CX# za3<(6cR78|T~80WJLm!T5Z&XRrMsM4bcg$dKH>gIw@?#v123<+h9asfQj9K3G0vq) zFnwt2Bj4AB2cRtgs>C$0HdL(WZzXqnEgwqX%O%5a$)o4874%fLfj*aQrw1}q>7L9m z-IW=qPh}R;Co(JPw#+8FCA*hCl08Y+Wv|m!xzFiC!B2D^o7m1MGR{dw#>HEGxyQk= zNibvh9>(xL4Dl)Q>}RLV^D)(u1G(aXwjDjX7otMh3;qv(=D9@x~`i|m-R~MqFxQ1(`(_J z(d*)z(jVZS&>!U<)1SjVs=tJHSbrVwp#E;&e*KgDz4{;Vcj`YA+iLJye3Jo_*Z|gn zwP4L8m?f8(B~CN8IK4XHbnC#Sm$uUM+(L=&n`qN*6KlF==1mvPBk7Dq3g?7H9`^%_ zGTveHTK)m^7XCi-F0no41LC{PMkRKb&6C(_woG!f*(RxtW(TF#n|&y~#_WOgO7kCO zmYFk|#bBWYlUV>JESU6rM6WdZ!w&l&T(Jkk8)k_W4_pShjNK6_O+6m>|3PP+jU8=wHuIKZ968n!gfNi%yzZ>5}Td! z3vEs*OxS*+FxU1Qh1qsYVWu6EpJC4gV_?$94mPK)A+EGOIFPq0C`N2qK#Jrd{~VbKzaqK0zLoN` zed-lwc(*B!diAPI_Z(3j^qjBW@3C5=*JH0{x5q`TPS3}h?OwmCPw{4|ZQe|!)td={ z@_T-8$K^Kgtp{`^5&Qm=k^6$9`1)`ZpLRzn)8=US577>^Jj#c+C^B4pet5jpcv!m3 zjF3FRNN|b5w4iF`fxsq}zJN}(ZvR1z4!>DiZGOviT70+bHu|2?tM|LFSLgq;c5MLD z0BSXXOckiS=NrubW5oX^ZZ|HZLLVN?VO(bSr6d1EKei;u)0#vbT9#-{3liM9bK--< zX2eEI4#y7(THH3`n)CMoqs|wy^P!V+8uq^nF zVQI)ux}~8^3uu&tF?FE!o)-aFhf%11z}U|~Tjam&%|+gab}Ra`Hcf_>rD@QDG&34c zbLNhv_=!zR4wvjtjF;(2NE39#1>oy0j}Ir|FeO%r_{CSZ7oa ze#9s*;$!37$nW)YBbiQa6w?A4K>a;mVEx^}8u$?7J`UXj1;{rFk@J9cd3f%cBS8zY zRcJihh(@#RIYXIV{JxA3iLSI5=_#qna?Qz^3iU|^$~B4Qs+9@#nx*mWIz@2&P z46|ca8)w8EG)arOX_6NEtzmi`(*@c<6KK5W32rNHCt)3CVGZOX{>6xY3G_=*ca5(X z74m4jK#4{R^k^vGn)>qGc%3-`;;q>cQjM7jvb7m$@>S`%N@Zy!s>P`_8u=-$+S$od z^)r%Y8KxwzG)_v~XOfV39eiz=ki_&7lbANp0-Eo6gn77ub#NN-KUjo0C`0aD0sjFk zE=TT*zKoU#Xs|?!dP~fyqu7btTIeIzP!K9vlNT#fk((kY&B<0O%q~*N&8pJK%xu<9 z&FIlfN*^OUF|}0Ka~jG$c{Qa$;^ie#(#6Gzas`DM3ONP&!i@ZK^_08@ zt%Tez-I$yagNW?KMxog|jf1l`LaN#{11bk~d@CmOyvn!edz4?$cQ1dg=TXLVfR<+|(*zptxrXEMIM%@~ z#C!wvmqT0R0nMC(Ip{#{*#-S>$Ilq4p&ZkyFV{^O1OA@Tf9_s zYr1TBOMyagbEUF>Q;V8+W50%b!#r)5`pr7d_2;ym>b}r&s%4r^HB7^~hN(MOGqv|z z#9BOrwYL?w+p97DMaaS5ctFtZ>qYJ}74;WG@E?Xalrtotj6qFG9WbHf0eed5_vFM* z4dO-iMT>>@rbq_%b#oAj5{RPmUiLp-`cBB(k1VO z=H#`&i98nglG{WCIZq^W9p>lpZRb^sTg~m1FrPD1%4B?L1o4|Js1}U#?do`*qr6 zyVjI!);N;Y8eg(l9YyA=)5&yIDaUAKE7xGf2v2v#8lKMbvpnskFL|1anV7~xCa$r7 zNvKc!gUE*x`CxOQKMMW+k+&XD{RHwqXy>d%4a8bJ1K)^e;9Kwvd1O9Ranos z^6MDyJ*%KU0sR@!9~^z_0W~bd8d#2J@X$`#ggMv-4`>(m!0bZ}#3B4Gj-&7&jv@a) z$syrs2~s$%K!TH6BzwY~WR81~)bS{iJeEh|A2bvH=uF}s*-4zkpAqK>_I?~j9mYX$ z;2#!3e-`wIp#P>033Fb7nCD^KDchmH2l@x04_&7d*oSc%H4x|U9*B$ZA1=XvxPtuq z8jpBa zLLb^u7<1rp)PO+O@jS*4P2;Q3zkwY57IN^9;Xix=|KU^Q*LT2wBylsOh#OK}+z{Bo z3kiGslCb+Y(C^{H@8N^)p&zJ8A=IS2XBhgu^WZ@(!+8MfEf4dZasV|b&<;3@8jug+ z|6GIrbMu}50xi{h82aXg_(!Q?#P*pUJ=nQN@kA89WJpkb|36#N#_m@^^>9 zf55l@r4Qq8MHj2rz4d_7pdSPMK#bcRW461E8j#Q6zdXb{IPm$xXYfCsBmNi;|0VRk z2KYE6o^M;x_uqk69MpygkhtgpH;@c)Az~w7CD{94reN*YA^s)X;K5+c#beq+G2bE& z$O-Ybyo>QeUkhVYfvx~rl0U%zcmbQ9W5c(B`e?x-sju}__`Xjfj%$-=77au71#uJfy3YoxC-vVb$AX>;uUgr zhHk#yhw|Qk5P1(G58ypa4xWPyJP0wgZ~9@17CAQsfH;r?%0VOO0)t=%m=BghYYjY* zEzsKoy`yj&&Lb>05u!)v!ME@U{(vv>PYuX>{b%^udzb=peF1z2X+ZdzL@}`jULX>r zfg(@?omOb{BJ9&Jwpq}bfV;2^T5F-R75>LQ_!7sVd?`Czk?~O!EZqAg#bQ;1oD1@A46uq1q6cxj5P;iE611`pwo^eHx)~87&^1yCrv0{Yk zx+z-*kENEb$hFcXxh}dWH$WEzqjXL%m(B>5&?&(>Iw9Ca$K+4YQTZEm2xjg+rC(^b z665SpW}Izct1{zmnFRj)H!*$a8^8lF#NlorLBHt==xYr%dLlHUyGp3RQ1+v1!f5(X zB@I4HKAl!8hsRP!$JJWtsCqXYQ6Hp3>SJ_3eLn5eSWbI1Hq%axgS1`aB5l>Y&)KN? zJ$IcJ2d)*=jh%-DEhEyTNb)Z=K<4-WtQ5{8ff0 z`6~=>^OqQXCAQG$FY)ThO_b=Y znE@TMu&0BTzO=_Gl6F`n(^ji&&L*oO?s}_A-df8B{wm8h{tC+;v1OLi#FtpimRM-9 zL}J2XljJ;$LsD}ru1d|ad@40&`J42xCEhq>#iWMbF^E>g?-={H4zTmRu>Zjyxo;pY zv3|(G@u$bn*vrxp2Tj`RWI@}V-Ds0@Fs*Zr<*af}nHg|>Jb(K;UoTIu6UOX0;V^iJT-^U4q#_so-+ z=}{^<>Rv59?A|0ZH^6fsoiml!wO3hvigbiLBRO-BrsMdJhQmyj- zR;AL1DOdO~MW9gb!{p!d73Sap#(WL>XOfV6r@;S6Med8Qx1b+uBV}o6qy|ky!heW# zqOnL{?(~Ro{y=z~cwbnmWLId8bbCm#Y+G=ppgE{fp&_tSsWxCpSQRi=wcLM=da3^b zjbi`n8ifHb)e7Og6a+G5pp+lT6yNg&VsHmBxRi)|D-HfbCiKCk4BT&{AIst;X(C>Q zX2l!QNW2{l#d&i2VuN{IG0|e}(MggmQ5n*Wk@>Q95oLnv@H)kcur}q=uzuB|&{^vF zAuBX;L-uH9hg{am4E<6gGmNPMAy5WN?|Bl0;~)k;6vlr9+PiUi-;#^*qg{@EEJ)?i ztP~|0PSK@-6f5dYcI9*=`SV&6!^N5s;w0+gQ>Cioa%3xFiv^`I)rv(i&BDCsUe)ZV zG4+hdrJ8AxJGGJ{FKQ=6exaEZ^_N;w6jK30p!}Xkq3|CP;6J1z2GHJ)+tBre$i2}n z%0tePg`6W(K>eAT)SYQc?HP{LlJ3oINDJZDq(+NZq$Ej}CTGeNB^3zr6Dt&R5*n2= z;=5E+<3}`-;udMe$8OV(jXkd&6ZcdrChkx5m^h{y8^?q|`91e=99+R|)^TXu3S}W%Qd2=TwAKkapzQJ2k^?XBE(8E6C?{V(xr3L^W?J9 z$`sO4>y(pII#lCRrm4pyFVKuk+M*qvbXGeo>9JN=@*nEq$xIcfgeNf}P!@3q^KcRC z;0VUN6PoMGFbCzxIVy0^k9G)s=`I$h)*@wUDAJ?aB1@_)bmo*6_;QQ#L;3l6u@c$2 zDN-4^*|I4)#qx>SHA=BrZ7PwOgKA+J^E87qHfjZ?pVA6Qf20|Z{=0f$I#UHgp!}ZO zI1WXA+yTUYEA&@aBJZolvv)88dhvZ*C5P%OI6ZfOuNvm{C^ ztvFFKxhPXQzNkLH#R*fsA*Z6T$s>Aq+ zRdM36m1&ZZ6?rmY<>i8)vL;2p(q5r=$!t}RlC^4X#mCiLi|?zt7X7N?TEvuH3*VrG z_6?V?_KzS2+Yt9P&|eJQ@g}UpX5_vtxbJU;ejEIURzBslDo{p?7NxeBQgSo=hh|TT zZ3^N>HAeHp8T@Im>PuyO>KX(dwcQG?H8Yi*s#hyJR)3)EP<>z7zVcTk`wFJ` zhH|E0|Bkab21PYl8xi;A(4P++%C&M;0uM?BBBJwwcEN|A(HTdlNnTc@l;>zKf{WtF^j^HF)L=6eFG#$N@N z4NPEJ|BgDe;22`C19Pw%@m`2=&z^$ZuM_J4xBjhtc>dgv+-C^y0GN*4XP84_!*Ucn zU5x^!8Ia$Q75NOgkk^nuc??F9+dwkcxj&ERFttj|cIp%ftG*FQ^WGIwW<5uwOndH2 znsogpW!%A}joa}wbqbSt&wk9oM$G?G=+DL2M=|bxXtxhw55P3!|0Br%XTX1$jpzS! z@%()rpWNpO$QAp8oaY#loRsN0uw)$ZUlwnJ(8S<7Jj)wA6(RmIjjEl6cZxl1ti)Ye{oaAE_@| zK&lJ(lgfhoq%!X>j&Kg+3db2wIQt*gWBo0L{%q(^hkh?&-hy#gL%V1JY9N*%|ATJm znztT|^CslqTi`!zL;km2iqy9$k?K}0QrTik%A1`?X>%YcY)T@*#$uA&&_*)rXOYyp ztt7ej7Wj>%Rx^@X30C~WGR*&6tho`)`_y5a3o!QD`B;b0&RU6m80+8xLDvtOt~;?0 zVh^6d???WB5dOnq)E^w-lN8>SB6bLW`{R%i{0#@<9K=QF04~1!aiZIY#Vq;**n5t{ zfcz3C0fzS9%!mFM^ar5dK92l%A@X00I}6&07;iXq{q~{;1e$h7kb@sX4t^3j_-XhL zXOaJ(NB(_*i{ek@`)7qf9~VG7+zbT*T!hZm;%5l%nGLuoUBwO3Pq;}zA5Q$kOz00{ z-HGae8Wv+8&T7;_VcZGO7S+J`Lf7>K#(xI-(9{1A`j=4yag9js25JyKB0PY3$4BC* zFHyh^krp-wTjHj`AD|zf;-LEvCis)>;1>8E{~vvL$291RJfKE|s|;h$M$8lTy{&=q zIfWXK3-EuSX>uL$hnD)s82_iJfw&9(`_O*yj)!R9fB(4@YEKZ8FOA_(cmd4I*XYOB z=*QOx?#qkdF?jxl9_Tk??km^AgMofB^dmn&{1ImtXj?Rl`P{%i09_(c{;i#C!MXOIZ0z#xDi+W|1ipcUU%ufcg}E9zkIqnM*O z1`&6UOE?A*XEW&QVSH-PR)nrJ^myOl^M3@dz|SJBN$s!r`Zf3iAlxV*0Ls7ugn@j} z2yhU-c~9#QenlU@ih6@L7@9qWu5KJ8-5#4T{Kj5FzqGjDbDy1<^oMgHZzNz!cC2 zhQVyG09wnTvktz*Huw_z5rUJ@yn^t2hA_Q=U+^n>^0o%$E3_hgk+#Ttc=QhJpd%&; z4}wG>?|;(|ksss^!axehhgK!DnxNB(Pwj`!2z17wvjEew9Nx!z_ygPFOB}-TIE&>W z%KATtSMbAs>pn!jhX|4O191NxZ@dp#)L@7Mf)_!Wzyvq~e?&4Cnwiil#;4W5(P)8A z7jy=oGlFF~4vhtf?FxMU2B_|Y_i+>&7vX)}K{Q^#_xSa{zQ=8R>Md|{5=;^PhdgR9 zq~SgAw8y?wB zcvCkpC)eP9T)}T&Lhs-E9_R4MXTh0CFcleiFz_AZ_yAsr47?a|4aD9U|Iz`m^uzq2 z1_NHqXPA>u;R$_A_0VaBP8W0rpfduUap)|9zp@%tIa{G|7*#?S;ZA)<*eQg_pmKU?YS9mU{!QjW!b+JskELKPt z#VhE%cs-pJZ==)V-E>l7fR0Oy(gzZA>4?NqIwZM)4oL2$eUfKrkJKl$OZqF?CJXmY zj?qT20WUdTKZ!T56;j80C^T@$gKyP%^qsN{eF=~3zO)`{Tx{r?tQY*2a5^hUqLT{Q zbWEX$jwn>pA;ktdpx8$H6nkio;t=gpnn^p97SJ}O)wD%v2W?V1P8*bO(i-8Hv{K~{ zTBgD{OH>(W5m-10ri&U79cUZDW;VsC*BDl|F5ZKIe%w@2r4NNBsBLkg57hkWpn5cX zmK55hnM2z(i{Y_U(Ppg%+Nd>!)@$|BTCHicT5C3~)Luf%wKvid?Sq^}+Lt*CbUx?I z)%}G#TbFTX>N4I8Fa}27@s}Y^tI&RC3vEYSZoqXLbWvM@j_c{tK?5szE*`Ya zD1G+JYvM=Om>Xt{AUXQ^=`XR&cRXQ6Q)cfRomcdqe#-nj89-Ynx?ycx!4 z`6DKu@u!*mAU0si#HNBi&^w7&W;oqL|Gpb8dEW3pd~m7t#3dGeIc6?R`z+LGyQK+j zuyUj|*1ojdCW4mOB+^2g49;9|O?7G&?{Vyr>~b8I zYImHFZgW^C)8cSYrpe*DY`xfSzubkjRVeHqTeKr#QLo{xE zqLBCD^0Ec}TI(f8OT9E{!W;gBw-e2P7c=Y~#u@UCdNvOuQjEz)SEzi`KGMj&DU z-Q#h%{Yu2Weggc5Sm>i4OHmUuKM?*y5d4QATN(`Vps7KDobJFVZbv{OuPq=$tl2+L zywR^zvd*tYs>ZifrqZ`huFPkKpx9@rLZQzN#eARhin%_I6?1%lm(TWPazIu@mM@bL z@dPmt)dHP^{*ffy?xiC425XYxf1n=|5qz2%u0+!#bg4hWf_frcs3Y8$(;6PeZ3>I! z)rY3=YeKWdt3rw-%Y!SWOM;tZ3xm1^d4VJHIf085GXu9Mr3ao?P7Qphlp6S}LP`*m z2XgO_MJxL-0P}ztoWl6`ry=Ldg#QBG_#f!UEc9)9tbqDrHK{Wedmm!$sX4}z>SKa9 zHPMm0%BTc>SyZ}sab%ukVMLj9UPQfYc6f&%BW#*NYS@HQQrJf2gwT`1xUl=mabdqG z#)UC?AOLdjxrg<48RI>MvG0NQrX1uOxwzj)I|tv7pdV9{q^SdYq*{^;s3FOkY7qvtBcM6FkjiaI8Yh`I}Y zR*Z;Z3P2tRfSias;qW36gTonE2hd)J+ni+u$UDJ|Jlt>N=baf6)SRwFb?G`(m1a)m zX^vEq>P>~I!Q8x*XkKUnvH~GX)?Y6vqVbxs7=^lY)83#&H1M)kW}t-~)_+O}lU!J8uY7RoEX9D7RZ4y- zhn0O(K2h>b`AN|?naKmeJLJ&H-N1Z{YNB>SYeO;CK^bCDju=$no*!)+epZjZlo!cR zQK1Uu73x!VfhA=WI8$1I4<+Y^auV}nd2x9uV$r!d65+X}QXx4FGJ)CM0>A7rdGD+h zik?{q72UILE4pX?sNkN#1n-b@PiJos?ae!a7;J~m8t5;sL=37C1LUE-Xq&3=+!a47 ztPrQ{az#on*P;}7F-c{1lu+hDv1Ngr=+a1TL`fnqv?NO`sJK|duc%hayQo9Pqi{sd zwP2aRxnRG%Q^75PQ~nPEr#vR-n9F1*a}LMCeyo8_rSRgQH@_A!Kq@fUfOP;GKrzU| z&r<4TDWOi4V(auNs@9SsYT-ZB`cOzsC?~Kcj_Y5Y&hx1%5c906mT;|{BIQ&uUD}~y ziHu$OURm4nn=&?K-^U2AdAqNK z{jRn4-skM~o^{T}S5jr!KK#1|Gr9=m5pqv>*T8p6gXvy@{;-nVYZbW{NLpUb=Hhf+`#yuFzSqtOjKC7oSnCkV!U?bO`Hzl-NUBAp(w?drO?i7cKi(K@NZm-27}Lv1~agZ{C5-i=T`FX?c~2Z(I57Zf9@my+fV*?z))QF zTZ;2OdvV(9Ar5=?J(Wxa5c{SgDR?N0NCuWm>7qcx|G2a9> zYE@<%rg0qpJ@9W?#yrILrD!n2_}o3oJb<@iFEt1Ur~x?4HTa|C|0l@*PLcneCjUF5 zBNk`O#Qc=4n4WYMlM{hrbRtd+sY%g4Rwueg`$c_(VBUU^lixMO-;aQni3j$BebYDq z|2Fv7!@mOG7ZB@tJ9ri;{#G4g433e1!Wnm#8i+f%27f2l;4kqEihIcaFQY%)OZ~;Y zrW{~w&^BB^0Fyi(6msBYvfa55@s(iS!9PC#G7Io~1TeVI0i^{y82gRzk2B_r@V$?C zx58b0ocln|a39D8YCzx&zZd@d8G{GO!5<o?gsc$mw5!DbQP#kd)POw8b4cNGAe1(*;{WSB1N2R5Al`!i?HRm7 zd+Yss25ioV$tSL86p?5Y#N#Ut{9iFQzQm5t>G*Rx_#9inEe)s(e&ty>CGckwvV_O+ zA76c6Vg9{={sM>9`;7mG)Sr9=-zR`BNC2nr#rcBv%bC|-(S8HIGeL`R;5i*3BrtiP z18~6k4+H+6bKuGpM(}^$an`_j)&TP?m+?-%fd+-|LHO+nztg+;e-r=V)VtOHw{Uz9 z{sI2^hu62>|IBCq0so_cWbnQb41sa5aSB@D`ubG8fzrAGU!dgNN)1Las0UqO2u$At zGF=O@o%j2|5pV|F1+D@mn|%d*hzWlqZ~g_1VEP`E|6l!KD%V$Zhg&rm?z|2HN)1LH zr~<8^4=e=B!35ZhF0ccR{jeNGmpDs?e>WP)HMEIm;C+J-eNNu|PxON6dtknxRrHUW zK+zxGn}JqO$@Qr>n1b>eU1lm4b7|8-DQJMN3%&ukM&VdN&ubZq&1fGx;MkAHN0|cW z&?W9cCwK(a;RSN`_s|Od3h!?y0Mpzb(kk4y>M`EpwbsZA-2n|kZb6BO2MB>zgO7P| zR>9W-Uk^M(a4cr}t-#9(I5xqt1CIUpcZ?A@k1lZ^8UGEI$t!3dpP)(nb4v4gm0p!m zdU*y)U4{a!Df)*Y`MyvGA?P4eT8uM3`@%aHAJg%z7@unCtqq=DIEL_JG0SoV91}!+ z6WYpl)Svxu9H-w4MByqmAJ6dLJLnOAVV3+#g^Cu-wA`^XjYnw}Xe|t}Lua~-Ntf3` z4F+1wx6IEkxj*R>Nx(O1FwkP&V1B-aCiF6Pyo4PuFfPwAKb}Dcc?vt8ARafUS$d2b zok!4w9)jUPtl~)}sAWIMz4Uw!xEovo$}Co(_20u8X!MMZkkDMOFDFi&{L;mTaBR6c# z}mvdb%9wtJV$q<5Wc@ote#-d(cZd%mpoUZ`5* zy+XChd%b$4_b&Bv?-M#>-uLM&_IW{PkZL(L>Lo!-bVh^L=q?DF)Ey4GU2ibxlK%Xlr}g`SZtC|0|D@j?tkv%d*6MZs0bhi1 z`i(<>NM*Z`34b=*AKG)gXImzRB8+8sgq=)AqCZ3h%UZOURZ%IjA}UvwMU}{ssA|<{ zRHJ%9WQTej~Lg+zHL$+`wye4 zSgiuX%4rzT8hnVaFQ>EZg8xb}`a>yueQ=m<2W8u`HI3_yDdw^=#a_lzJY`X8h>WDh z$Y5%U^rz&=yp&>9cXE}wBe_whExALtIcdIrW74QWoo0B;Bbhx|}!%ZrufycB83%Td+k6{~7;t97b!n{~@`y7fzPh7F6dR~Y4IZ!yWq zI%1lYb;T?*>lL$%tZz)yv$V!DFiOwVPT_U7Hjfhnr55OX9U4pnF<`5+jdne@EF&HZ z%GA7 z!6uXR{KKXx`Ik+T^Z#U;l>b+gqvOpTZjQz-Nb&M zc+9WnnjiN_wN%{j^fc9@ z2JzMF3}dVI8PBb{%Q&j)dE>~+uZ$uqvz;jaEA;Xmyp`RzJyZ375>4c*$rhBaI?3~hMMFr@xVgWx)?L2&I1YG}cA=D|Jqc?zC`-Nc}GT7wzyM+X9Z zps9~NH=h^GGnDKeE6M0_kTmX5O6d-gq;B+w?j(ut%8}TvQi<-WS4DMpslz)*bV52- z=?1m$(hF$6L(jkM89l$YFZBFcwEDiyGiag(*O-SFT3L_q?}2N}z_bRlY#1E~bdL}N zPy(`epFC_T2}3p#H#A3L2EAqOV2DHw#!AFMnuHA$NGOvhcz&A%^be~1`d6xb`gW+j z`p)Ti^ggBbnD@Eby<4l}-lf%5;MO^fdl`e%#Qz}t+XsmOTuL3x=qTf`m~mJ_?l;E$ z2TRc(KiofDxRZ_;<2bt+!iermxWu! zdBJIMTJVH84u2+&Lt2%?fL1+)`Ly67bKofayGc7X!~H)R%u0R>W1Q!JtVVyBKz~?^ z{;-bvhjn`5wa#2T*4l~tgp0VY@fDXf;o`hnBaW-{#9>vn%pUI)J8rD9S-DZHR~!?| z<=4e>*=J%orWLCtU@;igsuY}NoRwOjZSb$h=ar<$3*qfsL;ek4^+x8wX7bOi#9$lx z!*=wC9pryIb!7HVQ?cD)BR1Qe#cG?6SWZTW#bmOWZ7mYhEsbKlc|eRvstq>o7rhNv zM0fq$qPOlR(Vx(Y{u;1)8n+Ywor}o-iTCOiPZ+{#$7X90;gd$y0AMzH}p-2Xnlgt4QZi?8dJaA_(9qm5P!S_YT z{)PyxoyK-rpxh6(g>jxB?n{aH&?b&Y_}j3HG1$+uFb|`H9VP!fLH>D~8icbv2ZHWw zFOdIVM1Q!bW>TB5xUGOA@CMj%E)#HoJHtSq#<5cvfASH8;Ey>N|H=e_e+HBAuY-R% zW36Z~Jv&*8aF^p}F1*QbM#C3;7d0sNPy=xv8qodJAYA1hj0eg80ULNO{1^fYPCT=L zC!b-*gPaT>fIt7CRX$VR|DP@!baQkNM=q8HlPnOAPdO2M!i4`AqL1nPqnBwvnZi^Z4j;E4 zLxaKhLgJlv6%Fn>{^O%^50E2VHgBRoyo3JoF7f{Wo|}N7B@z3*{dDH_XS82|zZs!T z*l_`sP5ZAIBv8$?O8^7@%USMS!AUR#f7dbQApV!$%^H9|@iEo_arTGb4gT40<3GGc z3f~vtFMv>z5KiO!nfAMx&z1HE@C(qYfF+ilRyl;Tl!E1m!`1n5> z#FX}6PHPKXK@hz0{OfFDSq5JNib@Alt)D5O+>0{CbR36cEz@%|96RyvAlk=C{JRrf z;u<{9!}C76#NVfMkC*86MWBq-^WZt~EYRwk@PB3uDq&DXr$$+m2HKQ7yW_;+v{(kO)8m6vf zF?z%bxsDx=V#mXb%Qfy1x{5Y+1x4asthoovFAHQ*5X0L+tGf;F7tv=A@b~BdI zKZ(bejLl7Igx;neZPtr4gNI4(+N)>EW_v?qv#&X@q7YZWeYre z=>NE!<=tswae`yL{bqus%3fX${_*#v&ELTa@^e{tlNMkfxn4jkrr|Dv&hJf&+T zj~KY1y9CJHCed=iG!>mCPfnSY$#JtKsH&vB@T1z}SOSTchM)t$a?s@wg()oJz9>a>7nKdt%}J_%+HM046tBKB$I z8tL$-l5=4TrNXj1$UwFPpg#ms{}ALWD}uviEI2_%gVSYUaGneYm&#ypjm!^jmcHOF z)x6*VRd?_bb!YH|x-EEzPD}6!-KOBHx(y+(>DGmOqgxxI)vW>5AzB>;Rp3Kn|EEM^ z0QVJ6^B2hVPtqRdRJ&WlJ}+7&Ys1kWBG4Zq(H|m%WH>5X2BMOrKPpS+MHNbSRE2a# z)k}M1o2oUkPu(23P~8wYu2UB|saq3yRIe)XKD~;_Kk1c4{zb1eN~>2Asnu0bJcDZnbWNN<9&(QowKV75M zn?fQjcrBT=Nc``E^IQd6-6~=LCV9OUo0ek7aF$wnGt8wkW45$paPLEgztm+!NOgL= zRHmm&c}A|3Wt2)uMy;wSqfMQk-mjCJzF0RaeVtxL+CKfXw2KBQY0nrWr+ucMl=g4^ zq*SfmAMq#R{U|ZGyPPd}HS@5RGyrU>AqJJ`57^ev_Z@jA(v)W-b$N57I@en&azmsn zH(H8wlcXp&TMBZEB`>#Hm6O||&dTZ4NzYlNo0>DBmz2FnKQa3b{edohrjVoRwgc97Cy4=E}N zl!Bs2$ty~boT3cLE-H}BqDo0GYEq>X^{A5y7w9AuuGWn!+@%*&a87@2!4vvX1t050 z<^QS|nXlE0%=;aR7Ig)lbMPH$VlA|gceavug0W`y+kDNY$Wjl1k%g*S9mEF(_ExoB5 zQu2##NHI^*S5P#K8^rn^#^4mbAB1}2c0Kw7T1;xa zha}eril#1766+ErzAjT@>xw11u12Ej+Eo#?gQ~FFL2F1Jt@$1PH-<}fj?r9Lu?jG^zUMy~18^xvTs5p0C6Q_=M#i{*A zaca}5oLWK4G%gWi<({y;@Nb1@Vu-moLhiYMaahPWfbvD?4?qJVLEu7T@m*jg-V5x- zbHrUdMgqirI7(cHlV#3uo;VLxqjhwM{oshq8eA)O1Bb+B{sUs&|F+ol{UA2;xQf{G zJI=Ke1Ne8~?=1~x@hBS15@N8FxwxF?Fs>l?T1gB*DDVL;D~-i*rKQ-fu$S4(-NkNs zpx7>p7Mo>hVzsmct)ofI$L5RKl5sIvyhn^i?-s*FuZYpYZ^d|oo$K%nhR9ABZ{>ch zE%-eF_p&jLN$_@$qrt2u1{37}>&X8%5QB|qFq^pkzZv}jSZy{GvrQIavT?Q;Z*Ui* z4S`~?K34SCXN&H-Dp9XxB26qt*x1U!<_v@UG?V>H4*pt#4VF#g0Q^%L41cOs zgSTZpH5l*|@Xu4XlYj0+gV{~~y_ftO*zH4q*pL3O--v;(Q3YKm)qS7|@ON-P9ml<{pfD zg$>sX?$+mkZ^psKmd{+!If6I|rO;OLzL)QC@Gd&M>pJ+D1Hw;0%VvRJvsgK;!HnR0 z_b&2pxJ&Rii{VSS0RJWU;c|Zf{s-ZIh?rJgU`W_;I}CaX2B%ukTGsNLjF&Dv+y?o-teo${}JL3hy9bx z!DsOQIsAVCu9v`{z{`JVmCuy-JTF$>FhK`lY(De_*zsXKVBkL>(C-t>_p#&s4|xBn zEZW8#+{YXwOqIl3(O{D9XAR(&|5GBa@XdY&{o!@S|4sClx8QgOya(O~bTjo@X>Yy% zh}XC&U+be!SW^q)MFN#fiyc2MLYsi}hcf^te|T*AUMTo$;V;6x4EW>lJruva;df>V z*}`oKr;gJ1XMp8JGB~Y;{Php*-)3HaL;DZFkRue3#F+zkkO->509Xxn0HuyY(HxX| z1Ep1JAC$U-V2}XvfKr3e0(!v+7z3-p2Cxn60f)gUpk$O+!BgbTe)_S9AxZ4nxsF6g&JtG{^uYpdNGpMT=R8Ys=_gc`oTjIJTin z?7`*3WY(u)yaeY%C?C(0x4%oK`#1RiL$*0x1ETON`p28#&p^>2rnQgTXcg^2(IRF6 zPY?z)Adem@;b}%==_V&0ATM7;|I6rqHT_RNljCdbP@c*01+_7sk!gR*BKepJdK1;) zLo9oro^JWBaK8c+JwnkUru7d+cQ68q7NNvs4!r*O7>#e~_*9GzQ3FRS96S#cJ3b~J zAIVbsUq%1x>3=J+-Ho<$JKD!t{JRh3;R&pG6Rxk}`t84($1^ZI4V0RU>tI^{paw&k zI>P-Z#N$Wi$M?kI8}k0I$ooH$9JtEhse_{xjvhD$@MQtZa|~@@C6QkX#}@MR-OPp~ zu-`$xewD+<^W^Lwq7{5UHB;_~;R?79+&hCQ>28W&u|Y+wjquru+2l+d{TRC_I8)%u zgR2~#dVFYwqZ^L-M0^B2VhJ-~1-i;w^ntB#?B(!z96jPL{(B6i>~-{ruZYgCQ&09) zX75RGVg^%>(!qwG!MEURQ4?oVxb4tC+~5tuH-3wRF?kfN@EWxs4`9cA*l`&>=5F+u zyU;-{qQ~5U9aI}IBeubD04vTAk^7k?&!Z42RXfU^RMRVIH|?$&@U!3Y6Ni#-IIMjF z-sAOKXfZF#ZE)K&US9YZif;+bvrwvn+vUUa&v_Dwzm?JCle-5i}&gSrI$0452p42xW z##VUYB$rga=q(X)T2CX#^|R!5gF-oISRwlj>twG{i|jV)k{w3?>uNeZ7o1w8>(Jd9ui1NESGZ z$*|*E8Fbt!^Bqr0pVI>}kB6UkIsZjEoKc=Z8)%)ur|`c4?=w-9dB(sG?%?%FK0C~J zdw7P;B+oS7Fo)+qxcH&9gv&D5cyyLDbe3FnmJ%6utCB&tMw#!{A${)sGS7XHbi1#T zPLD}x_c$W09`{PK$BWYF`K8o*{zp~ksa4g2ni+f;!09%UQ+_=8RuWr^WcWF?9LAR2 zVeGa2bY!jPZL-n}{lVKq7Wo9ph|gRZ^wG$C-wf&X&6ghEGU@WIl@8w)Y4h!o7QbO> z@>?zqew(Du?~tm-?~ZDlwJ{Q)heH#khXgJY#5I7Qlmv!#XFm?mmt8iE_8E~Hax zLI$KNWK2~NvR+jdvQJeKa#39r@|3zD^ka2?=r8KL5bYFlX%)Ol+@He#2jEsTm=k$i zlB3;`#dZ@r#);2pgnGdihp*L9NP-q%Tr{ZXA1t5s_h#7yHQ4JFCh)8){o zN~blL9cApbONc=MdwuNaV=LC4WFn0kYpK;ZNu`E+A2dNytcjFDO@icW(j`xmFS$t- zlAY8jnMqxeo;0FLO&V7xCv8`2G^cbDG>_=SYu?j|)BK>0P1OF51g-ix`hSr4D>~5e z3NDFOF%Q7nN>UJP8p4h)Y;4RhKzFf}vUGbXPIr@nbU(>U3zHn~mC8y_k&N^lNl!18 z)bu(@PVW#+`j9FieT6D6eNr8hc0wJU_K-R%?H%whb!4h`8Y#5kDdxcyVz1~xx7V;1 z>PSPt>RR%RYGQyLt&CB1zFJCi&CpwHB{z4DWaW5EMozG#<;<0o9E~LBWQiuHND^{u zBtExIVsqw8bj~tWRL&MvM9wjFSoVYJ(CoL=A=&>_hh%BhA(=DC(5i1R5AGrMr{Fut zmUDX}*S(uq3$zOxhygKa#J+ND$uBXMtYRxkLyJk_nKem8{*qV}F7ZY25?7QaF-7?@ z7cC~Ls97S4`XsDqOhO7bse%fRr~(QfQ27_Wq4F>IUge*!Rr%#<)lNA{7r15u|zdmN;vl@g*LcJNQ0jQHH1lEeZ2VBXNX^Yk@(ct zidTK7c+@Qr_qw&>T6;j|)Ls_nnpefS`de|XqN2KT3M%IctxC~>PU5Rli!=2+n0e$~ zeT)O>1-qEEaL7k@J-(fAj9ggDN;UQk_f#T5~CGPD>;@X}oE^U?K z+}0+JZ9`(;x=LoZ>=C<`OJdvnC$Vk%o7gt8du{;r)40HxE6*j~1&8uHm{kMRWDQae zG{ik1pl+DF1Eljh4utZZ&!C>T4w%WD`8MJ--$@+%y~MshSZ4Rf$gI9}vF$4no8CsT z!U>Cc%f)=&PBHDdAST_VAF`wGZ_2>kY(r=X_#-udCo*0`I_5=HhbZ;!5V?Dsr#Y z#9$3Em>>pg(O`fbFkh=L1`}qYv)WcvtDWh<2ShNTIJmClfV+|jv;xPL^ADFDLTtPW z!8;uM{{^%h0H?7R{w?@EF*>cmjI1F4hPRP_R<<4uW)t&Z3-e$r8q7BG@9pH@JIH^5 zF;ML=MB^|gkhWkB3(FTo(P;+YK(`4eH!kDkvy(-CiD171+20XB;a?3_k*zJ@D}4X2 z27`Ys8yN#Q^WaF?O%23eY5)#UgK&_05DrrVa69?u5%T||!ej!64bV2sfDIrXhp^*N zG~nQKuwlC3Wpmdw(`Z0v z8H4kT!3Am%F2a2$P+H#09Xh}mSn=MG&wcnlmbMVI@i~*|JPw_^4>9sBHVNM&kYmFs z?jRucW5j)MGjjmHYxh$Ffp2M;9D5E83Jz~VIOj6_SKz-NeukAzKAVm|w902wpX>2C zF?nPbCuA=W2TB1G|2h-v(c^sg1h@&XjVAKFSV0M>w=K)L#?Xby@7 zp|pw);sydhEVxyJ(a3A%ewaZp3RZv#uo>(C`@vCg4qPT@e+<>*WisuL$RWSyzhB8= zr=J6QllDEJ)Ihum6di!7HK5dAC{Xf#r3OQ(M~MVV4MriT0j*#j7^45h^uGdaU;>U! z)=)JCorvjn9?dR14>Mk zSOkH1kPTNEJoRw2<5MsF57Yl*`d>lq#~N5R!l68qV;|bYF^1~`5x6QhnYbUIPP~g% ziuQ8LcZK_TplA?JfNAYRpH>$b0wo@fSkCXU5{vI888s)Le7q8#MmRdi%lnubL-amM z|I3)3tLc9OUT-6|`^eXiqZQnVkB^{zyo@EEv6OyBmzaJIhoXHv0v-ep0G{9sm>N^i zf!h>+?ci1JLkWU27Jo9}DuSm9jwbr=p#NTUfkFIP$nso@PpjZqk6)AM68q5!PNIF> z!(rqJc;04o{>DuC-+NH*|YSI}Z!>cJ?0!b z$QksQQ`m6=JB~3fw^JK)2*&*=Bzx&$7qL+8qS_AQB-jeJ%-~lQ_rY)|`O*N%74Y-u z%vW9FYcAK(=pGbz{0oAU8VuISS?oB49mlcb2=nwX>*XNpc0YFP#g5(RQ&c_>i7ouM zpH*`fixidU6=v_3eDf<-PXF?fvceVogG2k*9LnCap$5a2L$4LreL18&qB6i|OT2Kv zPfz>{fio7qRAybS>{b=a4t1q$Q`gH@ofg@w(QN5s zIZ@N%BRh@4WYRcJHk+i%29sP_Yg#O8Oes(oET_THYs$|5bL56JFWx%Fa`fV3TukA|d zvE3|Pb_b=y?k;Jwds>=#0951bU!;M1gX(8cM|%_gSKxj;4F4lJ<%3h9%Hs7N;?3 za$YYD&U>ZK`3|X>^SD&a`9LaMew1<-ew};RNvY=02%3zRgnV*CWM# z3#8C*mE`+xmt6molI{PHWcj}(8UFtO+z~m0G}>43_j)2aR2o~XEXE+4EngyH!6!M*DLQvypkQhuIA2Cqwhq?>ylX=7-pBR7*Ig9~z zEF?a?*wGqpEDhn-QXS5-7s5THG(13x!o#H?JXZ3;QzR!MN3tVIBr~E`G9ue0Epk9o zB9}>0k=FJE#L`R5n9zPJd3Ya@%sY&$BLK-C2Y-$*?zMXU5Xup z>Fo2cqaoHnDq}39G-kFG#<)sew2$OO2TNvjlw`#4`x7ylk{VMe$+6Xv6x$++vHcPs zJ0@|l8zm<8u*{9UPoiR9mB`q?Nkk0QnK4>bMD#SCWbE&Q>n!|-%h-08lM;YcydLGV zKE7+sV4sz&Mt3oj{6rhcPIQutL{CXe43Lz>2uVte7mX%O63}AeH5C%8X_9EoJejK* zl}OEc3D+EyP|alt(Y!3dnr|dHkt;$d<~x`;;u zF)CxOomuMC3$Dcut~(=4MI=bumG?wGx=yDgHSl;+wNZd~)`R zSI(W{nf<(YWPdFlSzI;C1R2v%YLXNk=rH^{8%fKWNf*H=n9utbY^lPQd~8g`o>;V) zs1hp)FGhbTrv9PWSAvQ|C7?J~{EJh?w>V#XimSw{xJ^8Y2F1N-TwIHG$(+Iq;#~Nw zI2U{=&iQB$c{9jm2a1oU;81FDCR<4d+Zl%r#sT#5x{1%qn#eo(UPF8$YxN|k+Drnf z`3=%)C-JF5f2ax&&#FjquhNKHRgSn+mCKx}W^tpnAJTlX0@M)dCjk4 zUd>JxJ?1uW2H%ugoNZmq#d#bP`bf#=QxgSRK^4dY$^F#d^IZ_|>_vZ=XC!t#7Gm2y zORT$H#Hx#XK)S-@wys1m>&z9?&MGnP=n$ihg<{aYLG;>=h)(M@(QSE0^qQ%dZQ@EA zdW?c&EyMu+&Gfx`fMdcCTG%l8{{q&-LUbUIwTSuy5C(jC?*b^-6!Q@yF&efM!=c%t zKjbEQgZ`pB7$xd~G_(#T`g~)>8F zLJZ*PTTBebh`}=E;&Sr;6~tgAxfk#lCkC|Ez<7nOs8^UGTv!1%Axqs^P(dJ$0cF80 zVWKT&Ku76l(GCu}7b!M+2`B%;U~4%5PGcARiUzY9-^Sn`b(0QOM>Kv)d{ z+r??@)n&6`0xTI=`{|9xo-kggf(jNrvDi)kw;v$r4{|afDc41V|m&%u8I z{=4A68~)4iGqu?8vhf1<&4AC@^z&ZLcgAc;tkFqa(L*9YF0We==Lz*w^S6|tZuWDZ(LB)UZ*=;HNiK0C@Ln$BK%3w$L*jCm)%*WJz>#P4)`j=|3$c-^nV z50B-ujQ@-D`zP=Ucn$n{2Cvg9cmupT^O~+nl-RI?&&^1loVbV^#l0&fTukkyT?-iC zZwTTy&&WLZo8Yg2KbNJG1b-y_e)#M13~PX)Glf&OC>N8b9p$ipg^g8Z~?wxE=UK(pboTyKCl2NwIJhQ zJy5c?UEm-%iT-gBed0m%iRZ|u-Xq)oJGuMMFU`#(FHX6QkdMJXY8h@JUznd(6KA~Pf z-%H3KSJMAla^=mi?IgQC$TZ;hVOb>aGfCe;b$N@wzNPyp_rAOU6z$_t@GzLZ|3pR0 z(1@aiSm3KYyq<7|;!gr`%w~v7;i#qm7POUa^oV|Z8lnHi#C18otzoKe!oQtN-9zN; zXYuoXa_8q*J~!d~mt3cJMf*^A7474e9x=_W@K56>V(}ew%io8S%Z4SET^?rCZt zuA{{~gckDv_iJ5dZd}5Si`a1NSo54?TD*T`7l7H(=AA+72o`Hcb^&lGEy{y$c8IyC|qjZY7 zaU5;x2zDG|ejH$)?nRH;jSjMtIkFu)wqnO-#$_Y(bRBBQ1eUELN-ObbIUX)$UMMSj z30Mq9!M7&pKOD;5w4w$BJkBZSVMFCqppX7>8(z$Y*A319e2ZiSCc>40o|6Yx30zh9 zR1ae-qtMN29Y9;*J`i3l;V(w9bOEzqSam}Nb>5Nrx;*Vym#Qu@G+7RhrtA~`PiNtVkilIi-5WVmuBQh)}N242DE z>u_F9%j$5pr2%X|J<%UrsDE%pe{h{6^={r$;}#@U?om?Vo*<>} zX;SQ-D~0Z*Qs7Z5d1x^?9`hy3b4)TkH%gl4AxZJNEJ7K()=7f@eu?+LB(eS+?fk!zXtb9p_|bxAIHf-T-yIpu zgKV~=Iczs+*YJJ`pAW{d$6*^(AEYl80k=s>;4CQ!oFln`UXmRcC>enfk{%c@sX=Ly z9F!+XLFJMd+$agbJrWnZP-227WNye_i3+(>B7&cj@Zc{1PYVNKLDP7G7~G5BXW%}R z&lZ?1*M2MXAwUOeHtWTC&0%B|XetQo{TsDJ(=1!=fc2JW1lh zvn3WSCOV={=0 zdq9BxoGFr zlXLNT8F9(sZ#0ZmWP!c}<(W$W_bB=0If_rNyLjdLi)U`QxaTH_8(NG@Zi&pvZ4js2 zdE$_}MC@}m%dDIeVwdx%*k*qqc3D4*9XgC1I?OCEr30Zs?167{Gvm<8IDjG0)xvtf zo+7@>AU5&X6Gm)&%8bOL)I!`!`3=(2IWnizOPorB#jzw>>`PK*c1ZymM~&E)bcuDz zBC#soC>BLW#k}YtF)w^q%nN>y+t6Yx&|(yvqt8QdY=>`sJLAyFICPPBfp%V3caV3r zF%Enmjg3Lr!@}t!@*O>Jc%j znh?XvgQ8z?A9zjl%l|G0W$a+lV+>2EX(j%9>3<7cYr08;=W$%>V;n$JFZDnmo6iz@ z$bI?V58GU^ZB~b_ShSgnS(~+(v^t1!EBAo31c*V)T+wey6I~{fy17wQ&GYGKC4;(~ z1$GyKC1I)i8lmmqOm^)Q4&&Pn`rH7|ct5%40PA6p{C|i)8|L~uNCVLzn9sZbvbSMs%>P)P%tozlXKBpS6hja}JaL9-#)}C^ZPj;64tNRso-}c?ZPgh$*n< zGiSaFq)h_ld^e8;2*Dx#!NJElAbkoLk9}O@BkqfccQ1Z7!(EPl+3+UPXE;2*r^&z1 z!G8yH;39M2PPi`3;BH#nntIQ7Q*|j*?+w_LS#UCRL=(Y|`#2%oSI7GiHY$^RM<;g^ zi%Wk+sQzV2gXt&ct@vFDcMiU4PQeeSFI+B{;lCgMuj2nTcpe6ig2%x1KfHc~*9w&H zZh$9L)Qgz1S+++D2?AN51rYQXA$su&n@H?`lz3V#;-@$iSj?~R|zvuLeeK!Z{E-k9n0ecBJfP4Ll7`~QB=(wj=! zzM=gG_(h*&b`}T)g`f+p00duqj4Vgd94vrRi(vi!EN}X^P~M|`6DS&hQh%Z70Mm6BiteD) zU?{fyu?8cS_nDvs)Y5+|efOZM4ASc&vbJStAFFV6J=yFeTzk+bj-XGRCwG1T_UGXK z5Z&OP=o8b=0a5rB{o^_CB)9>l@B7fBRiJ1gN<5r^4~W2*B=Ye*ILgr?>gl@;Jz^fA z9zgq8NZ(`V63R0<)-g0&VcdiEag@CM0{XbFEdkO91aeRG;CG#Em`|q_M_wf26P;`jj`FW%-_WX-jDEFc8#4CLA#-A{_65z;0 z4=AGlN;IT;v=!xElrH-3W0?-aF$%|We4AhtlzUP3p%t9MiYstEk0$X6x%)3uy2mM4 zPk@y)8igyilX4{1lwnz)MF^V{{%QsT#2P(qHO z$|w~o&tcgos3Sj5t)_9f6|4mRoZtY?0#<=C+XJ!TbltYEG# z!;Uf5%wl?9$g2_lVh~H`<3TU8q=#A6&8kvV&JOt7K^yptQ_CB+)Sz%!f7pRj3Wu{h ztjT$><#x3J+!pvb3%$ja+2)IHVMICxh9tN$Wsxdh7N|;PSY0iH>PG3;X_sEzKIze2 zD4lxa(ylitt@=l#N&jAHFnCew4SC|P(XUctq?PI!d;sT5E}ViqIIVf1ztA3^L+;BV zey1g;eCkow=vvAOU3*!g?=A}s{AJiM44owgoh3=;8D~njaXvarDLPAyw3;+avuU?9 zm<~&w*>b5d+bmUPho#*7vXq*?D8=SqN|D8Xr0_PX;6MTRKlq;ZMuTC?ago#8i9k;8 zzU(ovWup_@A{(|nW+pOXYAy549A%!lhjiT*Ang|6(rOWl&XOz*mYGs#Ss*o*bG(;-!36s+7*kk>Xj!QaGzx z@@Kb5?(AO4wjY&D`?Zp8zgJQnE=aP&)1q3^g4tgY6K<)ORtNM4d-Mkf2dQv$lTt@tDRK&t0;jo>=aeWp&gqinoG%&9 z<&r+9K~m>*OR~!X(YUOZ1eaYB=W#u+-0}9Y#p5}CVe+=_L!xk)s z7=X>m!~lC16O#eH?_`Tw@1ajE&23VQ7E|EnEV*u;lI<2C8E#>c<{l#{?#Ytuo-G=W z5=ro=lX#B~iS-mO^h+$?>w5OmBXB!rN7ny?sRE9V7|f{Qjj+g2eh{NDNxc zT68B5^euAymzFfA!Y;`7R zm-2oHJG!v1fjE^hN3sJAB`v^0k^<}`A;4MU0z4!-z+dJDgh^B&&wmW$`3q<# zBiZB{#HWUFD#X_G2t70xGl@lunH%OHk)du99_AyVVZjm-HdlhtVgkc+Bp|#@{305~ zH=~j zQ4Y3n`;kN@qd!m=APbsUqxAn4?MEp`=SUj}0o z&-jGpsKh7Jm^u>lhYUM$&2W}E8J^;t5h#x7QR0x1B(pPe(Kjl^Hltmv(?`TAeL^hL z56W$6_la5R>tdezt=yKv&M|o!=jr<}{L1eVt#2enY$E4rChw$e=Y2J{S^KwTqE%y+U@&GX|n=6K8sc0NUqE}WgI%U10DqY5d47L-{ z^CYs*Fu^|qKXYRNcMzk;9HGx$aBPHgyo>eF!4Vyk12>oB&Ke_Xqm#kHBBp5j3BI0@ZHIQJuo zrItm{v(4b2r2n<>E$?L<`jH0ab8H5c1FQ#-#Op}l&v$Np#DKrEn8*DG{C1FZn_{pP zlgA#op>z0yxeR1FU{beXaN7tSt|Os6!a{olapMCHIzM15Hh^95Z=}CfaEuKSgJHVj zpVTcR28)=BAa0Zx059OgdmCWF_v!^|2F?hL!<>I;%|v$K;O+r}Ik?0F7F0h2*oX1G ztI#_Ru-G2J?zbWNC%}ddt`y_b1imhZd+J^w!qz-Sm>9yG70iQ`=uqS2pLF2@?0`AY z2g2vF5<8X~vY^cXb}Yq?rJgv%0e&o%MOMZDccOVLA;>!*xeL2r!|rdf8yl!cxuwA@ z#>e?f2^+lCt62*Z9Fx{lgRqergiX`{Y@r5VD>VSXa1w3>yk;ZG!4+&|TsAT;8yS}k z*s;M2M6uX7u&iTots}^5>2S?yg8mHle}(=3;eg2Rp%B+84F>K`xa;_{qRr@FaKvDC z@NU-PUh?1lvY43AD_yJLpU)12^1>+18Q>Fp#5_mKAuny=yJ|F>b5V*6C&o}Tre|-LV@FoA|r)dplBhz&o%LIQj@h|QK z{QQ~M1^6$)e;NKO@ZV4WSHZOzJVc92Q(&r1eW!e{e9rWj8%A8HvSqXAiyo2;YQX|N z!_lYjqJ4=(;#29z_m*8OSNzVw=S2JngVzgAN4WUuNHMxWe@}v^!LtAtrtl)|b2G1B zpjEz8-YcKK3jPe<)#E05=ISSYBvJnld*=aPRrT!qN$-dB-g`(wdV_={gaiU4gc=|O zLWj_M?>+P?2na|=0TB=oMT&q5VgoyhUH>YmV8e3WcjY9IoOAI1KJWAHefPeb@cFU# z*?abySu?ZNUNd{`wevv*K>43h;m@7|=Vbu)?y(6UpzbaA;R76wrM|(`!}B~j7|Jxe zN?C7#cfkAL!x~`H>fax*Rr_kI&V39%17Cw%KuxCq6FL5lCcrKPbadKj0DWjxCPhi*Fc&N)YFSC#xPkn~cH;H}M0BT!A+M4* z_>!E&&xoUY4@JETqUL|7{D;axs62pf?M;<;PgvU(Qo7__?|_AcZp+vKrOnpI28XY;6KBe#eD$>yMscdjcE*PXT(WI#9CMQ%_eU_a_#Qf@d-%WMl3^ zOy7ZAQa4D;#TcACIuF=3&&z@N10=e<~(e z&z)%`9|&FCYbn)EATi5@lZ^`KsX)FhJnB*3RB zb!h>QLU^=C+g-4H5BT(kPk(YOmDGMTw~|TRMrOlfDVdOs#OqJswP%s+1}*7lyhk^~ zO3lMi_z|98f%gEt7Pi!NaYxBZJxMNRH~zT|n`|N{^ay#nwfN{t1 zn3uAEHZu=7W+TT;>@uC~z!dTilaOjWl8+((FbeM+foBhg?=YZRc_sLUZtMeFx;DD^ zv*5UmE(N}icCsD++=xxq&}S^C&!}Q9$O7b;i#}%IA2Z0uOrzaSArCSMADMt0<7hLZ z;V_cDVMtTST#!N3U;vq`et2vjT2yb^NH2Jo*WeQeQqOc@=RAlz!2wss`rP)mAj^7e zvK*T%A{R4<{4GyQ;TMC&li<*ly5+#D74>RIop=ufeGG!b0RHbwtt+4|M`~6sk?tn@ zrPSn{bTPXjoy_h^M{_c-HF%F(%1gf7hJv`Y1;am(M+`pPUOB!*!}!M1TBe&cg0DL~ z{bhtnm<%1Z=oN^I6h zF*n_|wl7E<+YhC+-Cb#A&zHGDfgSH(_+f)kaxhWETaBqd+qGc~O^{`-7h?;CBxCJt zWSEts46^o=el`J8VH+;xcCk`smn_}wn@U&vTjb@y* zZZuo+9al@9<8H}yJR>=dHzeEXS849VY&?(!E{AZdi#p}PEdo3@OlQm`%=*XL!LFDiv8Ifn}lp5 z(a8XRhQA)ne{j`E2RAn{RPhgY3|uxvb?%UGq1jq z;l=t3-cu#Td#NOOZ<7S?XC>bIU5WF#1Gpo0_|{zB*vfV*Od4kyzX;$@QuYKa|MF)VJ)keBU5eh=q( z-=>VSQwYuC7~@2llXJ0`^e|^h3GT{CMfDr35+@-{*iBh+Y&$yrWz5fCxzbkQ1?ynUfGtRcsuL>#uZ|Re8!pR zrwe*&i@kF2k+c{aNhB8&7wsmo(LNF#9V}7NkrEjbPhKTM!pOyh#uiC%Y&Qvt9UuX* zW5q9ap7_RY5bxN-;uZTE_+GqXcxzW7ns1B2V+Zx#0AKYubY2JAV+r9$F(Di>l_5um zTbeB`hI8LKDIzI3ZSo5>q6AT#SEW8}Uu-Dn3bl#VcvF zcrYi%J#oFbCLR)(#8<(0;+nvyEuN=F-~c+`2LHA2t?ER3=t6q{BiSE-Jl#0g9-R~v zkbgjyge>w88RQ>$M=2=XjU0ZpT>51Z%o+%#bt;9XOlenf=h)eniaZaBp zj_GSflXgHH(p~}Ih$fYHFUY|-f_;>~8UCv}V~0}0p6-NWU|2U|uCBBPWGUi&3uH-$ zwh6L?wjlqIL;fL~{6n^ zUX7X9m)MDQiIZ5i_YjNr{$kcXTuh7OX+$`BF-~5L@J0Q>cmmi(IQCX1I6X(DKET25 z@IX~tjZNr$CG}naPxUU){dp*ZNLvi#a*pNpBlhKd3^IivlQ;BE$YtHbT1>lZa2glj zjp2d;oogwLs4I@vwIiTnT}Bhg&m)lAL`QR+ioJ`I{REJqnDHFtErG`zcuuLr4nwiS zFxn*O1X_VijwNs|VhB1Ij1Hi+M?Uib7C4O^oroiba|6C44#EJs=uJcF-Hv=m8K98z znRIsR38;=xxogP$1F|ARSDtO5ss)ri12Q!aMZG@LAF;cQK?h^e0Z0YWAQ*UY+>!HE zfWZS7Skal;vKZqoVP67ECwF^MtL6d1{D=GW{a=(VWw~!SXKo#|yGXh_v z%(3toJOPm>p)M{dn2H}v!w*2%baF7j1=s>pjxjJqd0>(y9V_~n;0QcuSOFl0j;~LV&vY9+%F;bzmORjzCB|EGiTv+o5|OlM)pt1(fon@ z+6U{gz`H*r-QcTSIAI(QmDywKht7x_x@xg86fT&|Et{V(LPs-7pLjRag z&VX~^MR2hOmuh}rsQFFRs?V$AmjUiC*G$Ms;Hx)638Jz=85qxRnBIWVjdRi$y_Zt= zcH3x&d+-6oYfL=@;O%;r9E>UtaS8q&ymvzvLqMp@e_UsaX{&>zYi!@C+5d>`XW$#~ zBls0Leh+6(NJ|!2=nZH9_f~=(qMLi6=3l7XgELU?p#*?vkPh-ed(aJ3fI&b-N@Icg zKFbVp1oOy!RFR)pP4u#f9K~*;+oMEv7s(skAdbF6EcZA00o^(rZ?IK)0F{GKc>rC` zLFFA(9z@CF1XM1jc0NjT_6vziJCcj&LAiY>aS&A-PT931MYUJMZ!jsY!ddH{3w9e_J^3xaPn@o*aXj}|&I6eWC7inKo-iF_gQjhv=c0+_@Yzi6Lw%{`659Ek_*s2nNVmex z9JaH7s{0Q>HKX^yRq!IZIE8;6B`wb>y5L0;DQVS>edcrdhei7)d z3A|F!b|!g`7I11!U5en<30~dtir#2q5V?qv@S03!W&o^+ZgJ{8oafz`Qs~O9h5fA#+!igl&4~rP?Wv~=EzcW02Ei|q>_!~CQsC7LUb%2= zMZJpfPTm8-F1^Tr^rY6^(L^aycflh%;+Z9QR&jOfy2Y$7wGy)KO}w|@d)}Md5W}B+ z0r2-D?(}4wjVue0Z-%)gyfxI*6@Bsz4t%sfa`Z)x-pEmo9A(JS9XVKiQo5LQk&dS2 zQerkxip@qzTl48sXuechSv)5B7RMyd@^xup^{wPs{Utfpe5DIq@nIeex3z=e=s%K2 z9&A_edm*ySU??)4Zhp9pH8~fJ^fBf8o94dK-6B{@EhD9~Wt?=hN|6%lW>Re3LfY9B zN*kLF(%QDW_MO%sL{(F}c%Wl|tRFE7Rx3Zbr8+H=WT#D%=yXKlonDtX=kEczhZ>xr?uX&M zGZ`JEqJtD{ki>8cnZ|Oi5*aIexwpEJe{drI;OHy`PUIh){3X{pRI;5}|Is-?nz^J) zhD(m5xfV*QYez|T?J0?FtiRyK`U`IJCB}WDM7tlBC|*j9bpICcmS_#0gO^&5Wm^Wr zvSy6)!5sFh=YSyFfW{2TLm6M-FNK~~(!xz6&D~t3nY*`SxCcp^M}(w!#7dF}>o0g_ zO1x*j#Cf%sSg#%u?L9!Eyzyf1xf1TZUP5^>ImG+21p9mgctW8-9_B;>c7W%W<_!CC zcodh*&@&r9K$fBCr7!2ZVVmMG##n)7(u`b8nzxfA`*=vAkDtW*gh&%#)_?S6{RKbP zUmzC~>CgHL{;ady8*$kod$zkXwlpPjWFH zv8+KE+g@B_dx~>xr8vb-l}544#UXaL*vFg$H^m|5cX1#GqXGNiu^IlW;JdH`eL_ck z0SsloFUPu|6Q(mr9&{P_OcV4LmP-C1iTp#NlXxb2kYn)^x5QBLD>33sF2*S-M;av+ zi9=EkvFC*e+oVZilT;;EOd7XLd|oUQJ`k&fJ7P@^#yXDt19er8VV8FzRO&+bR!Ud~ zhIFM(cE%S<@CEdfgDfe?5{-Q@xp+0R7MBe24;ie1nc*#(j39Byh!VSuM6u1t66>ag zV%4-%ESR)lmN8CDGZvCW*+SrO5-=c6{XtB~!I*;Wl)n!CRq&nNgEm=)FZ3itW!tMe zZ4y}uIG>3uiO@#2Wz3D-?)hfSE3p%s7OX*$>mio8{$i0EE@ruLVw#JxauGO}3v>HW zfzeRD7~JqeYkajrKo z?)Ma|MMiD)+Q3W8X%C32)}&SQP=aWN=yR<BR7^KnN+y-Ei0Ui;;OJGG9dAuaWg{81d5F`_#IfMHZ{4svgy(Y=7&pTq1=5B5PfR?9>>rY& z`3?D*-%P!$-vyeAswk%mI-53H-jZKtSSt-co7b`mjS|6>T;X95Va!O$QhW9x^|-*X-hfTl*=oZ z68Q)|@F@H@GY{x7a!^3Ff$cTe%2q)ghlb&^Zi~9*F$-uLL5m#Q;-PB|DmV_qEfl=z zIc}7nFlhNrM$f`5)Vl|~i&oQIIh{-yQSb@ag%2XK9jQ3?x@Cp0jybMseT(c*D2qZU=LEfSZ7)8#4O238R zZ@erO)V<3Fd;py`-;ED&IQ$4cc#<|idFB@=;}viPyawI?Z`I&B+t=$HQ~cfp*T8$= zBk(!+4*X=#L+xPZgk*y<7Fih2b~#YduPv|xD&|+Y2RESJ0f_|3AQz}epfgZ&LHdCq zM9?FNmd26$m_m+VCb^IK?`&dT=vyIsO0P{0W6GvWSe#R|gslSMBbn9?jWqT34 z0G(`VkxofdU6zx%T1#3kKpzJd+$)nbxKqG&jZC<&A~VXw5aI;)Eo?@6Bn-VqfQa< zOM*uh<+q~j_LQc+4cn94KwmN&gUEXfBdQ-wZ6{))>6l7=A9fk}i4B-=H_`S9V(8a! zk?(OE-TV_Z590~28|cr$P_q1rK7L>h#y9BVGjzdBWcZ{~eh#K@ja$^di&92jrGoMY zP<|!lk3{q1aouUuQN4?@oVa}xns|b`p2adB5y$>c&OkT+V+-32K;3yk77`&RmBc1Ydh!|Uw_b8_P&XnJsXu20owjWJ%Fg!-UV;p&jX~fL)@rqS+ z9ovxP2>f2fLf;WX>)u0I&bA6H1WJZKz-{muco)2iE?$&Ijee-D|6)5PsJh@XGMgLP|w zs<{|y4#r)0au==6zo;gsn)GQgA%|oOUuUlN!e&9#Ckj3Z)FmAr+3?VopIh$Z>~G}QDl=|#VuoIAsizb5^MI!x{6Z{Y<85?&KH zGY?N*kM*8}@{%+&`AnLc{w5iwWTU|)cpeWV2NlX~H4OcSQ2zkN`5d3@%5aF=-XJSW zsW5SXuM2hbq<;Qz3YB7$#?sa#UJ6Z9rIlHx3^CEF%JvTW0&nQe2)uxll0b|sQx-(8aI`%8lT zXo+)}C9w{xCE8)1L^-@Dk(y7yZ-BcJI2F!qKbB!e0-P2$aI_(2A-4w#Y3P>*Ak=%o)jDn(aC zK8*9-$v-%ge{drI;Os7m&b|`w94t*(uO-%n^%q=Nf5A0dBFV*syLFZ@H`ZTpWBmnp z)?aX6A%5<=#Mk|-__*H$zq8LB8XTd{yWqdE88K&bhIe3UCd1=&`~ba_Hz6FrHf^v? zjz8mUaxwAl8i{pxA-Cg6ZpU9DJVGVhgY_RhlO%*(Ot4ph1bVUlf_J(2c@GsIzFp+y zy-Ym3cZj?93*zR@`U~E_0iFlY^Km1m<1InFrxfTLDgJ(O;zusV*FR6Z{fouZzf3#=28&z3BykB? zBF+KZ#mWD9arFOC90Pt~pQkq9N%(Aq|C*Kzd0R0=2Wl-6K4T$$v#~={?0{`r;xA3H zO#-%w46&A=KnDp3bP>NmPw@%z7w@1jaw;+65u76K!8zg@(oS4Lx`|WBKyeJ2AexZH zVjr?q>_Se8ZOA)pe-!&*p4x!DSqyREx3V?E?Y4vj?Pw2dD+@&`@)_qLUt4U`0^6jc zlQ?V_7HuxR5q9Dg?kFDNZsHd1Bd+1WiCm0hM3!hG3&kO_tJp>M7u(3OVjVeO zEF(9GMdUHDhRRSTk9}vPmbgXwpZ_nv4>YCUXef z8MwwiO(Cz7$oLodQ%o9@e?af+;9u2&HrWYZVAwkn3}nBYV=Sv7t=r%W$dZJwL}8x* z{Kq}rOf)HWVw>zJ*2(T-mFz1PeAn7MIaW-Q5i}X%6e^S43yc6WDPSFg=0h0tRqzc# z+#e{Gb);!K=36QgCKxJ)j}EF)+fiZe|It10M7jCvWRB17t*$jNvL{>!@3CVS8( z%kc%o=5s324|MB^FCa@cvZNqObVr^&V=qsNZiMWXt*yvy*wbj7$Zfa-KLn=JYZ(tP zTuYoPkBa3D#-S!-&=oZBeH8dIa(|6{$dIX(weVkzsIz)whrakiKVskh_yYT#`oMzY zO_41gdBTy?k0M>T#tyxicC^4)wlrRJ(H>nCd(nu{MNu?~H0;q1r*4PvZ7HzL1RQG# zhTVhR>8C z*kKrL67(9*C7=~(2I4^&$9;wp|5pZjmMoq6lx4$OA21> zff?r*NRjsdqrpfp!UAVTj$z0#)B|9X$~cgNyfm`v`6yFqaO;ry85rF_-rtdxIT-Ms zLOEmLI|yE7lTg(Z%ruRA!wj18OmZ-@umR8j3m~=NJOezO8E}nEMkkYOKqG)06GK5V zMs7`mD@SgG9=jB|4=^x!3)$}?H*XHWdpx`=aogUM*KrmuGZ#NxKwDfy9;}M~XbJHz zms%}@PhD`F!8?Pl8enb)y>z&iz=3__SQx>6Q=F~?jc+g=;cRY7+i-vj+!(&4Ltvdd z>OGt~_vKWV`M54TGneB7tMGxf_`rJRLTz9!)T5NGunBCgLG7`P{C)<fJ5L8%($v z+jEU4TYO_(LbIg@U+2N1T7j4G}cfo%T zW$p$0!4u%g8tkh%rj9F`S~_)H(H#VbZFyixn?2HmE&h3!2KW?A4{hZa>db4=}@1Zu0!p8>F_esFsg zA9>A(AR>wUMG4!X1aphYYask9FH1Q(?}+XSsCUzy_yF>U9A+L6eC<_PwdK6b{$=ng zc&!Fk^^Uz#=lnJH-vb|mFTr=nam$~eGJ}WPrC>Bz!tWi7?yZ5Ef1zT2m3vTgLA*dH zhzFUV6;Kh0dOF*i$Y205Y9-OqC?e_c#OhPY6U-uRTuAO?1yA=k62V{JIIA`-!1XlQ(#myu=-H2f8&#PP0`xhy%db z91NwCzlf`T<=L|OF6?(a&;5#c`%@yKkI4yrh#c>skGF~9t`TLuNoV;6d63uWuwEq} zqx(MWLL!+}#O_XSs3`-w2?uL(Mm+N)@P7Trk|-Ni;Ec>;bf!tV>ZINjHURPJLgm<|4f=da*jK)LXn z;1arcff(~RasOfB{sY9@dwJ(!CwaPU=wk~Jq zWGiW?gW)?6)Xu~B3V!bZ)%;a=pt>Zw2y!oG=+X|p&hS)oFaqEehVEkElLVg(_+(R` zmei*W^=S{UF0_&|c&Y3|CEhxY)-#h|YNpC|y!ctpzE8{g15hdjg@DE5){y@2*U zB^Pmzt9S!so5cXhZC)3V!ft#QaWCpN^4G`mr0_}N`ikPI1AsW>Be^j zbDIS#g1FuLGBiP!amY86+izbB3re(uuM>6T9T3_|8{}w(9QnwRha9=ckqyTz{FGTC zcuT3In)H!m6Rc)BRpL#TN)yv<5@YtPM4P=Wjm>XMlm+i!fae0ZeTJcfD0I-6ScmOw zeoqW!c*d=6fE&X=hD9B1EScM4FRe_SB+tZMa!h?B%PdHmnXv|?d5ol4Bua_}Yd~1$ zN`h4ziL>e~vDUpLns0l*UHujQX?Ifwz?viZdD~Yy25^ozJO?XEr zhIfRb?Xo1wzCa@EJ4l#APYH1tB0-u-5};xI1^3Yi9SPR#-7gsuv)S&plTq&YJG!NEbAI5b*MBevvVY`w@oz-JTuSF~X0+md`- zKJAh1kX%BOEJCDo!ZhT}!8U2wrU^O;4>BX)Vk@3Ljl|u@Ra||&#KkvIoc$uikz7n8 z|EA*LpD*?S9mO`FLTmy?idDcYu?Scv<^c!A%>S~O`+q4G{`bVvkM$oi@dfxVEx;EF z2@$|3Fo@sfd9(-QYu%K12R}(gC$ZQrl(yp?W+g7c_Tm`iB$^-(aR~M!rxHqzC01-g z(!?q>Pb@>*i+O0dn1&7$lh7GlxSD|?cY=_Y30l4YzY#!^e?a%@+eB4u@r5FMp_n!a z2DBsIL6#Eq(h8k4N0wx4))+qt!cRP6%%oA2EjbqQ50P$S73m`uk-=gX)mTiUaPTM! zj-tTEWo#?KRIn05?&HDjCGa^4{S}aZfX6EMFKkbn>_EKJiEynW{>Xk0j#JHaBt{L-`o{nD5YQEvcBxAX#+*bl8atap^IsS8j-~dy*Qy4>tlcds!nBlM_*tH31sBXoD^a-4GgGv<}CK5&(_76-q3i@PbhkHV>H}L$Rlk z`4i;4hkU$ajLXb{mzo-&=AjH9h#f#z_S^J_gyX#GC6UPF51kt_*`Xs-uH%MHM{a?_ zY-q#|bg0fWBoFc&K_G@oGzBC^F9?=kX{qpF-Y`krf#Vc&%rzM2<3V5QbAHf;=1xV^+^c zp>1?shs?*2_e12w7A35lNSSI)+Uj{IBPkM`K$6!K`0K$PUaOSuTU`SAt_h35= zaJ>vMCC`Cf1|i2ld}E+5az=rs1l2{L0-49dU^&iq5Sgzd^DoHCGrr;UOO&bRp$r&@ zxVUf8B+NAh7e?%;Y2;vl+jMNe)&wxPdXV>kmcc8P09-HQu+3QfV+?YP2>?yVd9;85 z&NLE%N6g3IySN!#M)n_&oA*%QJ(lYSQ&vxSmdwCyX5+GRX^W5tEg&Ddka;+`wAo^K z)CI>GHq*h@U=CZZmD%WIwk<%8*}-&v$r!vX=*tZWCet@i2mEr%S15on{zOFLaPOIz z2)8Yu+>9mo04fV!g%7MD2eg(s253Tv><8dneont{&H zj37Aq8k}T%tmfG9ntes1png9Ko&#sWB@1#P?gTNh0GnJI%mYo7 zegR=$ydXVR(sa>r-WGg-`bNWB%|mg4pY^GlGG1hR0lWlW2Csn2HNP*_Y}K(^8nu5F zyanC^H^G%|;b5x6C=0=0P%NA}%85QqioKs^a<$J~$3#HVFM z27Sm$3?xn+%AAi;M35866HFs+RBLink^5Lf6tk5W_7HP3E)c`tB%-)Sgr!@H^b*_W zfXYGCpMzn-5tR#3b3t5yF9;{kk^rAf%Fib*FXH)pXUgqC)L22;1BlZr$$N|>ULHrT zU<#4L9C%d`zpkr}_TRvOSIJIX;p$f?@gg_@6z{!2WrDP*sXi=`SVIKjL45+@7fl^f zD8D&n7m)iXLdBiPdvvGdUPNyFD1R{J564tv;W0(dBgYvMm#2x$PgQ3h&LGJR-0B{A z16}S#~`p~BW_npoqAG! zAM`qqXmTj^8$+%@y^AuJdM>9aZK1gw#xhsf`>vYrM!2eXP?mzlpjL*T;r|Wz2=KHC zm(#%0jXZ=uS(I>i#8HPd%5P2-pHKO1X?`W>vI`M)-8DfgiEhVGzbWKC=EG|>lI(=v zbMX6+F6~d^SY7U8I$Jdl<9B%80bhXk!4+^8UGVe`>7A&LCv^#=E)mSbh$A16M)_G- zJCD)}$$k|RNp>PX&<*XE6Jhs*PbGZD(q&F3@?VU%ZX{1|2+zHY*W5xv-It$7uvPOg zZo%^tpqi0tdh{C1*r5AB7u$*ZH{qY_iE~%cPFS&z664^J3Xe>zo-4Dk%?xZajYxC~ zwwZ(+qP$pTh4=pt=Cn{Tu?jz-Dx@ zmiDUVU@XKYbMTEB$T0;uCNf87Jo%V0MAM`2krCKt7*TK~wi!$YU?6hzgJU1sOE0b{ zLp$BkU{`9_8EzeEW%cG@!0)07{LM&1vsK+K-DP$6zevofg&n1+IT-Hn^Tqpv$tpyW z_lSi{5_ykw>eL)wE%DAact#0U>4uk7z>DvZqWvOj(3U=<5bd_2<>ce_s>f+jgDci_ zsSc#CHOzSj8|;X8=+YJ-2e%SzXhm)8Da{!Tdr-13wG4(=1e{{wm54W_QLilW9xdV5 zmVX_|n3Z8Az6HwFxmYS2jb>8AX4oTxcA8E;C=D6ZibUs~=;GbDy?79B0W}9?A;;8o z(DCNn>ahu53&eH~=+haV9`N(U6NB-B2x=Zf-4dx=8vL5WFCTtINYoWeRlsW~d?sPJ z#ZYY_`}_ywaWq$9&2h5cX~QBpHFWbiLda_G@oW2KJp>xt(K=InSfob$2ZMyL;yS-VQF^9}fu ztRwjcccC(}Nk;aD0d-!!8%@tJtc@+FS}uZQ^#lMw4139@c00XAL5&$gfV*p3!2+u7o2yH4C~4~U!X%i?PH8Tb{D ze~2Ux1aGw->|$=KGn4Sc1cs_HjBU_MH+0e-TjgW3Omvc@u_WJOCy_Re5^n1%p|)NU zY!@JbcA*krA1!|NN#f&>C0?x6;^EL)+%b(WNtJ1JT26g~rQ?UcbdLvsGbkr`CP%3~i9)1$fIxUe-W9zb7tX=ktmGechcK%dsoPPsa z*+ZQlh5zzQV$S9aGr?%~2Odg&C09k5Lfev%qQ*y_tT%hOc+Ty4a|Swk+zSzKH^ z#M#APoLoc2(KS{yZmDAL)c9L2%I zP3%2=#LkOvV0bkaYwu*S^v)IwpLSyA(?d*r2J?V$GFS%ag}u*#j~OuD0X)Zm&sz8| zZiz3n!WY2s0>Tk~cSn}?$d`|v)O3R6cw|79VEn`@#FRNLHe&0i5oCfFrIUZ z!FC4ir>O`s1lR*N*IA+v6-M5C-|1dgU~HEANc59h{!1q5zesR(!Hh#LfvKO-CY z2l!VN(>FposWTy27yPj+ZGvsrPK^1HB^$jYx26b+48%Sx4J4XoJo`?w#7XQpsiDz2 zqbNGHM4Tlt3ShiMh!ZIwp*I*sqnbzXvW3bpV2?qD=wB$5F%|q5!fQq;_lNF;gFWb< z%XHHPI&!Q4xtgMzIEo6xPQJ+Of?jNpM{>;((1ySSU1Xw*rs$%nAHW_><3Sb;nu{`e zqR8O{53^}pk0SF)Wd0EOenmFk0fEO{cu$2)%|jVhL7M>G*)QT)E;6NbLtJzd1g!`1 zXpqMoJ27dHhKnv*+2XhkI1akVLl=2LbST)P1%_*Z0&^)Kw-R}0kkEJpnV%t``vBRH zA)PTLJZ8afQXl$-{`lhne1UD}eiYpoQsjw8m0{)h0s3)<-Wr)1GSPv7PNp96yMDVF)?^-9Zt^ zrCFzd#+(bF2sh~LkLcM%96k#i8 zBKH>Lei6C9LvGf)JzRaw$X82jCpTb~-t*3@p{dGINe2$MQh7*koxj12v3SMJ1|saf7%@ z$MBO3MfTqC?u3~NC?kV1W7g6RH;@C`L=FnI5vVc90FQ$mHQSwRx7ERB_7$zdV>QRO zv)y6JAjt`Yf-KODW9Z^>Dzp6rWqd4ssCO6Y+$sn8jM{DK=ln|emlKUtpC()NU zRjtV}j8Xb%V#bMN7^ah-m`Cno899Ru#ISpa+nvC4A3_`WqspFOlPOVuw$OVQxwVarywF=}P8&j3QSsf&7Ge zx<8-%#0u1~nJD)lD!IhJFHx6n4$2X>inq#tJO=9XX9-y`BLcD~r{hX}{K&IJP=|Qx zkU`lkD7%od+Y_I6A@9+HIJp<*=|}m4G0g~a1-0*@EFgYeP1Q176qJVad?enBVn8r0F6Jb)vyxCi_K;S+_~6DhwbE|yDPK)s7ngxhr@iY=x5 zp5!I^kS7>KL^&J_j3;iMPBU6WlUk29_7UB@NY3C3;%MFck0opufVn`)@HhAod=8ZR zT*XH&po`PQGAD@pj}XTkBqy{VIrbpOPRXHm1(e^GrdUFqYS#ph8 zhbUS^6TC4AUK#Mprp4xyXK71rpc5^p3|{@=rB*DRhSiqQsvbw$7qH?d#La)AMJ@3< zIZG?j%XZY?fo|R&{x;lB%yhTINysr8{~SWx-;a2^7wxqNa&$$G&dAY$Xr5ITXdy{( zPRBE{;a5hi8Su;D|JHa*2l$n7 zbRc}jqOrN;A=YEXLul|-dYx~9Ze^t7bm9Aauz?@3jz6)EFR_jXw{_&I)G!o6Po-jl zEv?b3J=%7Jrz`wC;pYcG*1<%M6y!+aA1h+uDS7;AhxeAkuP?ktk@uL1#H-1w?Wf1N z#4MvP(B?f#ApZc*ok8dz6di=2gAj&Efs8GYYXI_t3Fpa6j}P9mr^f=pXj6|I%#HWPX5D&xh)Rp)rmT~QAaQ87$6}gp%P@m z8W5(e0b$lme9iO4+oD7~Ey~5+a;UgkP8Ao+<>F+yQyeW{5RE0@z_j`S{4M+7w>gI4 zP&{@>VEDyWJq8<$EWOZ6XLQmQo#f&x>XCB1g9Z5yTM4ytkRS^u39xh*KT99+u?iF~ zt8nqOZX)hBY2s?rLY!@j#mTlzGoMcHvW?8$#X7OVhu;ue{irBFFTF6+c}Gyork#C`-(H` zwKzI77L7v^d6wp4r)euTj@`uCagbO#P84%T-a2#KE~bvpiAkgPz%4P={4I~eYdw6I zG!@-Elx7TtGZhCiA3OL73?Omq^0&-%EKe{i%Vm!lB}M^~|D?Oa=@ zK(TR(AkUH@md=@C?$TP!n4E9oGJt{cc(4d;1*aIOzYA`GzhoM zsE9`a=*+{xzHG;W1z-yU)sp}j+-`%vWizBJ;klp%zR;33na?+EV1A@9#xm5msx`jQPB#x_B$o^< zCf+EbO(0KO&gG()RCE=CuLM&)Uc(Z{IG(*nTM`J_lGC804@Vc_9somy)7Zij7$Bp_ z2nr7G35J1LIP?Y*DaSAVw!Bu32PK&Pf*~fYc#PwY(oJ)5es1~7put=F_k#<42-!RxsM_@GDO@# zF4i@J$Gnb&V_ootZuleQ{XkFlOE}h&b9{nXVyR9D#qkQ7@JgOo;k%M$inF4N40Mr( zE>h7&iYLGxDKzX91Wo3GERoRpqbpbJWQRN^*om7Yxeqe}L@Nx2Uu0{zz!mr*Uj#@3C@+(W zWcEkiNxDET{SfkALpEecVq8g`r^9 z2n?DEaP0OF7mq~Vg*5nG$a@7@e?mUyV8CPIVD1q^X_Le715h#?aX}i027w%N2lmjI z;#v%9bOQ@yWT-@k29U2>wqBOVXHP-cq$h1eEq+~=i>#di7j~P4%#YFVFCyQ!Arr6x&;WCOGdwiRdvJZd+{_RQ0Q*6XW+M9&u+0SInBWPbki9vwcLfMMW(f`8AOp4c$=UoVW8giA>&xI< zJPUW3i@PqM9W2HNs_=oOHSggpr(A^#&inx~}$GHDUU(0Dw)l(4aP9_(x&%=+oc>={*nEfOq~%`Xcy7Q*Ho!TzAk0 z_E63~@C0}g90X5+Lx66k`q%-s3W{!j4eHV!<=9DuMgZS9nFDB4C#JAP=o8ON7sM%C zOEX30_-%CMySXP)hAkr2n^SU{-)F!%a2A{g&x6|I>ezF2eyj5@fy>}c@E&;InV_yQ zA9*O`_aJ}*-rdfL`hO;7<`$OLHnw&Snnq5}F0Sq#p58vbegT0&!J%Q{kx`AKW1Gaq zCnhDQrln^z%gkz?liQ+YenG3kHf@WF+n03g)VWJ(x9(*<%X?My>DzC>z(GSQhYlMt za@6Rt%GRlpYHzr%dfxR`{U2Qwg0<+^=nW6-%$VmS@t(ERTj5WE^m*| zJ1VDl!|T12+xsiO55e&xl;_7N*H6IrQJRoS<)dafI270JQ z&|9^F{xpNZsvQhh4Pgu|VS;K3Q)vq`Rb!Z^TEk+R!!p$#R;vc_2rXihY7*O2o7hF8 z*r!^>L7K(Ws$Cpc4dXdl#u?Q#E~vKg3XS8cY8`K>=J77=<3rUzKB0wtp_<6QR2#WX zBe|nm$#1He{PnkX_`q=qstuT#1mXO8t-;vu{~&$+AFJy>SzlseVrpt;W{%HTT3K0J z+u%2L_Vy0AP9sN0C%nhS75{Pfz=OQJ@gbZDFT#!RBOFP2Qnf4Td`Zuls(lGpYFDJX zQhL%JJ$qI3?LTmEcNSvUU58U38bKw|x2tPEqX> zdM=^!iE5`%UV&TS7dQr{Oh&P{!#K;h2bVnd-ak;>-Z#9(u&4fk;OfFc z>AwB_$UD-CZbk9T2|8{WO=9M$&z3@;1?d?0azxd$8+YcvbIMbkk1`YflYTzH= zVEg*&vj-pD^YEtk#f~{cy5?5wzV|TZPY=%uZN%s2u04d{z4GSWvY6p*1)$F^7c&OoqSm z5Q^94=4({*&fj62;ID1n0@ZnmT6D)K#cLCI>014{P5RO(!FKhTqUyil9~&hY8CY{( z`rj~0(69Eqlo}^k7eF1W{|Y`dTEbhUIdML`IPV{u^npHLp1@z6*>Ioa^RNOJG0QSf1om zjR1;j*mp*X_?owwYyWzGVT3^2q4qS(m>3J2LDsbt!Nzkj z7s@-eZWRf7x_$mRBSrkYG^_T!MDH>}aJa1s z9&~>`Ym5+#s2x2(<1pU{L7leCYB@#-)XFUP|Azf#Nb#>a9fGVU6v;;Wpb-xU;*GZd*P`!JBz|Q?NUe5TN^C@choc=ULOrC1c;kiR z2OnR*X8Fuv|KKx5%WgRM|EC7Nf8+SJr4u$iq;g+AHNS7m#x%j+MsJJSqZ4yobzT`~ zG{JbV=lvU}#S9}n&h)6$yn7lM=v3AeU(F=f$elIzexqO~`h+2m^11|`dkqOz)g`cg z)Q~_evGm|?i6OzyaSu+jdY2)AmW8n%{Eaa}u<^lZ))X^B@NrF;RZZ8^2!VEHog%s! zBeu4{SDi_oo_Hch4WvJ}tl|^^nmz9iP^w_U3BTU)Ho!$w%E^t2YgcSXlPpdhEHO**CR;%*nr2i`yq~^413Lh zK-;oz5$>-M>W!EKbxtZ7hiSu1P(L!3HI0 zE9wz+HzZ)~$vS^Uh6JzHUhm_E8_&-epf8V_D#Hz^^ zos9;?f1O=j*}5?a3RXLxOke^`*wrL66t%V%8d#z~~|P0TUxExMV}z zeTs*~y|0y<>qtzt8DpW2oh$7#apeLD`rohI@O@k$}?Fjngk7^F!rI@PBbr_bWo2jli?$^rTW+OhRZ=xRW)%c>qlp#j0W&FT>}Hz3fC zuSbw#NN}v)7$we-;LnLX<-PwGV@ROAST~p3*a(64a2=aQ7$eZ0QH$j3{``y*Xn(DW zWWXAc-DCPe;{>Eh&VBOJ!_GAv|4-GxU-y3dS(^rVc49X_eD7B|bVbG^Cfl zSljCiKa@iRf2>FG;ORzAn)iCu;_lhKZ8i?N_@TZ@|=DN+J5y3^i`?VZmvH@ zG0Z@G*tK2}j~TGgK3317Mg|bKrqoSIuP`h@dt-DR3WL5_UGV(^x(D@V;+Jih)T%D> zRFASz^EVxN{jL!P4Tt{wXyBI{S56;!V%PR9k8W7EZtaGD=}CTgTKDW|KR(!9`={T! zGiy)OZqxNjc&7G5?bccU0cD*5v5w{yy&~!b_KHORru9^9*|pnsdL?|7SbL^sYY-<5 z)FT+JSAzBtJ%Yaa1Q(<0Q*_ZM_@%5qL2G>i?Xm}YR-{=51Q&Da6_I2>pq+I;`=BVJ z3<>HDFvQ=GK)d6?GjX128zInctwYhs7=iX=R}(w`g#5B`YfgS?lt6pyl0$;qT_jgOLh`aiM)F*lb z+C?Vy3Xp611kcuH7HjA(=@a}kynYF%^a-?2q}8L)?`pN$+tcesghs|FuMcvlQ%02m z3$6C`Vbwzr6StVc%9RFVHFd#vRb^urZhGpHfewt6)^OtL(Hj>}pLqJfzFj-E zZ`-lwm|ktSPwUq{C%JKux2uCyb$IQlN044O=&tt6+x00n)NxncuHLiWuWB9wJ)k>T zzXWZ)X&j1dhaQ1J|FBYz;F8|>L1yU@=#L*{j2?kjeT}#7Uw?gq6ZP{+Vvr{JMlXgj z9HVT~y>3&>Mt``c)xKNj5eZbG`Z8#>`|@kAR$GlBf%cJ%S_+qhjuVX#Xt(D2#lg8Uy@wB~1pOKB`Z*5K&rojAi^ODmeHPja_urOL<@2{%kKiZ$JnI}i0__I9 zbdSLx=B3WMqHF&*cw+r`RcP(G>aF2qkrylK^v5nK!}Mj)YWEaYU+NIjyyx_77k@M) z&~7g*=ss-rBZn^Qr>pMMzW0uviSD0i*lEzf|78vQ{MyNVTh=U{KWpm5v7<+h8Z&9m zmHHeGm$yo59N^K&`hJc;ui<>DA76<6;C@)WX2lQvG0F}PJ&M5kGHBmyqeq}0iv2xN zkHBCEvZL+{3Pr5PLVHy|O`=cmcU4GzCi(>0({+omkLzSW@cWvY0K_4(VDQqXjf5B< z&58<0Ea)+6@$Q!%(m}kxdE3#KKh}TVZosPHK!XPUlQeMWtB>A!RGVR~<2OR?(jtJ8{YO6L0)Oj^fVGhUGRKY0$ubSq*&n@~Ol7cW&LZ zVcptw8y?$xRDTY|rxW{jX`9!K$tvN&0e=2Lk;(cK90%CezjezQm(C5omYRpG{(xuTSvmK${0egceWI&-H5mSXVQCunfr?yyE0%`UPmU zmxg(XLr`k_p;ZUoGV;`n3!dy*K6=sClkfa)c(Lxt-!H%N*}aFIY&hPaf&Z2o`2DBv zzW(CVPd@qV>mQ7?i|bErUR5=3=9Ebj#*d#gvueYW=k+Riwo7hmlZZeccSn0`3v)~R z|1}A1GzCwPHiz7E34LT(LX@m%LeEUjLTc7`nj%E5rPft0!+bk9Bktq~h)RHOC)}77*A&Z&Y-I zhQPPS*DstpcKG1IL%=@JgZKY1f?;b%b#ZP+YI0&?a(Z56=e%v`D%QJb%1a3GvO@=2 zSmn^0IhT{+V=%yCK-{|u-GrY#h<(92(Ougy7ddQ&E(Dcx%$T$nqx%6g?_Q%01Fwo* zRMotr=<7(JwyleDkFS`#=?c#F!;7Uew_n1c8EOWW5*Xbtqx{hj7!3iG5V*2`+s1Wk zRff9M!TO%jo3){~xE=GKlDTXZLNzoL_~wXHSZAV(py0m}L*lxvv*b*l-SS`S5jg z_7oOaqx5J9P%#94eEIb8)8}tETkrc8&7CuM!QvI`cU;0gNH{;r*EcXUzPNwu1MJS) z5W=A3lGe8RJeJM}SjYJimyGW9S1?O2n0a4U_HMd@OE*~2r@Po42Qy)m9@HTK@52rd 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 + + + +