├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── AbstractContext.php ├── Engine │ ├── DelegatingEngine.php │ ├── EngineInterface.php │ └── PhpGd │ │ ├── Extension │ │ ├── Core │ │ │ ├── CoreExtension.php │ │ │ ├── EventListener │ │ │ │ ├── BackgroundLayerListener.php │ │ │ │ ├── ImageAwareLayerListener.php │ │ │ │ ├── ImageFactoryListener.php │ │ │ │ ├── ImageMetadataListener.php │ │ │ │ ├── MemoryRequirementListener.php │ │ │ │ ├── SystemRequirementListener.php │ │ │ │ └── TextLayerListener.php │ │ │ ├── ImageFactory.php │ │ │ └── ImageInfo.php │ │ ├── DelegatingExtension.php │ │ ├── ExtensionInterface.php │ │ ├── Gif │ │ │ ├── EventListener │ │ │ │ ├── GifExtractorListener.php │ │ │ │ ├── ImageFactoryListener.php │ │ │ │ └── MemoryRequirementListener.php │ │ │ ├── GifBuilder.php │ │ │ ├── GifBuilderPlus.php │ │ │ ├── GifExtension.php │ │ │ ├── GifExtracted.php │ │ │ ├── GifExtractor.php │ │ │ ├── GifOptimizer.php │ │ │ └── ImageFactory.php │ │ └── Save │ │ │ ├── EventListener │ │ │ └── ImageFactoryListener.php │ │ │ ├── ImageFactory.php │ │ │ └── SaveExtension.php │ │ ├── Helper │ │ └── ResourceHelper.php │ │ ├── PhpGdContext.php │ │ ├── PhpGdEngine.php │ │ ├── PhpGdEvent.php │ │ ├── PhpGdEvents.php │ │ └── README.md ├── Exception │ ├── BadMethodCallException.php │ ├── FileParseException.php │ ├── InvalidArgumentException.php │ ├── InvalidFontException.php │ ├── InvalidImageException.php │ ├── RuntimeException.php │ └── TranslatedException.php ├── Image.php ├── ImageBuilder.php ├── Layer │ ├── AbstractLayer.php │ ├── BackgroundLayer.php │ ├── BackgroundLayerInterface.php │ ├── ImageAwareLayerInterface.php │ ├── ImageLayer.php │ ├── ImageLayerInterface.php │ ├── LayerInterface.php │ ├── ParameterBag.php │ ├── ParameterBagInterface.php │ ├── RegularLayerInterface.php │ ├── TextLayer.php │ └── TextLayerInterface.php ├── LayerPass │ ├── AbstractLayerPass.php │ ├── BackgroundLayerPass.php │ ├── DelegatingLayerPass.php │ ├── ImageAwareLayerPass.php │ ├── LayerPassInterface.php │ ├── RegularLayerPass.php │ └── TextLayerPass.php ├── OptionPass │ ├── CacheDirOptionPass.php │ ├── DebugOptionPass.php │ ├── DelegatingOptionPass.php │ ├── EngineOptionPass.php │ ├── GifAnimationOptionPass.php │ ├── JpegQualityOptionPass.php │ ├── LocaleOptionPass.php │ ├── MemoryLimitOptionPass.php │ ├── OptionPassInterface.php │ ├── OutputFormatOptionPass.php │ └── PngCompressionOptionPass.php └── Resources │ └── translations │ ├── imagecraft.en.xlf │ ├── imagecraft.zh_CN.xlf │ └── imagecraft.zh_TW.xlf └── tests ├── AbstractContextTest.php ├── Engine ├── DelegatingEngineTest.php ├── Fixtures │ └── FooEngine.php └── PhpGd │ ├── Extension │ ├── Core │ │ ├── CoreExtensionTest.php │ │ ├── EventListener │ │ │ ├── BackgroundLayerListenerTest.php │ │ │ ├── ImageAwareLayerListenerTest.php │ │ │ ├── ImageFactoryListenerTest.php │ │ │ ├── ImageMetadataListenerTest.php │ │ │ ├── MemoryRequirementListenerTest.php │ │ │ ├── SystemRequirementListenerTest.php │ │ │ └── TextLayerListenerTest.php │ │ ├── ImageFactoryTest.php │ │ ├── ImageInfoTest.php │ │ └── TestOutput │ │ │ └── .gitignore │ ├── Gif │ │ ├── EventListener │ │ │ ├── GifExtractorListenerTest.php │ │ │ ├── ImageFactoryListenerTest.php │ │ │ └── MemoryRequirementListenerTest.php │ │ ├── GifBuilderPlusTest.php │ │ ├── GifBuilderTest.php │ │ ├── GifExtensionTest.php │ │ ├── GifExtractedTest.php │ │ ├── GifExtractorTest.php │ │ ├── GifOptimizerTest.php │ │ ├── ImageFactoryTest.php │ │ └── TestOutput │ │ │ └── .gitignore │ └── Save │ │ ├── EventListener │ │ └── ImageFactoryListenerTest.php │ │ ├── ImageFactoryTest.php │ │ ├── SaveExtensionTest.php │ │ └── TestOutput │ │ └── .gitignore │ ├── Helper │ ├── ResourceHelperTest.php │ └── TestOutput │ │ └── .gitignore │ └── PhpGdContextTest.php ├── Fixtures ├── gif_87a_palette_250x297.gif ├── gif_89a_palette_alpha_206x205.gif ├── gif_89a_palette_alpha_animated_339x473.gif ├── gif_89a_palette_animated_339x473.txt ├── gif_89a_palette_animated_375x225.gif ├── jpeg_exif_truecolor_480x360.jpg ├── jpeg_jfjf_grayscale_480x361.jpg ├── jpeg_jfjf_sos_truecolor_1920x1200.jpg ├── jpeg_jfjf_truecolor_1920x758.jpg ├── pfa_truecolor_alpha.pfa ├── png_palette_alpha_3000x1174.png ├── png_truecolor_alpha_300x395.png ├── webp_vp8_lossy_truecolor_550x368.webp ├── webp_vp8l_lossless_truecolor_alpha_800x600.webp ├── webp_vp8x_lossy_truecolor_alpha_421x163.webp ├── zz_gif_89a_palette_animated_corrupted_data_153x120.gif └── zz_gif_89a_palette_animated_no_graphic_control_550x296.gif ├── ImageBuilderTest.php ├── LayerPass ├── AbstractLayerPassTest.php ├── BackgroundLayerPassTest.php ├── ImageAwareLayerPassTest.php ├── RegularLayerPassTest.php └── TextLayerPassTest.php ├── OptionPass ├── CacheDirOptionPassTest.php ├── DebugOptionPassTest.php ├── DelegatingOptionPassTest.php ├── EngineOptionPassTest.php ├── GifAnimationOptionPassTest.php ├── JpegQualityOptionPassTest.php ├── LocaleOptionPassTest.php ├── MemoryLimitOptionPassTest.php ├── OutputFormatOptionPassTest.php └── PngCompressionOptionPassTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | composer.lock 3 | composer.phar 4 | /vendor/ 5 | /reports/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | before_script: 9 | - composer install 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.3 4 | Fixed JFIF header parsing bug 5 | 6 | ## 1.0.0 7 | 8 | * Initial release 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Xianghan Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coldume/imagecraft", 3 | "type": "library", 4 | "keywords": ["gd", "image", "animation", "resize", "thumbnail", "watermark"], 5 | "description": "A reliable and extensible PHP image manipulation library", 6 | "license": "MIT", 7 | "homepage": "https://github.com/coldume/imagecraft", 8 | "authors": [ 9 | { 10 | "name": "Xianghan Wang", 11 | "email": "coldume@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.4.0", 16 | "coldume/imc-stream": "~1.0", 17 | "coldume/translated-exception": "~1.0", 18 | "symfony/event-dispatcher": "~2.4" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "~4.3" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Imagecraft\\": "src/" 26 | } 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-develop": "1.1.x-dev" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/AbstractContext.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | abstract class AbstractContext 10 | { 11 | /** 12 | * @return bool 13 | * @api 14 | */ 15 | abstract public function isEngineSupported(); 16 | 17 | /** 18 | * @return string 19 | * @api 20 | */ 21 | abstract public function getSupportedImageFormatsToString(); 22 | 23 | /** 24 | * @return string 25 | * @api 26 | */ 27 | abstract public function getSupportedFontFormatsToString(); 28 | 29 | /** 30 | * @param int $modifier 31 | * @return int 32 | */ 33 | public function getMemoryLimit($modifier = 0) 34 | { 35 | $str = trim(ini_get('memory_limit')); 36 | if(!preg_match('/(?i)\\A(?\d+).*(?g|m|k).*\\Z/', $str, $matches)) { 37 | return 1024 * 1024 * 1024; 38 | }; 39 | $limit = $matches['limit']; 40 | $unit = strtolower($matches['unit']); 41 | switch($unit) { 42 | case 'g': 43 | $limit *= 1024; 44 | case 'm': 45 | $limit *= 1024; 46 | case 'k': 47 | $limit *= 1024; 48 | } 49 | 50 | $modifier *= 1024 * 1024; 51 | if (0 < $modifier) { 52 | $limit = ($limit < $modifier) ? $limit : $modifier; 53 | } else { 54 | $sum = $limit + $modifier; 55 | $limit = (0 >= $sum) ? $limit : $sum; 56 | } 57 | 58 | return $limit; 59 | } 60 | 61 | /** 62 | * @return bool 63 | */ 64 | public function isFileinfoExtensionEnabled() 65 | { 66 | return extension_loaded('fileinfo'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Engine/DelegatingEngine.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class DelegatingEngine implements EngineInterface 12 | { 13 | /** 14 | * @return string[] 15 | */ 16 | protected function getRegisteredEngines() 17 | { 18 | return [ 19 | 'php_gd' => new PhpGdEngine(), 20 | ]; 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | public function getImage(array $layers, array $options) 27 | { 28 | $engines = $this->getRegisteredEngines(); 29 | if (!array_key_exists($options['engine'], $engines)) { 30 | $engine = $engines['php_gd']; 31 | } else { 32 | $engine = $engines[$options['engine']]; 33 | } 34 | 35 | return $engine->getImage($layers, $options); 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function getContext(array $options) 42 | { 43 | $engines = $this->getRegisteredEngines(); 44 | if (!array_key_exists($options['engine'], $engines)) { 45 | $engine = $engines['php_gd']; 46 | } else { 47 | $engine = $engines[$options['engine']]; 48 | } 49 | 50 | return $engine->getContext($options); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Engine/EngineInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface EngineInterface 10 | { 11 | /** 12 | * @param \Imagecraft\Layer\LayerInterface[] $layers 13 | * @param mixed[] $options 14 | * @return \Imagecraft\Image 15 | */ 16 | public function getImage(array $layers, array $options); 17 | 18 | /** 19 | * @param mixed[] $options 20 | * @return \Imagecraft\AbstractContext 21 | */ 22 | public function getContext(array $options); 23 | } 24 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/CoreExtension.php: -------------------------------------------------------------------------------- 1 | 19 | * @since 1.0.0 20 | */ 21 | class CoreExtension implements ExtensionInterface 22 | { 23 | /** 24 | * @param EventDispatcherInterface $dispatcher 25 | */ 26 | public function boot(EventDispatcherInterface $dispatcher) 27 | { 28 | $context = new PhpGdContext(); 29 | $rh = new ResourceHelper(); 30 | $info = new ImageInfo($context); 31 | $factory = new ImageFactory($rh); 32 | 33 | $dispatcher->addSubscriber(new SystemRequirementListener($context)); 34 | $dispatcher->addSubscriber(new ImageAwareLayerListener($info, $rh)); 35 | $dispatcher->addSubscriber(new BackgroundLayerListener()); 36 | $dispatcher->addSubscriber(new TextLayerListener($context)); 37 | $dispatcher->addSubscriber(new MemoryRequirementListener($context)); 38 | $dispatcher->addSubscriber(new ImageFactoryListener($factory)); 39 | $dispatcher->addSubscriber(new ImageMetadataListener($context)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/BackgroundLayerListener.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 1.0.0 12 | */ 13 | class BackgroundLayerListener implements EventSubscriberInterface 14 | { 15 | /** 16 | * @inheritDoc 17 | */ 18 | public static function getSubscribedEvents() 19 | { 20 | return [ 21 | PhpGdEvents::PRE_IMAGE => ['initFinalFormat', 859], 22 | ]; 23 | } 24 | 25 | /** 26 | * @param PhpGdEvent 27 | */ 28 | public function initFinalFormat(PhpGdEvent $event) 29 | { 30 | $layer = $event->getLayers()[0]; 31 | $options = $event->getOptions(); 32 | if ('default' === $options['output_format']) { 33 | $format = $layer->get('image.format'); 34 | } else { 35 | $format = $options['output_format']; 36 | } 37 | 38 | $layer->set('final.format', $format); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/ImageAwareLayerListener.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 1.0.0 16 | */ 17 | class ImageAwareLayerListener implements EventSubscriberInterface 18 | { 19 | /** 20 | * @var ImageInfo 21 | */ 22 | protected $info; 23 | 24 | /** 25 | * @var ResourceHelper 26 | */ 27 | protected $rh; 28 | 29 | /** 30 | * @param ImageInfo 31 | */ 32 | public function __construct(ImageInfo $info, ResourceHelper $rh) 33 | { 34 | $this->info = $info; 35 | $this->rh = $rh; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public static function getSubscribedEvents() 42 | { 43 | return [ 44 | PhpGdEvents::PRE_IMAGE => [ 45 | ['initImcUri', 909], 46 | ['initFilePointer', 899], 47 | ['initImageInfo', 889], 48 | ['initFinalDimensions', 879], 49 | ['termFilePointer', 99], 50 | ], 51 | PhpGdEvents::FINISH_IMAGE => [ 52 | ['termFilePointer', 99], 53 | ['termImcUri', 89], 54 | ] 55 | ]; 56 | } 57 | 58 | /** 59 | * @param PhpGdEvent 60 | */ 61 | public function initImcUri(PhpGdEvent $event) 62 | { 63 | ImcStream::register(); 64 | $layers = $event->getLayers(); 65 | foreach ($layers as $key => $layer) { 66 | if (!($layer instanceof ImageAwareLayerInterface)) { 67 | continue; 68 | } 69 | $arr = false; 70 | if ($layer->has('image.http.url')) { 71 | $arr = [ 72 | 'uri' => $layer->get('image.http.url'), 73 | 'data_limit' => $layer->get('image.http.data_limit'), 74 | 'timeout' => $layer->get('image.http.timeout'), 75 | 'seek' => true, 76 | 'global' => true, 77 | ]; 78 | } elseif ($layer->has('image.filename')) { 79 | $arr = [ 80 | 'uri' => $layer->get('image.filename'), 81 | 'seek' => true 82 | ]; 83 | } 84 | if ($arr) { 85 | $uri = 'imc://'.serialize($arr); 86 | $layer->set('image.imc_uri', $uri); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * @param PhpGdEvent 93 | */ 94 | public function initFilePointer(PhpGdEvent $event) 95 | { 96 | $layers = $event->getLayers(); 97 | foreach ($layers as $key => $layer) { 98 | if (!($layer instanceof ImageAwareLayerInterface)) { 99 | continue; 100 | } 101 | if ($layer->has('image.imc_uri')) { 102 | $fp = fopen($layer->get('image.imc_uri'), 'rb'); 103 | } elseif ($layer->has('image.contents')) { 104 | $fp = fopen('php://temp', 'rb+'); 105 | fwrite($fp, $layer->get('image.contents')); 106 | } 107 | $layer->set('image.fp', $fp); 108 | } 109 | } 110 | 111 | /** 112 | * @param PhpGdEvent 113 | */ 114 | public function initImageInfo(PhpGdEvent $event) 115 | { 116 | $layers = $event->getLayers(); 117 | foreach ($layers as $layer) { 118 | if (!($layer instanceof ImageAwareLayerInterface)) { 119 | continue; 120 | } 121 | $info = $this->info->resolveFromFilePointer($layer->get('image.fp')); 122 | $layer->add([ 123 | 'image.width' => $info['width'], 124 | 'image.height' => $info['height'], 125 | 'image.format' => $info['format'], 126 | ]); 127 | } 128 | } 129 | 130 | /** 131 | * @param PhpGdEvent 132 | */ 133 | public function initFinalDimensions(PhpGdEvent $event) 134 | { 135 | $layers = $event->getLayers(); 136 | foreach ($layers as $layer) { 137 | if (!($layer instanceof ImageAwareLayerInterface)) { 138 | continue; 139 | } 140 | $width = $layer->get('image.width'); 141 | $height = $layer->get('image.height'); 142 | if ($layer->has('image.resize.width')) { 143 | $args = $this->rh->getResizeArguments( 144 | $width, 145 | $height, 146 | $layer->get('image.resize.width'), 147 | $layer->get('image.resize.height'), 148 | $layer->get('image.resize.option') 149 | ); 150 | if ($args) { 151 | $width = $args['dst_w']; 152 | $height = $args['dst_h']; 153 | } 154 | } 155 | $layer->add(['final.width' => $width, 'final.height' => $height]); 156 | } 157 | } 158 | 159 | /** 160 | * @param PhpGdEvent 161 | */ 162 | public function termFilePointer(PhpGdEvent $event) 163 | { 164 | $layers = $event->getLayers(); 165 | foreach ($layers as $layer) { 166 | if (!($layer instanceof ImageAwareLayerInterface)) { 167 | continue; 168 | } 169 | if ($layer->has('image.fp')) { 170 | fclose($layer->get('image.fp')); 171 | $layer->remove('image.fp'); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * @param PhpGdEvent 178 | */ 179 | public function termImcUri(PhpGdEvent $event) 180 | { 181 | $layers = $event->getLayers(); 182 | foreach ($layers as $key => $layer) { 183 | if (!($layer instanceof ImageAwareLayerInterface)) { 184 | continue; 185 | } 186 | if ($layer->has('image.imc_uri')) { 187 | ImcStream::fclose($layer->get('image.imc_uri')); 188 | $layer->remove('image.imc_uri'); 189 | } 190 | } 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/ImageFactoryListener.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class ImageFactoryListener implements EventSubscriberInterface 15 | { 16 | /** 17 | * @var ImageFactory 18 | */ 19 | protected $factory; 20 | 21 | /** 22 | * @param ImageFactory $factory 23 | */ 24 | public function __construct(ImageFactory $factory) 25 | { 26 | $this->factory = $factory; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public static function getSubscribedEvents() 33 | { 34 | return [ 35 | PhpGdEvents::IMAGE => ['createImage', 99], 36 | ]; 37 | } 38 | 39 | /** 40 | * @param PhpGdEvent $event 41 | */ 42 | public function createImage(PhpGdEvent $event) 43 | { 44 | $image = $this->factory->createImage($event->getLayers(), $event->getOptions()); 45 | $event->setImage($image); 46 | $event->stopPropagation(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/ImageMetadataListener.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class ImageMetadataListener implements EventSubscriberInterface 15 | { 16 | /** 17 | * @var PhpGdContext 18 | */ 19 | protected $context; 20 | 21 | /** 22 | * @var PhpGdContext $context 23 | */ 24 | public function __construct(PhpGdContext $context) 25 | { 26 | $this->context = $context; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public static function getSubscribedEvents() 33 | { 34 | return [ 35 | PhpGdEvents::FINISH_IMAGE => ['addImageMetadatas', 899], 36 | ]; 37 | } 38 | 39 | /** 40 | * @param PhpGdEvent $event 41 | */ 42 | public function addImageMetadatas(PhpGdEvent $event) 43 | { 44 | $image = $event->getImage(); 45 | $layers = $event->getLayers(); 46 | $format = $layers[0]->get('final.format'); 47 | 48 | $image->setMime($this->context->getImageMime($format)); 49 | $image->setExtension($this->context->getImageExtension($format)); 50 | $image->setWidth($layers[0]->get('final.width')); 51 | $image->setHeight($layers[0]->get('final.height')); 52 | $image->addExtras([ 53 | 'original_width' => $layers[0]->get('image.width'), 54 | 'original_height' => $layers[0]->get('image.height'), 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/MemoryRequirementListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class MemoryRequirementListener implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var PhpGdContext 20 | */ 21 | protected $context; 22 | 23 | /** 24 | * @var mixed[] 25 | */ 26 | protected $extras = []; 27 | 28 | /** 29 | * @param PhpGdContext $context 30 | */ 31 | public function __construct(PhpGdContext $context) 32 | { 33 | $this->context = $context; 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | public static function getSubscribedEvents() 40 | { 41 | return [ 42 | PhpGdEvents::PRE_IMAGE => ['verifyMemoryLimit', 869], 43 | PhpGdEvents::FINISH_IMAGE => ['addImageExtras', 889], 44 | ]; 45 | } 46 | 47 | /** 48 | * @param PhpGdEvent $event 49 | * @throws RuntimeException 50 | */ 51 | public function verifyMemoryLimit(PhpGdEvent $event) 52 | { 53 | $options = $event->getOptions(); 54 | $layers = $event->getLayers(); 55 | $limit = $this->context->getMemoryLimit($options['memory_limit']); 56 | $fixed = memory_get_usage(true); 57 | $peak = 0; 58 | foreach ($layers as $key => $layer) { 59 | if (!($layer instanceof ImageAwareLayerInterface)) { 60 | continue; 61 | } 62 | $width = $layer->get('image.width'); 63 | $height = $layer->get('image.height'); 64 | $finalWidth = $layer->get('final.width'); 65 | $finalHeight = $layer->get('final.height'); 66 | $constant = $this->getMemoryConstant($layer->get('image.format')); 67 | $dynamic = $width * $height * $constant; 68 | if (0 === $key) { 69 | $fixed += $finalWidth * $finalHeight * $constant; 70 | if ($limit < $fixed + $dynamic) { 71 | if ($finalWidth * $finalHeight > $width * $height) { 72 | throw new RuntimeException( 73 | 'output.image.dimensions.exceed.limit.%cp_dimensions%', 74 | ['%cp_dimensions%' => $finalWidth.'x'.$finalHeight] 75 | ); 76 | } else { 77 | throw new RuntimeException( 78 | 'image.dimensions.exceed.limit.%cp_dimensions%', 79 | ['%cp_dimensions%' => $width.'x'.$height] 80 | ); 81 | } 82 | } 83 | } elseif ($limit < $fixed + $dynamic) { 84 | throw new RuntimeException('not.enough.memory.to.process.image'); 85 | } 86 | $peak = ($dynamic > $peak) ? $dynamic : $peak; 87 | } 88 | $this->extras['memory_approx'] = number_format(($fixed + $peak) / (1024 * 1024), 2).' MB'; 89 | } 90 | 91 | /** 92 | * @param PhpGdEvent $event 93 | */ 94 | public function addImageExtras(PhpGdEvent $event) 95 | { 96 | if ($this->extras) { 97 | $image = $event->getImage(); 98 | $image->addExtras($this->extras); 99 | } 100 | } 101 | 102 | /** 103 | * @param string $format 104 | * @return int|float 105 | */ 106 | protected function getMemoryConstant($format) 107 | { 108 | switch($format) { 109 | case PhpGdContext::FORMAT_JPEG: 110 | $constant = 6.2; 111 | break; 112 | case PhpGdContext::FORMAT_GIF: 113 | $constant = 3; 114 | break; 115 | case PhpGdContext::FORMAT_PNG: 116 | $constant = 9.5; 117 | break; 118 | case PhpGdContext::FORMAT_WEBP: 119 | $constant = 6; 120 | break; 121 | default: 122 | $constant = 4; 123 | } 124 | 125 | return $constant; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/SystemRequirementListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class SystemRequirementListener implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var PhpGdContext 20 | */ 21 | protected $context; 22 | 23 | /** 24 | * @param PhpGdContext $context 25 | */ 26 | public function __construct(PhpGdContext $context) 27 | { 28 | $this->context = $context; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public static function getSubscribedEvents() 35 | { 36 | return [ 37 | PhpGdEvents::PRE_IMAGE => [ 38 | ['verifyEngine', 999], 39 | ['verifySavedFormat', 989], 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * @throws RuntimeException 46 | */ 47 | public function verifyEngine() 48 | { 49 | if (!$this->context->isEngineSupported()) { 50 | throw new RuntimeException('gd.extension.not.enabled'); 51 | } 52 | } 53 | 54 | /** 55 | * @param PhpGdEvent $event 56 | * @throws InvalidArgumentException 57 | */ 58 | public function verifySavedFormat(PhpGdEvent $event) 59 | { 60 | $format = $event->getOptions()['output_format']; 61 | if ('default' !== $format && !$this->context->isImageFormatSupported($format)) { 62 | throw new InvalidArgumentException( 63 | 'output.image.format.not.supported.%cp_unsupported%', 64 | ['%cp_unsupported%' => '"'.$format.'"'] 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/EventListener/TextLayerListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class TextLayerListener implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var PhpGdContext 20 | */ 21 | protected $context; 22 | 23 | /** 24 | * @param PhpGdContext $context 25 | */ 26 | public function __construct(PhpGdContext $context) 27 | { 28 | $this->context = $context; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public static function getSubscribedEvents() 35 | { 36 | return [ 37 | PhpGdEvents::PRE_IMAGE => ['verifyFreeType', 849], 38 | ]; 39 | } 40 | 41 | /** 42 | * @param PhpGdEvent 43 | * @throws RuntimeException 44 | */ 45 | public function verifyFreeType(PhpGdEvent $event) 46 | { 47 | $layers = $event->getLayers(); 48 | foreach ($layers as $key => $layer) { 49 | if (!($layer instanceof TextLayerInterface)) { 50 | continue; 51 | } 52 | if (!$this->context->isFreeTypeSupported()) { 53 | throw new RuntimeException('adding.text.not.supported'); 54 | } 55 | break; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Core/ImageFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0.0 17 | */ 18 | class ImageFactory 19 | { 20 | /** 21 | * @var ResourceHelper 22 | */ 23 | protected $rh; 24 | 25 | /** 26 | * @param ResourceHelper $rh 27 | */ 28 | public function __construct(ResourceHelper $rh) 29 | { 30 | $this->rh = $rh; 31 | } 32 | 33 | /** 34 | * @param LayerInterface[] $layers 35 | * @param mixed[] $options 36 | * @return Image 37 | */ 38 | public function createImage(array $layers, array $options) 39 | { 40 | $this->initContents($layers, $options); 41 | $contents = $layers[0]->get('final.contents'); 42 | 43 | $image = new Image(); 44 | $image->setContents($contents); 45 | 46 | return $image; 47 | } 48 | 49 | /** 50 | * @param LayerInterface[] $layers 51 | * @param mixed[] $options 52 | */ 53 | protected function initContents(array $layers, $options) 54 | { 55 | foreach ($layers as $layer) { 56 | if ($layer instanceof ImageAwareLayerInterface) { 57 | $this->initImageAwareLayerResource($layer); 58 | } elseif ($layer instanceof TextLayerInterface) { 59 | $this->initTextLayerResource($layer); 60 | } 61 | if ($layer instanceof RegularLayerInterface) { 62 | $resource = $this->rh->getMergedGdResource( 63 | $layers[0]->get('final.resource'), 64 | $layer->get('final.resource'), 65 | $layer->get('regular.move.x'), 66 | $layer->get('regular.move.y'), 67 | $layer->get('regular.move.gravity') 68 | ); 69 | imagedestroy($layer->get('final.resource')); 70 | $layers[0]->set('final.resource', $resource); 71 | } 72 | } 73 | $contents = $this->rh->getContentsFromGdResource( 74 | $layers[0]->get('final.format'), 75 | $layers[0]->get('final.resource'), 76 | $options, 77 | true 78 | ); 79 | imagedestroy($layers[0]->get('final.resource')); 80 | 81 | $layers[0]->set('final.contents', $contents); 82 | } 83 | 84 | /** 85 | * @param ImageAwareLayerInterface $layer 86 | */ 87 | protected function initImageAwareLayerResource(ImageAwareLayerInterface $layer) 88 | { 89 | $format = $layer->get('image.format'); 90 | if ($layer->has('image.imc_uri')) { 91 | $uri = $layer->get('image.imc_uri'); 92 | $resource = $this->rh->getGdResourceFromStream($format, $uri, true); 93 | } else { 94 | $contents = $layer->get('image.contents'); 95 | $resource = $this->rh->getGdResourceFromContents($format, $contents, true); 96 | } 97 | 98 | if ($layer->has('image.resize.width')) { 99 | $resource = $this->rh->getResizedGdResource( 100 | $resource, 101 | $layer->get('image.resize.width'), 102 | $layer->get('image.resize.height'), 103 | $layer->get('image.resize.option'), 104 | true 105 | ); 106 | } elseif ($layer instanceof BackgroundLayerInterface && $layer->get('image.format') === PhpGdContext::FORMAT_PNG) { 107 | $resource = $this->rh->getClonedGdResource($resource); 108 | } 109 | 110 | $layer->set('final.resource', $resource); 111 | } 112 | 113 | /** 114 | * @param TextLayerInterface $layer 115 | */ 116 | protected function initTextLayerResource(TextLayerInterface $layer) 117 | { 118 | $resource = $this->rh->getTextGdResource( 119 | $layer->get('text.font.filename'), 120 | $layer->get('text.font.size'), 121 | $layer->get('text.font.rgb_color'), 122 | $layer->get('text.label'), 123 | $layer->get('text.line_spacing'), 124 | $layer->get('text.angle'), 125 | $layer->get('text.box.paddings'), 126 | $layer->get('text.box.rgb_color') 127 | ); 128 | 129 | $layer->set('final.resource', $resource); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/DelegatingExtension.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class DelegatingExtension implements ExtensionInterface 15 | { 16 | /** 17 | * @return ExtensionInterface[] 18 | */ 19 | protected function getRegisteredExtensions() 20 | { 21 | return [ 22 | new CoreExtension(), 23 | new GifExtension(), 24 | new SaveExtension(), 25 | ]; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function boot(EventDispatcherInterface $dispatcher) 32 | { 33 | $extensions = $this->getRegisteredExtensions(); 34 | foreach ($extensions as $extension) { 35 | $extension->boot($dispatcher); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | interface ExtensionInterface 12 | { 13 | /** 14 | * @param EventDispatcherInterface $dispatcher 15 | */ 16 | public function boot(EventDispatcherInterface $dispatcher); 17 | } 18 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/EventListener/GifExtractorListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0.0 15 | */ 16 | class GifExtractorListener implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var GifExtractor 20 | */ 21 | protected $extractor; 22 | 23 | /** 24 | * @var mixed[] 25 | */ 26 | protected $extras = []; 27 | 28 | /** 29 | * @param GifExtractor $extractor 30 | */ 31 | public function __construct(GifExtractor $extractor) 32 | { 33 | $this->extractor = $extractor; 34 | } 35 | 36 | /** 37 | * @return inheritDoc 38 | */ 39 | public static function getSubscribedEvents() 40 | { 41 | return [ 42 | PhpGdEvents::PRE_IMAGE => ['initExtracted', 829], 43 | PhpGdEvents::FINISH_IMAGE => ['addImageExtras', 879], 44 | ]; 45 | } 46 | 47 | /** 48 | * @param PhpGdEvent $event 49 | */ 50 | public function initExtracted(PhpGdEvent $event) 51 | { 52 | $options = $event->getOptions(); 53 | $layers = $event->getLayers(); 54 | 55 | if ( 56 | !$options['gif_animation'] || 57 | PhpGdContext::FORMAT_GIF !== $layers[0]->get('final.format') || 58 | PhpGdContext::FORMAT_GIF !== $layers[0]->get('image.format') 59 | ) { 60 | return; 61 | } 62 | 63 | $fp = $layers[0]->get('image.fp'); 64 | rewind($fp); 65 | if ('GIF89a' !== fread($fp, 6)) { 66 | return; 67 | } 68 | rewind($fp); 69 | $extracted = $this->extractor->extractFromFilePointer($fp); 70 | if (!$extracted->isAnimated()) { 71 | return; 72 | } 73 | if (!$extracted->isValid()) { 74 | $e = new TranslatedException('gif.animation.may.lost.due.to.corrupted.frame.data'); 75 | $this->extras['gif_error'] = $e->getMessage(); 76 | 77 | return; 78 | } 79 | 80 | $layers[0]->set('gif.extracted', $extracted); 81 | } 82 | 83 | /** 84 | * @param PhpGdEvent $event 85 | */ 86 | public function addImageExtras(PhpGdEvent $event) 87 | { 88 | if ($this->extras) { 89 | $image = $event->getImage(); 90 | $image->addExtras($this->extras); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/EventListener/ImageFactoryListener.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 1.0.0 14 | */ 15 | class ImageFactoryListener implements EventSubscriberInterface 16 | { 17 | /** 18 | * @var ImageFactory 19 | */ 20 | protected $factory; 21 | 22 | /** 23 | * @var mixed[] 24 | */ 25 | protected $extras = []; 26 | 27 | /** 28 | * @param ImageFactory $factory 29 | */ 30 | public function __construct(ImageFactory $factory) 31 | { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * @return mixed[] 37 | */ 38 | public static function getSubscribedEvents() 39 | { 40 | return [ 41 | PhpGdEvents::IMAGE => ['createImage', 199], 42 | PhpGdEvents::FINISH_IMAGE => ['addImageExtras', 869], 43 | ]; 44 | } 45 | 46 | /** 47 | * @param PhpGdEvent $event 48 | */ 49 | public function createImage(PhpGdEvent $event) 50 | { 51 | $layers = $event->getLayers(); 52 | if (!$layers[0]->has('gif.extracted')) { 53 | return; 54 | } 55 | 56 | try { 57 | $options = $event->getOptions(); 58 | $image = $this->factory->createImage($layers, $options); 59 | $event->setImage($image); 60 | $event->stopPropagation(); 61 | } catch (\Exception $e) { 62 | $e = new TranslatedException('gif.animation.may.lost.due.to.corrupted.frame.data'); 63 | $this->extras['gif_error'] = $e->getMessage(); 64 | } 65 | } 66 | 67 | /** 68 | * param PhpGdEvent $event 69 | */ 70 | public function addImageExtras(PhpGdEvent $event) 71 | { 72 | if (!$this->extras) { 73 | return; 74 | } 75 | $image = $event->getImage(); 76 | $image->addExtras($this->extras); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/EventListener/MemoryRequirementListener.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 1.0.0 14 | */ 15 | class MemoryRequirementListener implements EventSubscriberInterface 16 | { 17 | /** 18 | * @var PhpGdContext 19 | */ 20 | protected $context; 21 | 22 | /** 23 | * @var mixed[] 24 | */ 25 | protected $extras = []; 26 | 27 | /** 28 | * @param PhpGdContext $context 29 | */ 30 | public function __construct(PhpGdContext $context) 31 | { 32 | $this->context = $context; 33 | } 34 | 35 | /** 36 | * @return mixed[] 37 | */ 38 | public static function getSubscribedEvents() 39 | { 40 | return [ 41 | PhpGdEvents::PRE_IMAGE => ['verifyMemoryLimit', 819], 42 | PhpGdEvents::FINISH_IMAGE => ['addImageExtras', 199], 43 | ]; 44 | } 45 | 46 | /** 47 | * @param PhpGdEvent $event 48 | */ 49 | public function verifyMemoryLimit(PhpGdEvent $event) 50 | { 51 | $layers = $event->getLayers(); 52 | $options = $event->getOptions(); 53 | if (!$layers[0]->has('gif.extracted')) { 54 | return; 55 | } 56 | 57 | $totalFrames = count($layers[0]->get('gif.extracted')); 58 | $width = $layers[0]->get('image.width'); 59 | $height = $layers[0]->get('image.height'); 60 | $pixels = $width * $height; 61 | $finalWidth = $layers[0]->get('final.width'); 62 | $finalHeight = $layers[0]->get('final.height'); 63 | $finalPixels = $finalWidth * $finalHeight; 64 | $totalPixels = $totalFrames * $finalPixels; 65 | $limit = $this->context->getMemoryLimit($options['memory_limit']); 66 | $peak = memory_get_peak_usage(true) + 15 * 1024 * 1024 + 3 * ($pixels + $finalPixels); 67 | 68 | if (1000000 < $finalPixels || 57000000 < $totalPixels || $peak > $limit) { 69 | $e = new TranslatedException( 70 | 'gif.animation.may.lost.as.too.many.frames.or.dimensions.too.large.%total_frames%.%dimensions%', 71 | ['%total_frames%' => $totalFrames, '%dimensions%' => $width.'x'.$height] 72 | ); 73 | $this->extras['gif_error'] = $e->getMessage(); 74 | $layers[0]->remove('gif.extracted'); 75 | } else { 76 | $this->extras['memory_approx'] = number_format($peak / (1024 * 1024), 2).' MB'; 77 | } 78 | } 79 | 80 | /** 81 | * param PhpGdEvent $event 82 | */ 83 | public function addImageExtras(PhpGdEvent $event) 84 | { 85 | $image = $event->getImage(); 86 | if (array_key_exists('gif_error', $image->getExtras())) { 87 | return; 88 | } 89 | $image->addExtras($this->extras); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/GifBuilder.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class GifBuilder 12 | { 13 | /** 14 | * @var int 15 | */ 16 | protected $imageWidth; 17 | 18 | /** 19 | * @var int 20 | */ 21 | protected $imageHeight; 22 | 23 | /** 24 | * @var bool 25 | */ 26 | protected $interlaceFlag; 27 | 28 | /** 29 | * @var null|int 30 | */ 31 | protected $transparentColorIndex; 32 | 33 | /** 34 | * @var string 35 | */ 36 | protected $colorTable; 37 | 38 | /** 39 | * @var string 40 | */ 41 | protected $imageData; 42 | 43 | /** 44 | * @param int $width 45 | * @return $this 46 | */ 47 | public function imageWidth($width) 48 | { 49 | $this->imageWidth = $width; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param int $height 56 | * @return $this 57 | */ 58 | public function imageHeight($height) 59 | { 60 | $this->imageHeight = $height; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param bool $flag 67 | * @return $this 68 | */ 69 | public function interlaceFlag($flag) 70 | { 71 | $this->interlaceFlag = $flag; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param int $index 78 | * @return $this 79 | */ 80 | public function transparentColorIndex($index) 81 | { 82 | $this->transparentColorIndex = $index; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @param string $contents 89 | * @return $this 90 | */ 91 | public function colorTable($contents) 92 | { 93 | $this->colorTable = $contents; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * @param string $contents 100 | * @return $this 101 | */ 102 | public function imageData($contents) 103 | { 104 | $this->imageData = $contents; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getContents() 113 | { 114 | // Header 115 | $contents = 'GIF89a'; 116 | 117 | // Logical screen descriptor 118 | $contents .= pack('v*', $this->imageWidth, $this->imageHeight); 119 | $contents .= "\x00\x00\x00"; 120 | 121 | // Graphic control extension 122 | if (isset($this->transparentColorIndex)) { 123 | $contents .= "\x21\xF9\x04\x01\x00\x00"; 124 | $contents .= pack('C', $this->transparentColorIndex); 125 | $contents .= "\x00"; 126 | } 127 | 128 | // Image descriptor 129 | $contents .= "\x2C\x00\x00\x00\x00"; 130 | $contents .= pack('v*', $this->imageWidth, $this->imageHeight); 131 | $interlace = $this->interlaceFlag ? 0b01000000 : 0; 132 | $colorTableSize = log(strlen($this->colorTable) / 3, 2) - 1; 133 | $unpack = 0b10000000 | $interlace | $colorTableSize; 134 | $pack = pack('C', $unpack); 135 | $contents .= $pack; 136 | 137 | // Local color table 138 | $contents .= $this->colorTable; 139 | 140 | // Image Data 141 | $contents .= $this->imageData; 142 | 143 | // Terminator 144 | $contents .= "\x3B"; 145 | 146 | $this->imageWidth = null; 147 | $this->imageHeight = null; 148 | $this->interlaceFlag = null; 149 | $this->transparentColorIndex = null; 150 | $this->colorTable= null; 151 | $this->imageData= null; 152 | 153 | return $contents; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/GifExtension.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 1.0.0 16 | */ 17 | class GifExtension implements ExtensionInterface 18 | { 19 | /** 20 | * @param EventDispatcherInterface $dispatcher 21 | */ 22 | public function boot(EventDispatcherInterface $dispatcher) 23 | { 24 | $context = new PhpGdContext(); 25 | $rh = new ResourceHelper(); 26 | $extractor = new GifExtractor(); 27 | $builder = new GifBuilder(); 28 | $builderPlus = new GifBuilderPlus(); 29 | $optimizer = new GifOptimizer($rh); 30 | $factory = new ImageFactory($rh, $extractor, $builder, $builderPlus, $optimizer); 31 | 32 | $dispatcher->addSubscriber(new GifExtractorListener($extractor)); 33 | $dispatcher->addSubscriber(new MemoryRequirementListener($context)); 34 | $dispatcher->addSubscriber(new ImageFactoryListener($factory)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/GifExtractor.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class GifExtractor 12 | { 13 | /** 14 | * @var int 15 | */ 16 | protected $kp; 17 | 18 | /** 19 | * @var int 20 | */ 21 | protected $dp; 22 | 23 | /** 24 | * @param string $uri 25 | * @return GifExtracted 26 | */ 27 | public function extractFromStream($uri) 28 | { 29 | $fp = fopen($uri, 'r'); 30 | $this->i = $uri; 31 | $extracted = $this->extractFromFilePointer($fp); 32 | fclose($fp); 33 | 34 | return $extracted; 35 | } 36 | 37 | /** 38 | * @param string $contents 39 | * @return GifExtracted 40 | */ 41 | public function extractFromContents($contents) 42 | { 43 | $fp = fopen('php://memory', 'r+'); 44 | fwrite($fp, $contents); 45 | rewind($fp); 46 | $extracted = $this->extractFromFilePointer($fp); 47 | fclose($fp); 48 | 49 | return $extracted; 50 | } 51 | 52 | /** 53 | * @param resource $fp 54 | * @return GifExtracted 55 | * @throws FileParseException 56 | */ 57 | public function extractFromFilePointer($fp) 58 | { 59 | $extracted = new GifExtracted(); 60 | $this->readMetadata($fp, $extracted); 61 | try { 62 | while (!feof($fp)) { 63 | switch ($this->read($fp, 1)) { 64 | case "\x21": 65 | $this->readExtension($fp, $extracted); 66 | break; 67 | case "\x2C": 68 | $this->readFrame($fp, $extracted); 69 | break; 70 | case "\x3B": 71 | $this->readEnd($extracted); 72 | break 2; 73 | default: 74 | throw new FileParseException('gif.parse.error'); 75 | } 76 | } 77 | } catch (\Exception $e) { 78 | $extracted->setValid(false); 79 | } 80 | $this->kp = null; 81 | $this->dp = null; 82 | 83 | return $extracted; 84 | } 85 | 86 | /** 87 | * @param resource $fp 88 | * @param GifExtracted $extracted 89 | */ 90 | protected function readMetadata($fp, GifExtracted $extracted) 91 | { 92 | $extracted->setHeader($v = $this->read($fp, 6)); 93 | $extracted->setLogicalScreenDescriptor($v = $this->read($fp, 7)); 94 | if ($v = $extracted->getGlobalColorTableFlag()) { 95 | $quantity = $extracted->getTotalGlobalColors(); 96 | $extracted->setGlobalColorTable($this->read($fp, $quantity * 3)); 97 | } 98 | } 99 | 100 | /** 101 | * @param resource $fp 102 | * @param GifExtracted $extracted 103 | */ 104 | protected function readExtension($fp, GifExtracted $extracted) 105 | { 106 | switch ($this->read($fp, 1)) { 107 | case "\xFF": 108 | if ("\x0bNETSCAPE2.0" !== $this->read($fp, 12)) { 109 | $this->readDataBlock($fp); 110 | break; 111 | } 112 | $extracted->setNetscapeExtension("\x0bNETSCAPE2.0" . $this->read($fp, 5)); 113 | break; 114 | case "\xF9": 115 | $contents = $this->read($fp, 6); 116 | if ($extracted->hasGraphicControlExtension()) { 117 | break; 118 | } 119 | $extracted->setGraphicControlExtension($contents); 120 | break; 121 | default: 122 | $this->readDataBlock($fp); 123 | } 124 | } 125 | 126 | /** 127 | * @param resource $fp 128 | * @param GifExtracted $extracted 129 | */ 130 | protected function readFrame($fp, GifExtracted $extracted) 131 | { 132 | if (!$extracted->hasGraphicControlExtension()) { 133 | $contents = "\x04\x00".pack('v', 10)."\x00\x00"; 134 | $extracted->setGraphicControlExtension($contents); 135 | } 136 | 137 | $extracted->setLinkedKey($this->kp); 138 | $extracted->setLinkedDisposalMethod($this->dp); 139 | if (GifExtracted::DISPOSAL_METHOD_PREVIOUS !== $extracted->getDisposalMethod()) { 140 | $this->kp = $extracted->key(); 141 | $this->dp = $extracted->getDisposalMethod(); 142 | } 143 | 144 | $extracted->setImageDescriptor($this->read($fp, 9)); 145 | 146 | if ($extracted->getLocalColorTableFlag()) { 147 | $quantity = $extracted->getTotalLocalColors(); 148 | $extracted->setLocalColorTable($this->read($fp, $quantity * 3)); 149 | } 150 | 151 | $extracted->setImageData($this->read($fp, 1) . $this->readDataBlock($fp)); 152 | $extracted->next(); 153 | } 154 | 155 | /** 156 | * @param GifExtracted $extracted 157 | */ 158 | protected function readEnd(GifExtracted $extracted) 159 | { 160 | if (!$extracted->hasNetscapeExtension() && 1 < count($extracted)) { 161 | $extracted->setNetscapeExtension("\x0bNETSCAPE2.0\x03\x01\x00\x00\x00"); 162 | } 163 | $extracted->rewind(); 164 | } 165 | 166 | /** 167 | * @param resource $fp 168 | * @return string 169 | * @throws FileParseException 170 | */ 171 | protected function readDataBlock($fp) 172 | { 173 | $str = ''; 174 | while (true) { 175 | $packed = $this->read($fp, 1); 176 | if ("\x00" === $packed) { 177 | $str .= "\x00"; 178 | break; 179 | } 180 | $str .= $packed; 181 | $blockSize = unpack('C', $packed)[1]; 182 | if (0 === $blockSize) { 183 | throw new FileParseException('gif.parse.error'); 184 | } 185 | $str .= $this->read($fp, $blockSize); 186 | } 187 | 188 | return $str; 189 | } 190 | 191 | /** 192 | * @param resource $fp 193 | * @param int $length 194 | * @return string 195 | * @throws FileParseException 196 | */ 197 | protected function read($fp, $length) 198 | { 199 | $bytes = @fread($fp, $length); 200 | if (false === $bytes || $length > strlen($bytes)) { 201 | throw new FileParseException('gif.parse.error'); 202 | } 203 | 204 | return $bytes; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Gif/GifOptimizer.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class GifOptimizer 12 | { 13 | /** 14 | * @var ResourceHelper 15 | */ 16 | protected $rh; 17 | 18 | /** 19 | * @param ResourceHelper $rh 20 | */ 21 | public function __construct(ResourceHelper $rh) 22 | { 23 | $this->rh = $rh; 24 | } 25 | 26 | /** 27 | * @param resource $resource 28 | * @param resource $controlResource 29 | * @return resource 30 | */ 31 | public function getOptimizedGdResource($resource, $controlResource) 32 | { 33 | if (imageistruecolor($resource)) { 34 | $resource = $this->rh->getPalettizedGdResource($resource); 35 | } 36 | $totalColors = imagecolorstotal($resource); 37 | $trans = imagecolortransparent($resource); 38 | $reds = new \SplFixedArray($totalColors); 39 | $greens = new \SplFixedArray($totalColors); 40 | $blues = new \SplFixedArray($totalColors); 41 | 42 | $i = 0; 43 | do { 44 | $colors = imagecolorsforindex($resource, $i); 45 | $reds[$i] = $colors['red']; 46 | $greens[$i] = $colors['green']; 47 | $blues[$i] = $colors['blue']; 48 | } while (++$i < $totalColors); 49 | 50 | if (imageistruecolor($controlResource)) { 51 | $controlResource = $this->rh->getPalettizedGdResource($controlResource); 52 | } 53 | $controlTotalColors = imagecolorstotal($controlResource); 54 | $controlTrans = imagecolortransparent($controlResource); 55 | $controlReds = new \SplFixedArray($controlTotalColors); 56 | $controlGreens = new \SplFixedArray($controlTotalColors); 57 | $controlBlues = new \SplFixedArray($controlTotalColors); 58 | $i = 0; 59 | do { 60 | $colors = imagecolorsforindex($controlResource, $i); 61 | $controlReds[$i] = $colors['red']; 62 | $controlGreens[$i] = $colors['green']; 63 | $controlBlues[$i] = $colors['blue']; 64 | } while (++$i < $controlTotalColors); 65 | 66 | $width = imagesx($resource); 67 | $height = imagesy($resource); 68 | $y = 0; 69 | do{ 70 | $x = 0; 71 | do { 72 | $index = imagecolorat($resource, $x, $y); 73 | $red = $reds[$index]; 74 | $green = $greens[$index]; 75 | $blue = $blues[$index]; 76 | $controlIndex = imagecolorat($controlResource, $x, $y); 77 | $controlRed = $controlReds[$controlIndex]; 78 | $controlGreen = $controlGreens[$controlIndex]; 79 | $controlBlue = $controlBlues[$controlIndex]; 80 | if ( 81 | (($red & 0b11111100) === ($controlRed & 0b11111100)) && 82 | (($green & 0b11111100) === ($controlGreen & 0b11111100)) && 83 | (($blue & 0b11111100) === ($controlBlue & 0b11111100)) 84 | ) { 85 | imagesetpixel($resource, $x, $y, $trans); 86 | } 87 | } while (++$x !== $width); 88 | } while (++$y !== $height); 89 | 90 | return $resource; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Save/EventListener/ImageFactoryListener.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class ImageFactoryListener implements EventSubscriberInterface 15 | { 16 | /** 17 | * @var ImageFactory 18 | */ 19 | protected $factory; 20 | 21 | /** 22 | * @param ImageFactory $factory 23 | */ 24 | public function __construct(ImageFactory $factory) 25 | { 26 | $this->factory = $factory; 27 | } 28 | 29 | /** 30 | * @return mixed[] 31 | */ 32 | public static function getSubscribedEvents() 33 | { 34 | return [ 35 | PhpGdEvents::PRE_IMAGE => ['createImage', 839], 36 | ]; 37 | } 38 | 39 | /** 40 | * @param PhpGdEvent $event 41 | */ 42 | public function createImage(PhpGdEvent $event) 43 | { 44 | $layers = $event->getLayers(); 45 | if (1 === count($layers) && 46 | $layers[0]->get('image.width') === $layers[0]->get('final.width') && 47 | $layers[0]->get('image.height') === $layers[0]->get('final.height') && 48 | $layers[0]->get('image.format') === $layers[0]->get('final.format') 49 | ) { 50 | $options = $event->getOptions(); 51 | $image = $this->factory->createImage($layers, $options); 52 | $event->setImage($image); 53 | $event->stopPropagation(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Save/ImageFactory.php: -------------------------------------------------------------------------------- 1 | 10 | * @since 1.0.0 11 | */ 12 | class ImageFactory 13 | { 14 | /** 15 | * @var ResourceHelper 16 | */ 17 | protected $rh; 18 | 19 | /** 20 | * @param ResourceHelper $rh 21 | */ 22 | public function __construct(ResourceHelper $rh) 23 | { 24 | $this->rh = $rh; 25 | } 26 | 27 | /** 28 | * @param Imagecraft\Layer\LayerInterface[] $layers 29 | * @param mixed[] $options 30 | * @return Image 31 | */ 32 | public function createImage(array $layers, array $options) 33 | { 34 | $this->initContents($layers, $options); 35 | $contents = $layers[0]->get('final.contents'); 36 | 37 | $image = new Image(); 38 | $image->setContents($contents); 39 | 40 | return $image; 41 | } 42 | 43 | /** 44 | * @param LayerInterface[] $layers 45 | * @param mixed[] $options 46 | */ 47 | protected function initContents(array $layers, $options) 48 | { 49 | $format = $layers[0]->get('image.format'); 50 | if ($layers[0]->has('image.imc_uri')) { 51 | $uri = $layers[0]->get('image.imc_uri'); 52 | $resource = $this->rh->getGdResourceFromStream($format, $uri, true); 53 | imagedestroy($resource); 54 | $contents = file_get_contents($uri); 55 | } else { 56 | $contents = $layers[0]->get('image.contents'); 57 | $resource = $this->rh->getGdResourceFromContents($format, $contents, true); 58 | imagedestroy($resource); 59 | } 60 | 61 | $layers[0]->set('final.contents', $contents); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/Extension/Save/SaveExtension.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class SaveExtension implements ExtensionInterface 15 | { 16 | /** 17 | * @param EventDispatcherInterface $dispatcher 18 | */ 19 | public function boot(EventDispatcherInterface $dispatcher) 20 | { 21 | $rh = new ResourceHelper(); 22 | $factory = new ImageFactory($rh); 23 | 24 | $dispatcher->addSubscriber(new ImageFactoryListener($factory)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/PhpGdContext.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0.0 13 | */ 14 | class PhpGdContext extends AbstractContext 15 | { 16 | const FORMAT_WEBP = 'webp'; 17 | const FORMAT_PNG = 'png'; 18 | const FORMAT_JPEG = 'jpeg'; 19 | const FORMAT_GIF = 'gif'; 20 | 21 | /** 22 | * @param string $format 23 | * @return bool 24 | */ 25 | public function isImageFormatSupported($format) 26 | { 27 | switch ($format) { 28 | case static::FORMAT_WEBP: 29 | $supported = function_exists('imagecreatefromwebp'); 30 | break; 31 | case static::FORMAT_PNG: 32 | $supported = function_exists('imagecreatefrompng'); 33 | break; 34 | case static::FORMAT_JPEG: 35 | $supported = function_exists('imagecreatefromjpeg'); 36 | break; 37 | case static::FORMAT_GIF: 38 | $supported = function_exists('imagecreatefromgif') && function_exists('imagegif'); 39 | break; 40 | default: 41 | $supported = false; 42 | } 43 | 44 | return $supported; 45 | } 46 | 47 | /** 48 | * @param string $format 49 | * @return string 50 | */ 51 | public function getImageMime($format) 52 | { 53 | $mimes = [ 54 | static::FORMAT_WEBP => 'image/webp', 55 | static::FORMAT_PNG => 'image/png', 56 | static::FORMAT_JPEG => 'image/jpeg', 57 | static::FORMAT_GIF => 'image/gif', 58 | ]; 59 | 60 | return $mimes[$format]; 61 | } 62 | 63 | /** 64 | * @param string $format 65 | * @return string 66 | */ 67 | public function getImageExtension($format) 68 | { 69 | $extensions = [ 70 | static::FORMAT_WEBP => 'webp', 71 | static::FORMAT_PNG => 'png', 72 | static::FORMAT_JPEG => 'jpg', 73 | static::FORMAT_GIF => 'gif', 74 | ]; 75 | 76 | return $extensions[$format]; 77 | } 78 | 79 | /** 80 | * @return bool 81 | */ 82 | public function isFreeTypeSupported() 83 | { 84 | return function_exists('imagefttext'); 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function isEngineSupported() 91 | { 92 | return extension_loaded('gd'); 93 | } 94 | 95 | /** 96 | * @inheritDoc 97 | */ 98 | public function getSupportedImageFormatsToString() 99 | { 100 | $formats = [ 101 | [static::FORMAT_WEBP, 'WEBP (VP8)'], 102 | [static::FORMAT_PNG, 'PNG'], 103 | [static::FORMAT_JPEG, 'JPEG'], 104 | [static::FORMAT_GIF, 'GIF'], 105 | ]; 106 | for ($i = 0, $str = ''; $i < count($formats); $i++) { 107 | if ($this->isImageFormatSupported($formats[$i][0])) { 108 | $str .= (0 == $i) ? '"' : ', "'; 109 | $str .= $formats[$i][1].'"'; 110 | } 111 | } 112 | 113 | return $str; 114 | } 115 | 116 | /** 117 | * @inheritDoc 118 | */ 119 | public function getSupportedFontFormatsToString() 120 | { 121 | if ($this->isFreeTypeSupported()) { 122 | return '"Postscript (.pfa, .pfb)", "TureType (.ttf)", "OpenType (.otf)"'; 123 | } 124 | 125 | return ''; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/PhpGdEngine.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 1.0.0 12 | */ 13 | class PhpGdEngine implements EngineInterface 14 | { 15 | /** 16 | * @inheritDoc 17 | */ 18 | public function getImage(array $layers, array $options) 19 | { 20 | $dispatcher = new EventDispatcher(); 21 | $extension = new DelegatingExtension(); 22 | $extension->boot($dispatcher); 23 | 24 | $event = new PhpGdEvent($layers, $options); 25 | $dispatcher->dispatch(PhpGdEvents::PRE_IMAGE, $event); 26 | 27 | if (!$event->getImage()) { 28 | $event = new PhpGdEvent($layers, $options); 29 | $dispatcher->dispatch(PhpGdEvents::IMAGE, $event); 30 | } 31 | 32 | $image = $event->getImage(); 33 | $event = new PhpGdEvent($layers, $options); 34 | $event->setImage($image); 35 | $dispatcher->dispatch(PhpGdEvents::FINISH_IMAGE, $event); 36 | 37 | return $event->getImage(); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function getContext(array $options) 44 | { 45 | return new PhpGdContext(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/PhpGdEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @since 1.0.0 11 | */ 12 | class PhpGdEvent extends Event 13 | { 14 | /** 15 | * @var \Imagecraft\Layer\LayerInterface[] 16 | */ 17 | protected $layers; 18 | 19 | /** 20 | * @var mixed[] 21 | */ 22 | protected $options; 23 | 24 | /** 25 | * @var Image 26 | */ 27 | protected $image; 28 | 29 | /** 30 | * @param \Imagecraft\Layer\LayerInterface[] $layers 31 | * @param mixed[] $options 32 | */ 33 | public function __construct(array $layers, array $options) 34 | { 35 | $this->layers = $layers; 36 | $this->options = $options; 37 | } 38 | 39 | /** 40 | * @return \Imagecraft\Layer\LayerInterface[] 41 | */ 42 | public function getLayers() 43 | { 44 | return $this->layers; 45 | } 46 | 47 | /** 48 | * @return mixed[] 49 | */ 50 | public function getOptions() 51 | { 52 | return $this->options; 53 | } 54 | 55 | /** 56 | * @param Image $image 57 | */ 58 | public function setImage(Image $image) 59 | { 60 | $this->image = $image; 61 | } 62 | 63 | /** 64 | * @return Image 65 | */ 66 | public function getImage() 67 | { 68 | return $this->image; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/PhpGdEvents.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | final class PhpGdEvents 12 | { 13 | const PRE_IMAGE = 'php_gd.pre_image'; 14 | const IMAGE = 'php_gd.image'; 15 | const FINISH_IMAGE = 'php_gd.finish_image'; 16 | } 17 | -------------------------------------------------------------------------------- /src/Engine/PhpGd/README.md: -------------------------------------------------------------------------------- 1 | # PhpGd Engine 2 | 3 | ## EventListeners 4 | 5 | | Event | PRI | Ext | Listener | Method | PPG | 6 | | :------------- | :---- | :----- | :-------------------------- | :-------------------- | :-- | 7 | | `PRE_IMAGE` | `999` | `Core` | `SystemRequirementListener` | `verifyEngine` | | 8 | | `PRE_IMAGE` | `989` | `Core` | `SystemRequirementListener` | `verifySavedFormat` | | 9 | | `PRE_IMAGE` | `909` | `Core` | `ImageAwareLayerListener` | `initImcUri` | | 10 | | `PRE_IMAGE` | `899` | `Core` | `ImageAwareLayerListener` | `initFilePointer` | | 11 | | `PRE_IMAGE` | `889` | `Core` | `ImageAwareLayerListener` | `initImageInfo` | | 12 | | `PRE_IMAGE` | `879` | `Core` | `ImageAwareLayerListener` | `initFinalDimensions` | | 13 | | `PRE_IMAGE` | `869` | `Core` | `MemoryRequirementListener` | `verifyMemoryLimit` | | 14 | | `PRE_IMAGE` | `859` | `Core` | `BackgroundLayerListener` | `initFinalFormat` | | 15 | | `PRE_IMAGE` | `849` | `Core` | `TextLayerListener` | `verifyFreeType` | | 16 | | `PRE_IMAGE` | `839` | `Save` | `ImageFactoryListener` | `createImage` | Yes | 17 | | `PRE_IMAGE` | `829` | `Gif` | `GifExtractorListener` | `initExtracted` | | 18 | | `PRE_IMAGE` | `819` | `Gif` | `MemoryRequirementListener` | `verifyMemoryLimit` | | 19 | | `PRE_IMAGE` | `099` | `Core` | `ImageAwareLayerListener` | `termFilePointer` | | 20 | | `IMAGE` | `199` | `Gif` | `ImageFactoryListener` | `createImage` | Yes | 21 | | `IMAGE` | `099` | `Core` | `ImageFactoryListener` | `createImage` | Yes | 22 | | `FINISH_IMAGE` | `899` | `Core` | `ImageMetadataListener` | `addImageMetadatas` | | 23 | | `FINISH_IMAGE` | `889` | `Core` | `MemoryRequirementListener` | `addImageExtras` | | 24 | | `FINISH_IMAGE` | `879` | `Gif` | `GifExtractorListener` | `addImageExtras` | | 25 | | `FINISH_IMAGE` | `869` | `Gif` | `ImageFactoryListener` | `addImageExtras` | | 26 | | `FINISH_IMAGE` | `199` | `Gif` | `MemoryRequirementListener` | `addImageExtras` | | 27 | | `FINISH_IMAGE` | `099` | `Core` | `ImageAwareLayerListener` | `termFilePointer` | | 28 | | `FINISH_IMAGE` | `089` | `Core` | `ImageAwareLayerListener` | `termImcUri` | | 29 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class BadMethodCallException extends TranslatedException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/FileParseException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class FileParseException extends RuntimeException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class InvalidArgumentException extends TranslatedException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/InvalidFontException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class InvalidFontException extends RuntimeException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/InvalidImageException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class InvalidImageException extends RuntimeException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class RuntimeException extends TranslatedException 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Exception/TranslatedException.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class TranslatedException extends BaseTranslatedException 12 | { 13 | /** 14 | * @param string $id 15 | * @param string[] $parameters 16 | * @param null|int $number 17 | * @param int $code 18 | * @param null|\Exception $previous 19 | */ 20 | public function __construct( 21 | $id, 22 | array $parameters = [], 23 | $number = null, 24 | $code = 0, 25 | \Exception $previous = null 26 | ) { 27 | parent::__construct('imagecraft', $id, $parameters, $number, $code, $previous); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Image.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class Image 10 | { 11 | /** 12 | * @var string 13 | */ 14 | protected $contents; 15 | 16 | /** 17 | * @var int 18 | */ 19 | protected $width; 20 | 21 | /** 22 | * @var int 23 | */ 24 | protected $height; 25 | 26 | /** 27 | * @var string 28 | */ 29 | protected $mime; 30 | 31 | /** 32 | * @var string 33 | */ 34 | protected $extension; 35 | 36 | /** 37 | * @var string|null 38 | */ 39 | protected $message; 40 | 41 | /** 42 | * @var string|null 43 | */ 44 | protected $verboseMessage; 45 | 46 | /** 47 | * @var mixed[] 48 | */ 49 | protected $extras = []; 50 | 51 | /** 52 | * @return bool 53 | * @api 54 | */ 55 | public function isValid() 56 | { 57 | return !isset($this->message); 58 | } 59 | 60 | /** 61 | * @param string $message 62 | */ 63 | public function setMessage($message) 64 | { 65 | $this->message = $message; 66 | } 67 | 68 | /** 69 | * @return string 70 | * @api 71 | */ 72 | public function getMessage() 73 | { 74 | return $this->message; 75 | } 76 | 77 | /** 78 | * @param string $verboseMessage 79 | */ 80 | public function setVerboseMessage($verboseMessage) 81 | { 82 | $this->verboseMessage = $verboseMessage; 83 | } 84 | 85 | /** 86 | * @return string 87 | * @api 88 | */ 89 | public function getVerboseMessage() 90 | { 91 | return $this->verboseMessage; 92 | } 93 | 94 | /** 95 | * @param string $contents 96 | */ 97 | public function setContents($contents) 98 | { 99 | $this->contents = $contents; 100 | } 101 | 102 | /** 103 | * @return string 104 | * @api 105 | */ 106 | public function getContents() 107 | { 108 | return $this->contents; 109 | } 110 | 111 | /** 112 | * @param int $width 113 | */ 114 | public function setWidth($width) 115 | { 116 | $this->width = $width; 117 | } 118 | 119 | /** 120 | * @return int 121 | * @api 122 | */ 123 | public function getWidth() 124 | { 125 | return $this->width; 126 | } 127 | 128 | /** 129 | * @param int $height 130 | */ 131 | public function setHeight($height) 132 | { 133 | $this->height = $height; 134 | } 135 | 136 | /** 137 | * @return int 138 | * @api 139 | */ 140 | public function getHeight() 141 | { 142 | return $this->height; 143 | } 144 | 145 | /** 146 | * @param string $mime 147 | */ 148 | public function setMime($mime) 149 | { 150 | $this->mime = $mime; 151 | } 152 | 153 | /** 154 | * @return string 155 | * @api 156 | */ 157 | public function getMime() 158 | { 159 | return $this->mime; 160 | } 161 | 162 | /** 163 | * @param string $extension 164 | */ 165 | public function setExtension($extension) 166 | { 167 | $this->extension = $extension; 168 | } 169 | 170 | /** 171 | * @return string 172 | * @api 173 | */ 174 | public function getExtension() 175 | { 176 | return $this->extension; 177 | } 178 | 179 | /** 180 | * @param mixed $extras 181 | */ 182 | public function addExtras(array $extras) 183 | { 184 | $this->extras = $extras + $this->extras; 185 | } 186 | 187 | /** 188 | * @return string 189 | * @api 190 | */ 191 | public function getExtras() 192 | { 193 | return $this->extras; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/ImageBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 1.0.0 16 | */ 17 | class ImageBuilder 18 | { 19 | /** 20 | * @var mixed[] 21 | */ 22 | protected $options; 23 | 24 | /** 25 | * @var \Imagecraft\Layer\LayerInterface[] 26 | */ 27 | protected $layers; 28 | 29 | /** 30 | * @param mixed[] $options 31 | */ 32 | public function __construct(array $options = []) 33 | { 34 | $this->options = $options; 35 | $this->layers = [0 => null]; 36 | } 37 | 38 | /** 39 | * @return BackgroundLayer 40 | * @api 41 | */ 42 | public function addBackgroundLayer() 43 | { 44 | return $this->layers[0] = new BackgroundLayer($this); 45 | } 46 | 47 | /** 48 | * @return ImageLayer 49 | * @api 50 | */ 51 | public function addImageLayer() 52 | { 53 | return $this->layers[] = new ImageLayer($this); 54 | } 55 | 56 | /** 57 | * @return TextLayer 58 | * @api 59 | */ 60 | public function addTextLayer() 61 | { 62 | return $this->layers[] = new TextLayer($this); 63 | } 64 | 65 | /** 66 | * @return Image 67 | * @api 68 | */ 69 | public function save() 70 | { 71 | try { 72 | $pass = new DelegatingOptionPass(); 73 | $this->options = $pass->process($this->options); 74 | TranslatedException::init($this->options); 75 | TranslatedException::addResourceDir(__DIR__.'/Resources/translations'); 76 | $pass = new DelegatingLayerPass(); 77 | $this->layers = $pass->process($this->layers); 78 | $engine = new DelegatingEngine(); 79 | $image = $engine->getImage($this->layers, $this->options); 80 | } catch (TranslatedException $e) { 81 | $image = new Image(); 82 | $image->setMessage($e->getMessage()); 83 | $image->setVerboseMessage($e->getVerboseMessage()); 84 | } 85 | $this->layers = [0 => null]; 86 | 87 | return $image; 88 | } 89 | 90 | /** 91 | * @return AbstractContext 92 | * @api 93 | */ 94 | public function about() 95 | { 96 | $engine = new DelegatingEngine($options['engine']); 97 | $context = $engine->getContext($this->options); 98 | 99 | return $context; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Layer/AbstractLayer.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | abstract class AbstractLayer extends ParameterBag implements LayerInterface 12 | { 13 | /** 14 | * @param null|ImageBuilder $builder 15 | */ 16 | public function __construct(ImageBuilder $builder = null) 17 | { 18 | $this->set('image_builder', $builder); 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | public function done() 25 | { 26 | return $this->get('image_builder'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Layer/BackgroundLayer.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class BackgroundLayer extends AbstractLayer implements BackgroundLayerInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function http($url, $dataLimit = -1, $timeout = -1) 15 | { 16 | $this->add([ 17 | 'image.http.url' => $url, 18 | 'image.http.data_limit' => $dataLimit, 19 | 'image.http.timeout' => $timeout, 20 | ]); 21 | 22 | return $this; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function filename($filename) 29 | { 30 | $this->set('image.filename', $filename); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function contents($contents) 39 | { 40 | $this->set('image.contents', $contents); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function resize($width, $height, $option = ImageAwareLayerInterface::RESIZE_SHRINK) 49 | { 50 | $this->add([ 51 | 'image.resize.width' => $width, 52 | 'image.resize.height' => $height, 53 | 'image.resize.option' => $option, 54 | ]); 55 | 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Layer/BackgroundLayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface BackgroundLayerInterface extends ImageAwareLayerInterface 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Layer/ImageAwareLayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface ImageAwareLayerInterface extends LayerInterface 10 | { 11 | const RESIZE_SHRINK = 'shrink'; 12 | const RESIZE_FILL_CROP = 'fill_crop'; 13 | 14 | /** 15 | * @param string $url 16 | * @param int|float $dataLimit 17 | * @param int|float $timeout 18 | * @return $this 19 | * @api 20 | */ 21 | public function http($url, $dataLimit = -1, $timeout = -1); 22 | 23 | /** 24 | * @param string $filename 25 | * @return $this 26 | * @api 27 | */ 28 | public function filename($filename); 29 | 30 | /** 31 | * @param string $contents 32 | * @return $this 33 | * @api 34 | */ 35 | public function contents($contents); 36 | 37 | /** 38 | * @param int $width 39 | * @param int $height 40 | * @param string $option 41 | * @return $this 42 | * @api 43 | */ 44 | public function resize($width, $height, $option = self::RESIZE_SHRINK); 45 | } 46 | -------------------------------------------------------------------------------- /src/Layer/ImageLayer.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class ImageLayer extends AbstractLayer implements ImageLayerInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function http($url, $dataLimit = -1, $timeout = -1) 15 | { 16 | $this->add([ 17 | 'image.http.url' => $url, 18 | 'image.http.data_limit' => $dataLimit, 19 | 'image.http.timeout' => $timeout, 20 | ]); 21 | 22 | return $this; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function filename($filename) 29 | { 30 | $this->set('image.filename', $filename); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function contents($contents) 39 | { 40 | $this->set('image.contents', $contents); 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function resize($width, $height, $option = ImageAwareLayerInterface::RESIZE_SHRINK) 49 | { 50 | $this->add([ 51 | 'image.resize.width' => $width, 52 | 'image.resize.height' => $height, 53 | 'image.resize.option' => $option, 54 | ]); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function move($x, $y, $gravity = RegularLayerInterface::MOVE_TOP_LEFT) 63 | { 64 | $this->add([ 65 | 'regular.move.x' => $x, 66 | 'regular.move.y' => $y, 67 | 'regular.move.gravity' => $gravity, 68 | ]); 69 | 70 | return $this; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Layer/ImageLayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface ImageLayerInterface extends RegularLayerInterface, ImageAwareLayerInterface 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Layer/LayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface LayerInterface extends ParameterBagInterface 10 | { 11 | /** 12 | * @return null|\Imagecraft\ImageBuilderInterface 13 | * @api 14 | */ 15 | public function done(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Layer/ParameterBag.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class ParameterBag implements ParameterBagInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | protected $parameters = []; 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | public function set($name, $value) 20 | { 21 | $this->parameters[$name] = $value; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function add(array $parameters) 28 | { 29 | $this->parameters = array_replace($this->parameters, $parameters); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function get($name) 36 | { 37 | return $this->has($name) ? $this->parameters[$name] : null; 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function has($name) 44 | { 45 | return array_key_exists($name, $this->parameters); 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function remove($name) 52 | { 53 | if ($this->has($name)) { 54 | unset($this->parameters[$name]); 55 | } 56 | } 57 | 58 | public function clear() 59 | { 60 | $this->parameters = []; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Layer/ParameterBagInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface ParameterBagInterface 10 | { 11 | /** 12 | * @param string $name 13 | * @param mixed $value 14 | */ 15 | public function set($name, $value); 16 | 17 | /** 18 | * @param mixed[] $parameters 19 | */ 20 | public function add(array $parameters); 21 | 22 | /** 23 | * @param string $name 24 | * @return mixed 25 | */ 26 | public function get($name); 27 | 28 | /** 29 | * @param string $name 30 | * @return bool 31 | */ 32 | public function has($name); 33 | 34 | /** 35 | * @param string $name 36 | */ 37 | public function remove($name); 38 | 39 | public function clear(); 40 | } 41 | -------------------------------------------------------------------------------- /src/Layer/RegularLayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface RegularLayerInterface extends LayerInterface 10 | { 11 | const MOVE_TOP_LEFT = 'top_left'; 12 | const MOVE_TOP_CENTER = 'top_center'; 13 | const MOVE_TOP_RIGHT = 'top_right'; 14 | const MOVE_CENTER_LEFT = 'center_left'; 15 | const MOVE_CENTER = 'center'; 16 | const MOVE_CENTER_RIGHT = 'center_right'; 17 | const MOVE_BOTTOM_LEFT = 'bottom_left'; 18 | const MOVE_BOTTOM_CENTER = 'bottom_center'; 19 | const MOVE_BOTTOM_RIGHT = 'bottom_right'; 20 | 21 | /** 22 | * @param int $x 23 | * @param int $y 24 | * @param string $gravity 25 | * @return $this 26 | * @api 27 | */ 28 | public function move($x, $y, $gravity = self::MOVE_TOP_LEFT); 29 | } 30 | -------------------------------------------------------------------------------- /src/Layer/TextLayer.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class TextLayer extends AbstractLayer implements TextLayerInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function font($filename, $size = 12, $color = '#FFF') 15 | { 16 | $this->add([ 17 | 'text.font.filename' => $filename, 18 | 'text.font.size' => $size, 19 | 'text.font.hex_color' => $color, 20 | ]); 21 | 22 | return $this; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function label($label) 29 | { 30 | $this->set('text.label', $label); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function angle($angle) 39 | { 40 | $this->set('text.angle', $angle); 41 | 42 | return $this; 43 | } 44 | 45 | public function lineSpacing($lineSpacing) 46 | { 47 | $this->set('text.line_spacing', $lineSpacing); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function box(array $paddings, $color = null) 56 | { 57 | $this->add([ 58 | 'text.box.paddings' => $paddings, 59 | 'text.box.hex_color' => $color, 60 | ]); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function move($x, $y, $gravity = RegularLayerInterface::MOVE_TOP_LEFT) 69 | { 70 | $this->add([ 71 | 'regular.move.x' => $x, 72 | 'regular.move.y' => $y, 73 | 'regular.move.gravity' => $gravity, 74 | ]); 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Layer/TextLayerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface TextLayerInterface extends RegularLayerInterface 10 | { 11 | /** 12 | * @param string $filename 13 | * @param int $size 14 | * @param string $color 15 | * @return $this 16 | * @api 17 | */ 18 | public function font($filename, $size = 12, $color = '#FFF'); 19 | 20 | /** 21 | * @param string $label 22 | * @return $this 23 | * @api 24 | */ 25 | public function label($label); 26 | 27 | /** 28 | * @param int $angle 29 | * @return $this 30 | * @api 31 | */ 32 | public function angle($angle); 33 | 34 | /** 35 | * @param float|int $lineSpacing 36 | * @return $this 37 | * @api 38 | */ 39 | public function lineSpacing($lineSpacing); 40 | 41 | /** 42 | * @param int[] $paddings 43 | * @param null|string $color 44 | * @return $this 45 | * @api 46 | */ 47 | public function box(array $paddings, $color = null); 48 | } 49 | -------------------------------------------------------------------------------- /src/LayerPass/AbstractLayerPass.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | abstract class AbstractLayerPass implements LayerPassInterface 12 | { 13 | /** 14 | * @param string $color 15 | * @return mixed[] 16 | * @throws InvalidArgumentException 17 | */ 18 | public function sanitizeHexColor($color) 19 | { 20 | if (!preg_match('/\\A#?([[:xdigit:]]{3}|[[:xdigit:]]{6})\\Z/', $color, $matches)) { 21 | throw new InvalidArgumentException( 22 | 'invalid.hex.color.%cp_invalid%.%example%', 23 | ['%cp_invalid%' => '"'.$color.'"', '%example%' => '"#CCC", "#F9F9F9"'] 24 | ); 25 | } 26 | 27 | $hex = strtoupper($matches[1]); 28 | if (3 === strlen($hex)) { 29 | $red = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); 30 | $green = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); 31 | $blue = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); 32 | } else { 33 | $red = hexdec(substr($hex, 0, 2)); 34 | $green = hexdec(substr($hex, 2, 2)); 35 | $blue = hexdec(substr($hex, 4, 2)); 36 | } 37 | 38 | return ['hex' => '#'.$hex, 'rgb' => [$red, $green, $blue]]; 39 | } 40 | 41 | /** 42 | * @param mixed $element 43 | * @param mixed[] $array 44 | * @return mixed 45 | * @throws InvalidArgumentException 46 | */ 47 | public function sanitizeEnumeration($element, array $array) 48 | { 49 | if (!in_array($element, $array)) { 50 | throw new InvalidArgumentException( 51 | 'unexpected.argument.%cp_unexpected%.%expected%', 52 | [ 53 | '%cp_unexpected%' => '"'.$element.'"', 54 | '%expected%' => implode(', ', preg_replace('/.+/', '"$0"', $array)), 55 | ] 56 | ); 57 | } 58 | 59 | return $element; 60 | } 61 | 62 | /** 63 | * @param string $url 64 | * @return string 65 | */ 66 | public function sanitizeUrl($url) 67 | { 68 | if (0 === preg_match("#https?://#", $url)) { 69 | $url = 'http://'.$url; 70 | } 71 | 72 | return $url; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/LayerPass/BackgroundLayerPass.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 1.0.0 10 | */ 11 | class BackgroundLayerPass extends AbstractLayerPass 12 | { 13 | /** 14 | * @inheritDoc 15 | */ 16 | public function process(array $layers) 17 | { 18 | if (!isset($layers[0])) { 19 | throw new BadMethodCallException('no.background.layer.added'); 20 | } 21 | 22 | return $layers; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LayerPass/DelegatingLayerPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class DelegatingLayerPass implements LayerPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $layers) 15 | { 16 | $passes = $this->getRegisteredPasses(); 17 | foreach ($passes as $pass) { 18 | $layers = $pass->process($layers); 19 | } 20 | 21 | return $layers; 22 | } 23 | 24 | /** 25 | * return LayerPassInterface[] 26 | */ 27 | protected function getRegisteredPasses() 28 | { 29 | return [ 30 | new BackgroundLayerPass(), 31 | new RegularLayerPass(), 32 | new ImageAwareLayerPass(), 33 | new TextLayerPass(), 34 | ]; 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/LayerPass/ImageAwareLayerPass.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 1.0.0 12 | */ 13 | class ImageAwareLayerPass extends AbstractLayerPass 14 | { 15 | /** 16 | * @inheritDoc 17 | */ 18 | public function process(array $layers) 19 | { 20 | foreach ($layers as $layer) { 21 | if (!($layer instanceof ImageAwareLayerInterface)) { 22 | continue; 23 | } 24 | 25 | $this->processResource($layer); 26 | $this->processResize($layer); 27 | } 28 | 29 | return $layers; 30 | } 31 | 32 | /** 33 | * @param ImageAwareLayerInterface $layer 34 | * @throws BadMethodCallException 35 | */ 36 | public function processResource(ImageAwareLayerInterface $layer) 37 | { 38 | if ($layer->has('image.http.url')) { 39 | $url = (string) $layer->get('image.http.url'); 40 | $url = $this->sanitizeUrl($url); 41 | $layer->set('image.http.url', $url); 42 | $limit = $layer->get('image.http.data_limit'); 43 | $limit = (0 < $limit) ? (float) $limit : -1; 44 | $layer->set('image.http.data_limit', $limit); 45 | $timeout = $layer->get('image.http.timeout'); 46 | $timeout = (0 < $timeout) ? (float) $timeout : -1; 47 | $layer->set('image.http.timeout', $timeout); 48 | } elseif ($layer->has('image.filename')) { 49 | $filename = (string) $layer->get('image.filename'); 50 | $layer->set('image.filename', $filename); 51 | } elseif ($layer->has('image.contents')) { 52 | $contents = (string) $layer->get('image.contents'); 53 | $layer->set('image.contents', $contents); 54 | } else { 55 | throw new BadMethodCallException('no.image.added'); 56 | } 57 | } 58 | 59 | /** 60 | * @param ImageAwareLayerInterface $layer 61 | */ 62 | public function processResize(ImageAwareLayerInterface $layer) 63 | { 64 | if ($layer->has('image.resize.width')) { 65 | $width = (int) $layer->get('image.resize.width'); 66 | $width = (0 >= $width) ? 1 : $width; 67 | $layer->set('image.resize.width', $width); 68 | 69 | $height = (int) $layer->get('image.resize.height'); 70 | $height = (0 >= $height) ? 1 : $height; 71 | $layer->set('image.resize.height', $height); 72 | 73 | $options = [ 74 | ImageAwareLayerInterface::RESIZE_SHRINK, 75 | ImageAwareLayerInterface::RESIZE_FILL_CROP, 76 | ]; 77 | $option = (string) $layer->get('image.resize.option'); 78 | $option = $this->sanitizeEnumeration($option, $options); 79 | $layer->set('image.resize.option', $option); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/LayerPass/LayerPassInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface LayerPassInterface 10 | { 11 | /** 12 | * @param \Imagecraft\Layer\LayerInterface[] $layers 13 | * @return \Imagecraft\Layer\LayerInterface[] 14 | */ 15 | public function process(array $layers); 16 | } 17 | -------------------------------------------------------------------------------- /src/LayerPass/RegularLayerPass.php: -------------------------------------------------------------------------------- 1 | 10 | * @since 1.0.0 11 | */ 12 | class RegularLayerPass extends AbstractLayerPass 13 | { 14 | /** 15 | * @inheritDoc 16 | */ 17 | public function process(array $layers) 18 | { 19 | foreach ($layers as $layer) { 20 | if (!($layer instanceof RegularLayerInterface)) { 21 | continue; 22 | } 23 | 24 | $this->processMove($layer); 25 | } 26 | 27 | return $layers; 28 | } 29 | 30 | /** 31 | * @param RegularLayerInterface $layer 32 | */ 33 | public function processMove(RegularLayerInterface $layer) 34 | { 35 | if (!$layer->has('regular.move.x')) { 36 | $layer->add([ 37 | 'regular.move.x' => 0, 38 | 'regular.move.y' => 0, 39 | 'regular.move.gravity' => RegularLayerInterface::MOVE_CENTER, 40 | ]); 41 | 42 | return; 43 | } 44 | 45 | $x = (int) $layer->get('regular.move.x'); 46 | $layer->set('regular.move.x', $x); 47 | 48 | $y = (int) $layer->get('regular.move.y'); 49 | $layer->set('regular.move.y', $y); 50 | 51 | $gravity = (string) $layer->get('regular.move.gravity'); 52 | $gravities = [ 53 | RegularLayerInterface::MOVE_TOP_LEFT, 54 | RegularLayerInterface::MOVE_TOP_CENTER, 55 | RegularLayerInterface::MOVE_TOP_RIGHT, 56 | RegularLayerInterface::MOVE_CENTER_LEFT, 57 | RegularLayerInterface::MOVE_CENTER, 58 | RegularLayerInterface::MOVE_CENTER_RIGHT, 59 | RegularLayerInterface::MOVE_BOTTOM_LEFT, 60 | RegularLayerInterface::MOVE_BOTTOM_CENTER, 61 | RegularLayerInterface::MOVE_BOTTOM_RIGHT, 62 | ]; 63 | $layer->set('regular.move.gravity', $gravity); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LayerPass/TextLayerPass.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 1.0.0 12 | */ 13 | class TextLayerPass extends AbstractLayerPass 14 | { 15 | /** 16 | * @inheritDoc 17 | */ 18 | public function process(array $layers) 19 | { 20 | foreach ($layers as $key => $layer) { 21 | if (!($layer instanceof TextLayerInterface)) { 22 | continue; 23 | } 24 | if ('' === $layer->get('text.label')) { 25 | unset($layers[$key]); 26 | continue; 27 | } 28 | 29 | $this->processFont($layer); 30 | $this->processLabel($layer); 31 | $this->processAngle($layer); 32 | $this->processLineSpacing($layer); 33 | $this->processBox($layer); 34 | } 35 | 36 | return $layers; 37 | } 38 | 39 | /** 40 | * @param TextLayerInterface $layer 41 | * @throws BadMethodCallException 42 | */ 43 | public function processFont(TextLayerInterface $layer) 44 | { 45 | if (!$layer->has('text.font.filename')) { 46 | throw new BadMethodCallException('no.font.added'); 47 | } 48 | 49 | $filename = (string) $layer->get('text.font.filename'); 50 | $layer->set('text.font.filename', $filename); 51 | 52 | $size = (int) $layer->get('text.font.size'); 53 | $size = (5 > $size) ? 5 : $size; 54 | $layer->set('text.font.size', $size); 55 | 56 | $color = (string) $layer->get('text.font.hex_color'); 57 | $color = $this->sanitizeHexColor($color); 58 | $layer->set('text.font.hex_color', $color['hex']); 59 | $layer->set('text.font.rgb_color', $color['rgb']); 60 | } 61 | 62 | /** 63 | * @param TextLayerInterface $layer 64 | */ 65 | public function processLabel(TextLayerInterface $layer) 66 | { 67 | if (!$layer->has('text.label')) { 68 | $label = ''; 69 | } else { 70 | $label = (string) $layer->get('text.label'); 71 | } 72 | $layer->set('text.label', $label); 73 | } 74 | 75 | /** 76 | * @param TextLayerInterface $layer 77 | */ 78 | public function processAngle(TextLayerInterface $layer) 79 | { 80 | if (!$layer->has('text.angle')) { 81 | $angle = 0; 82 | } else { 83 | $angle = (int) $layer->get('text.angle'); 84 | } 85 | $layer->set('text.angle', $angle); 86 | } 87 | 88 | /** 89 | * @param TextLayerInterface $layer 90 | */ 91 | public function processLineSpacing(TextLayerInterface $layer) 92 | { 93 | if (!$layer->has('text.line_spacing')) { 94 | $lineSpacing = 0.5; 95 | } else { 96 | $lineSpacing = (float) $layer->get('text.line_spacing'); 97 | } 98 | $layer->set('text.line_spacing', $lineSpacing); 99 | } 100 | 101 | /** 102 | * @param TextLayerInterface $layer 103 | */ 104 | public function processBox(TextLayerInterface $layer) 105 | { 106 | if (!$layer->has('text.box.paddings')) { 107 | $layer->add([ 108 | 'text.box.paddings' => [0, 0, 0, 0], 109 | 'text.box.hex_color' => null, 110 | ]); 111 | 112 | return; 113 | } 114 | 115 | $paddings = array_values($layer->get('text.box.paddings')); 116 | $arr = []; 117 | for ($i = 0; $i < 4; $i++) { 118 | if (!isset($paddings[$i]) || 0 > $paddings[$i]) { 119 | $arr[$i] = 0; 120 | } else { 121 | $arr[$i] = (int) $paddings[$i]; 122 | } 123 | } 124 | $layer->set('text.box.paddings', $arr); 125 | 126 | if (null !== $color = $layer->get('text.box.hex_color')) { 127 | $color = $this->sanitizeHexColor($color); 128 | $layer->set('text.box.hex_color', $color['hex']); 129 | $layer->set('text.box.rgb_color', $color['rgb']); 130 | } else { 131 | $layer->set('text.box.rgb_color', null); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/OptionPass/CacheDirOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class CacheDirOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['cache_dir'])) { 17 | $options['cache_dir'] = null; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/DebugOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class DebugOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['debug'])) { 17 | $options['debug'] = true; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/DelegatingOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class DelegatingOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | $passes = $this->getRegisteredOptionPasses(); 17 | foreach ($passes as $pass) { 18 | $options = $pass->process($options); 19 | } 20 | 21 | return $options; 22 | } 23 | 24 | /** 25 | * @return OptionPassInterface[] 26 | */ 27 | protected function getRegisteredOptionPasses() 28 | { 29 | return [ 30 | new CacheDirOptionPass(), 31 | new DebugOptionPass(), 32 | new EngineOptionPass(), 33 | new GifAnimationOptionPass(), 34 | new JpegQualityOptionPass(), 35 | new LocaleOptionPass(), 36 | new MemoryLimitOptionPass(), 37 | new OutputFormatOptionPass(), 38 | new PngCompressionOptionPass(), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OptionPass/EngineOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class EngineOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['engine'])) { 17 | $options['engine'] = 'php_gd'; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/GifAnimationOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class GifAnimationOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['gif_animation'])) { 17 | $options['gif_animation'] = true; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/JpegQualityOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class JpegQualityOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if ( 17 | !isset($options['jpeg_quality']) || 18 | $options['jpeg_quality'] > 100 || 19 | $options['jpeg_quality'] < 0 20 | ) { 21 | $options['jpeg_quality'] = 100; 22 | } 23 | 24 | return $options; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/OptionPass/LocaleOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class LocaleOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if ( 17 | !isset($options['locale']) || 18 | !file_exists(__DIR__.'/../Resources/translations/imagecraft.'.$options['locale'].'.xlf') 19 | ) { 20 | $options['locale'] = 'en'; 21 | } 22 | 23 | return $options; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OptionPass/MemoryLimitOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class MemoryLimitOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['memory_limit'])) { 17 | $options['memory_limit'] = -10; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/OptionPassInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | interface OptionPassInterface 10 | { 11 | /** 12 | * @param mixed[] $options 13 | * @return mixed[] 14 | */ 15 | public function process(array $options); 16 | } 17 | -------------------------------------------------------------------------------- /src/OptionPass/OutputFormatOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class OutputFormatOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if (!isset($options['output_format'])) { 17 | $options['output_format'] = 'default'; 18 | } 19 | 20 | return $options; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OptionPass/PngCompressionOptionPass.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0.0 8 | */ 9 | class PngCompressionOptionPass implements OptionPassInterface 10 | { 11 | /** 12 | * @inheritDoc 13 | */ 14 | public function process(array $options) 15 | { 16 | if ( 17 | !isset($options['png_compression']) || 18 | $options['png_compression'] > 100 || 19 | $options['png_compression'] < 0 20 | ) { 21 | $options['png_compression'] = 100; 22 | } 23 | 24 | return $options; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Resources/translations/imagecraft.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | invalid.hex.color.%cp_invalid%.%example% 10 | %cp_invalid% does not appear to be a valid hex color, here is an example: %example%. 11 | 12 | 13 | unexpected.argument.%cp_unexpected%.%expected% 14 | Expects argument to be %expected%, but received %cp_unexpected%. 15 | 16 | 17 | 18 | 19 | no.background.layer.added 20 | You haven't added a background layer yet. 21 | 22 | 23 | 24 | 25 | no.image.added 26 | You haven't added an image yet. 27 | 28 | 29 | 30 | 31 | no.font.added 32 | You haven't added a font yet. 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | image.corrupted 41 | The image file is corrputed and cannot be opened. 42 | 43 | 44 | image.process.error 45 | An error encountered while processing image. 46 | 47 | 48 | text.adding.error 49 | An error encountered while adding text to image. 50 | 51 | 52 | 53 | 54 | unsupported.image.format.or.file.corrupted.%unsupported%.%supported% 55 | The format %unsupported% is either not supported or the file is damaged. Server supports %supported%. 56 | 57 | 58 | unknown.image.format.or.file.corrupted.%supported% 59 | The image is either in unknown format or corrupted. Server supports %supported%. 60 | 61 | 62 | 63 | 64 | adding.text.not.supported 65 | Adding text to image is not supported by server. 66 | 67 | 68 | 69 | 70 | gd.extension.not.enabled 71 | PHP GD extension is not enabled. 72 | 73 | 74 | output.image.format.not.supported.%cp_unsupported% 75 | The output image format %cp_unsupported% is not supported. 76 | 77 | 78 | 79 | 80 | output.image.dimensions.exceed.limit.%cp_dimensions% 81 | The output image dimensions (%cp_dimensions%) is bigger than the server allows. 82 | 83 | 84 | image.dimensions.exceed.limit.%cp_dimensions% 85 | The image dimensions (%cp_dimensions%) exceed the server allowable limit. 86 | 87 | 88 | not.enough.memory.to.process.image 89 | There is not enough memory to process the image. 90 | 91 | 92 | 93 | 94 | gif.parse.error 95 | An error occured while parsing GIF file. 96 | 97 | 98 | 99 | 100 | 101 | gif.animation.may.lost.due.to.corrupted.frame.data 102 | GIF animation may be lost. The frame data may be damaged or corrupted. 103 | 104 | 105 | 106 | 107 | gif.animation.may.lost.as.too.many.frames.or.dimensions.too.large.%total_frames%.%dimensions% 108 | GIF animation may be lost, as it either has too many frames (%total_frames%) or dimensions (%dimensions%) are too large. 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/Resources/translations/imagecraft.zh_CN.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | invalid.hex.color.%cp_invalid%.%example% 10 | %cp_invalid% 不是一个正确的十六进制颜色,正确的格式如:%example%。 11 | 12 | 13 | unexpected.argument.%cp_unexpected%.%expected% 14 | 未知参数 %cp_unexpected%,正确的有 %expected%。 15 | 16 | 17 | 18 | 19 | no.background.layer.added 20 | 您还未添加背景图层。 21 | 22 | 23 | 24 | 25 | no.image.added 26 | 您还未为图层添加图片文件。 27 | 28 | 29 | 30 | 31 | no.font.added 32 | 您还未为图层添加字体文件。 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | image.corrupted 41 | 图片文件已损坏,请尝试上传一张新的图片。 42 | 43 | 44 | image.process.error 45 | 处理图片时发生错误 46 | 47 | 48 | text.adding.error 49 | 写入文字时发生错误。 50 | 51 | 52 | 53 | 54 | unsupported.image.format.or.file.corrupted.%unsupported%.%supported% 55 | %unsupported% 格式不被支持或文件已损坏,服务器支持的图片格式有:%supported%。 56 | 57 | 58 | unknown.image.format.or.file.corrupted.%supported% 59 | 图片格式未知或文件已损坏。 60 | 61 | 62 | 63 | 64 | adding.text.not.supported 65 | 添加文字功能不支持。 66 | 67 | 68 | 69 | 70 | gd.extension.not.enabled 71 | PHP GD 扩展还未开启。 72 | 73 | 74 | output.image.format.not.supported.%cp_unsupported% 75 | 输出的图片格式 %cp_unsupported% 无法支持。 76 | 77 | 78 | 79 | 80 | output.image.dimensions.exceed.limit.%cp_dimensions% 81 | 输出的图片尺寸 (%cp_dimensions%) 超出服务器可承受范围。 82 | 83 | 84 | image.dimensions.exceed.limit.%cp_dimensions% 85 | 图片尺寸 (%cp_dimensions%) 超出服务器可承受范围。 86 | 87 | 88 | not.enough.memory.to.process.image 89 | 没有足够的内存来处理您的图片。 90 | 91 | 92 | 93 | 94 | gif.parse.error 95 | 解析 GIF 文件时发生错误。 96 | 97 | 98 | 99 | 100 | 101 | gif.animation.may.lost.due.to.corrupted.frame.data 102 | GIF 动态效果丢失。文件已损坏或数据丢失。 103 | 104 | 105 | 106 | 107 | gif.animation.may.lost.as.too.many.frames.or.dimensions.too.large.%total_frames%.%dimensions% 108 | GIF 动态效果丢失,原因为帧数 (%total_frames%) 太多或图片尺寸 (%dimensions%) 太大。 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/Resources/translations/imagecraft.zh_TW.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | no.background.layer 10 | 還未添加背景圖層。 11 | 12 | 13 | 14 | 15 | invalid.hex.color.%invalid_color%.%example_color% 16 | 您輸入的顏色 %invalid_color% 不是一個合法的十六進制顏色,正確的格式如: %example_color%。 17 | 18 | 19 | unexpected.argument.%unexpected_argument%.%expected_arguments% 20 | 未知參數 %unexpected_argument%, 請從以下選擇一個可用的參數: %expected_arguments%。 21 | 22 | 23 | file.not.found.%filename% 24 | 無法找到文件 %filename%。 25 | 26 | 27 | file.access.denied.%filename% 28 | 您沒有權限打開文件 %filename%。 29 | 30 | 31 | url.fopen.not.enabled 32 | 服務器不支持 URL 打開文件。 33 | 34 | 35 | malformed.url.%url% 36 | 您輸入的 URL %url% 格式不正確。 37 | 38 | 39 | url.resource.not.found.%url% 40 | 無法從 URL %url% 獲取資源。 41 | 42 | 43 | 44 | 45 | no.image.added 46 | 您還未添加圖片文件。 47 | 48 | 49 | 50 | 51 | no.font.added 52 | 您還未添加字體文件。 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | unsupported.image.format.%unsupported_format%.%supported_formats% 61 | 圖片格式 %unsupported_format% 不支持,支持的格式有:%supported_formats%。 62 | 63 | 64 | unknown.image.format.or.file.corrupted.%supported_formats% 65 | 文件格式未知或者文件已損壞,服務器支持的圖片格式有:%supported_formats%。 66 | 67 | 68 | 69 | 70 | text.not.supported 71 | 服務器不支持寫入文字。 72 | 73 | 74 | 75 | 76 | gd.extension.not.enabled 77 | PHP GD 擴展還未安裝或啟動。 78 | 79 | 80 | saved.image.format.not.supported.%format% 81 | 保存的圖片格式 %format% 不支持。 82 | 83 | 84 | 85 | 86 | image.corrupted 87 | 圖片文件以損壞。 88 | 89 | 90 | processing.image.error 91 | 無法處理圖片。 92 | 93 | 94 | writing.text.error.%valid_font% 95 | 寫入文字時發生錯誤,原因可能為您所使用的字體文件不被支持。服務器支持的字體文件格式有:%valid_font%。 96 | 97 | 98 | 99 | 100 | 101 | gif.animation.lost.one.or.more.frames.corrupted 102 | GIF 動態效果丟失,原因為一個或多個幀的數據已損壞。 103 | 104 | 105 | not.animated.gif 106 | 非動態GIF。 107 | 108 | 109 | 110 | 111 | gif.animation.lost.too.many.frames.or.dimension.too.large.%frames%.%width%.%height% 112 | GIF 動態效果丟失,原因為帧數太多(%frames%) 或尺寸太大(%width% x %height%)。 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /tests/AbstractContextTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMockForAbstractClass('Imagecraft\\AbstractContext'); 15 | } 16 | 17 | public function testGetMemoryLimit() 18 | { 19 | $previous = ini_get('memory_limit'); 20 | 21 | ini_set('memory_limit', '40M'); 22 | $this->assertEquals(40 * 1024 * 1024, $this->context->getMemoryLimit()); 23 | 24 | ini_set('memory_limit', '-1'); 25 | $this->assertEquals(1024 * 1024 * 1024, $this->context->getMemoryLimit()); 26 | 27 | ini_set('memory_limit', '40M'); 28 | $this->assertEquals(35 * 1024 * 1024, $this->context->getMemoryLimit(-5)); 29 | 30 | ini_set('memory_limit', '40M'); 31 | $this->assertEquals(35 * 1024 * 1024, $this->context->getMemoryLimit(35)); 32 | 33 | ini_set('memory_limit', $previous); 34 | } 35 | 36 | /* 37 | * @requires extension fileinfo 38 | */ 39 | public function testIsFileinfoExtensionEnabled() 40 | { 41 | $this->assertTrue($this->context->isFileinfoExtensionEnabled()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Engine/DelegatingEngineTest.php: -------------------------------------------------------------------------------- 1 | engine = $this->getMock('Imagecraft\\Engine\\DelegatingEngine', ['getRegisteredEngines']); 17 | $this->engine 18 | ->method('getRegisteredEngines') 19 | ->will($this->returnValue(['foo' => new FooEngine()])) 20 | ; 21 | } 22 | 23 | public function testGetImage() 24 | { 25 | $this->assertEquals('foo', $this->engine->getImage([], ['engine' => 'foo'])); 26 | } 27 | 28 | public function testGetContext() 29 | { 30 | $this->assertEquals('bar', $this->engine->getContext(['engine' => 'foo'])); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Engine/Fixtures/FooEngine.php: -------------------------------------------------------------------------------- 1 | getMock('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface'); 13 | $dispatcher 14 | ->expects($this->atLeastOnce()) 15 | ->method('addSubscriber') 16 | ; 17 | $extension = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\CoreExtension', null); 18 | $extension->boot($dispatcher); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/BackgroundLayerListenerTest.php: -------------------------------------------------------------------------------- 1 | listener = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\EventListener\\BackgroundLayerListener', null); 19 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 20 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 21 | $this->event 22 | ->method('getLayers') 23 | ->will($this->returnValue([$this->layer])) 24 | ; 25 | } 26 | 27 | public function testInitFinalFormat() 28 | { 29 | $this->event 30 | ->method('getOptions') 31 | ->will($this->returnValue(['output_format' => 'default'])) 32 | ; 33 | $this->layer->add(['image.format' => 'png']); 34 | $this->listener->initFinalFormat($this->event); 35 | $this->assertInternalType('string', $this->layer->get('final.format')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/ImageAwareLayerListenerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', null); 22 | $info = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\ImageInfo', null, [$context]); 23 | $rh = $this->getMock('Imagecraft\\Engine\\PhpGd\\Helper\\ResourceHelper', null); 24 | $this->listener = $this->getMock( 25 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\ImageAwareLayerListener', 26 | null, 27 | [$info, $rh] 28 | ); 29 | $this->layer = $this->getMock('Imagecraft\\Layer\\ImageLayer', null); 30 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 31 | $this->event 32 | ->method('getLayers') 33 | ->will($this->returnValue([$this->layer])) 34 | ; 35 | } 36 | 37 | public function testInitImcUri() 38 | { 39 | if (!ini_get('allow_url_fopen') || !$r = @fsockopen('8.8.8.8', 53, $e, $r, 1)) { 40 | $this->markTestSkipped('No internet connection or allow_url_fopen is not enabled.'); 41 | } 42 | 43 | $this->layer->add([ 44 | 'image.http.url' => 'http://www.example.com', 45 | 'image.http.data_limit' => 500, 46 | 'image.http.timeout' => 20, 47 | ]); 48 | $this->listener->initImcUri($this->event); 49 | $this->assertNotEmpty($this->layer->get('image.imc_uri')); 50 | 51 | $this->layer->clear(); 52 | $this->layer->add(['image.filename' => __FILE__]); 53 | $this->listener->initImcUri($this->event); 54 | $this->assertNotEmpty($this->layer->get('image.imc_uri')); 55 | } 56 | 57 | public function testInitFilePointer() 58 | { 59 | if (!ini_get('allow_url_fopen') || !$r = @fsockopen('8.8.8.8', 53, $e, $r, 1)) { 60 | $this->markTestSkipped('No internet connection or allow_url_fopen is not enabled.'); 61 | } 62 | 63 | $this->layer->add(['image.imc_uri' => 'http://www.example.com']); 64 | $this->listener->initFilePointer($this->event); 65 | $this->assertInternalType('resource', $this->layer->get('image.fp')); 66 | 67 | $this->layer->clear(); 68 | $this->layer->add(['image.contents' => file_get_contents(__FILE__)]); 69 | $this->listener->initFilePointer($this->event); 70 | $this->assertInternalType('resource', $this->layer->get('image.fp')); 71 | } 72 | 73 | public function testInitImageInfo() 74 | { 75 | $this->layer->set('image.fp', fopen(__DIR__.'/../../../../../Fixtures/gif_87a_palette_250x297.gif', 'rb')); 76 | $this->listener->initImageInfo($this->event); 77 | $this->assertInternalType('integer', $this->layer->get('image.width')); 78 | $this->assertInternalType('integer', $this->layer->get('image.height')); 79 | $this->assertInternalType('string', $this->layer->get('image.format')); 80 | } 81 | 82 | public function testInitFinalDimensions() 83 | { 84 | $this->layer->add([ 85 | 'image.width' => 200, 86 | 'image.height' => 200, 87 | 'image.resize.width' => 100, 88 | 'image.resize.height' => 100, 89 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_FILL_CROP, 90 | ]); 91 | $this->listener->initFinalDimensions($this->event); 92 | $this->assertInternalType('integer', $this->layer->get('final.width')); 93 | $this->assertInternalType('integer', $this->layer->get('final.height')); 94 | } 95 | 96 | /** 97 | * @depends testInitFilePointer 98 | */ 99 | public function testTermFilePointer() 100 | { 101 | $this->layer->add(['image.contents' => file_get_contents(__FILE__)]); 102 | $this->listener->initFilePointer($this->event); 103 | $this->listener->termFilePointer($this->event); 104 | $this->assertFalse($this->layer->has('image.fp')); 105 | } 106 | 107 | /** 108 | * @depends testInitImcUri 109 | */ 110 | public function testTermImcUri() 111 | { 112 | if (!ini_get('allow_url_fopen') || !$r = @fsockopen('8.8.8.8', 53, $e, $r, 1)) { 113 | $this->markTestSkipped('No internet connection or allow_url_fopen is not enabled.'); 114 | } 115 | 116 | $this->layer->add([ 117 | 'image.http.url' => 'http://www.example.com', 118 | 'image.http.data_limit' => 500, 119 | 'image.http.timeout' => 20, 120 | ]); 121 | $this->listener->initImcUri($this->event); 122 | $this->listener->termImcUri($this->event); 123 | $this->assertFalse($this->layer->has('image.imc_uri')); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/ImageFactoryListenerTest.php: -------------------------------------------------------------------------------- 1 | factory = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\ImageFactory', [], [], '', false); 19 | $this->listener = $this->getMock( 20 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\ImageFactoryListener', 21 | null, 22 | [$this->factory] 23 | ); 24 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 25 | } 26 | 27 | public function testCreateImage() 28 | { 29 | $image = $this->getMock('Imagecraft\\Image', null); 30 | $this->factory 31 | ->expects($this->once()) 32 | ->method('createImage') 33 | ->will($this->returnValue($image)) 34 | ; 35 | $this->event 36 | ->expects($this->once()) 37 | ->method('setImage') 38 | ->with($image) 39 | ; 40 | $this->event 41 | ->expects($this->once()) 42 | ->method('getLayers') 43 | ->will($this->returnValue([])) 44 | ; 45 | $this->event 46 | ->expects($this->once()) 47 | ->method('getOptions') 48 | ->will($this->returnValue([])) 49 | ; 50 | $this->listener->createImage($this->event); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/ImageMetadataListenerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', null); 23 | $this->listener = $this->getMock( 24 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\ImageMetadataListener', 25 | null, 26 | [$context] 27 | ); 28 | $this->layer = $this->getMock('Imagecraft\\Layer\\ImageLayer', null); 29 | $this->image = $this->getMock('Imagecraft\\Image', null); 30 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 31 | $this->event 32 | ->method('getLayers') 33 | ->will($this->returnValue([$this->layer])) 34 | ; 35 | $this->event 36 | ->method('getImage') 37 | ->will($this->returnValue($this->image)) 38 | ; 39 | } 40 | 41 | public function testAddImageMetadatas() 42 | { 43 | $this->layer->add([ 44 | 'final.format' => PhpGdContext::FORMAT_JPEG, 45 | 'final.width' => 100, 46 | 'final.height' => 200, 47 | 'image.width' => 300, 48 | 'image.height' => 400, 49 | ]); 50 | $this->listener->addImageMetadatas($this->event); 51 | $this->assertInternalType('string', $this->image->getMime()); 52 | $this->assertInternalType('string', $this->image->getExtension()); 53 | $this->assertEquals(100, $this->image->getWidth()); 54 | $this->assertEquals(200, $this->image->getHeight()); 55 | $this->assertEquals(300, $this->image->getExtras()['original_width']); 56 | $this->assertEquals(400, $this->image->getExtras()['original_height']); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/MemoryRequirementListenerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', null); 23 | $this->listener = $this->getMock( 24 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\MemoryRequirementListener', 25 | null, 26 | [$context] 27 | ); 28 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 29 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 30 | $this->event 31 | ->method('getLayers') 32 | ->will($this->returnValue([$this->layer])) 33 | ; 34 | $this->image = $this->getMock('Imagecraft\\Image', null); 35 | $this->event 36 | ->method('getImage') 37 | ->will($this->returnValue($this->image)) 38 | ; 39 | } 40 | 41 | /** 42 | * @expectedException Imagecraft\Exception\RuntimeException 43 | */ 44 | public function testVerifyMemoryLimit() 45 | { 46 | $this->event 47 | ->method('getOptions') 48 | ->will($this->returnValue(30 * 1024 * 1024)) 49 | ; 50 | $this->layer->add([ 51 | 'image.format' => PhpGdContext::FORMAT_JPEG, 52 | 'image.width' => 1000000, 53 | 'image.height' => 1000000, 54 | 'final.width' => 200, 55 | 'final.height' => 200, 56 | ]); 57 | $this->listener->verifyMemoryLimit($this->event); 58 | } 59 | 60 | public function testAddImageExtras() 61 | { 62 | $this->event 63 | ->method('getOptions') 64 | ->will($this->returnValue(50 * 1024 * 1024)) 65 | ; 66 | $this->layer->add([ 67 | 'image.format' => PhpGdContext::FORMAT_JPEG, 68 | 'image.width' => 100, 69 | 'image.height' => 100, 70 | 'final.width' => 100, 71 | 'final.height' => 100, 72 | ]); 73 | $this->listener->verifyMemoryLimit($this->event); 74 | $this->listener->addImageExtras($this->event); 75 | $this->assertNotEmpty($this->image->getExtras()['memory_approx']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/SystemRequirementListenerTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', []); 19 | $this->listener = $this->getMock( 20 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\SystemRequirementListener', 21 | null, 22 | [$this->context] 23 | ); 24 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 25 | } 26 | 27 | /** 28 | * @expectedException Imagecraft\Exception\RuntimeException 29 | */ 30 | public function testVerifyEngine() 31 | { 32 | $this->context 33 | ->expects($this->once()) 34 | ->method('isEngineSupported') 35 | ->will($this->returnValue(false)) 36 | ; 37 | $this->listener->verifyEngine(); 38 | } 39 | 40 | /** 41 | * @expectedException Imagecraft\Exception\InvalidArgumentException 42 | */ 43 | public function testVerifySavedFormatWhenFormatIsInvalid() 44 | { 45 | $this->context 46 | ->expects($this->once()) 47 | ->method('isImageFormatSupported') 48 | ->will($this->returnValue(false)) 49 | ; 50 | $this->event 51 | ->expects($this->once()) 52 | ->method('getOptions') 53 | ->will($this->returnValue(['output_format' => 'foo'])) 54 | ; 55 | $this->listener->verifySavedFormat($this->event); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/EventListener/TextLayerListenerTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext'); 21 | $this->listener = $this->getMock( 22 | 'Imagecraft\\Engine\PhpGd\\Extension\\Core\\EventListener\\TextLayerListener', 23 | null, 24 | [$this->context] 25 | ); 26 | $this->layer = $this->getMock('Imagecraft\\Layer\\TextLayer', null); 27 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 28 | $this->event 29 | ->method('getLayers') 30 | ->will($this->returnValue([$this->layer])) 31 | ; 32 | } 33 | 34 | /** 35 | * @expectedException Imagecraft\Exception\RuntimeException 36 | */ 37 | public function testVerifyFreeType() 38 | { 39 | $this->context 40 | ->expects($this->once()) 41 | ->method('isFreeTypeSupported') 42 | ->will($this->returnValue(false)) 43 | ; 44 | $this->listener->verifyFreeType($this->event); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/ImageFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\Helper\\ResourceHelper', null); 29 | $this->factory = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\ImageFactory', null, [$rh]); 30 | } 31 | 32 | /** 33 | * @dataProvider layerDataProvider 34 | */ 35 | public function testCreateImage(array $layers, array $options, $outputName) 36 | { 37 | $image = $this->factory->createImage($layers, $options); 38 | file_put_contents(__DIR__.'/TestOutput/'.$outputName, $image->getContents()); 39 | } 40 | 41 | public function layerDataProvider() 42 | { 43 | $outputName1 = 'image_factory_image_should_be_valid_01.png'; 44 | $options1 = ['png_quality' => 100, 'jpeg_compression' => 100]; 45 | $layers1 = []; 46 | $layers1[0] = new BackgroundLayer(); 47 | $layers1[0]->add([ 48 | 'image.imc_uri' => __DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif', 49 | 'image.format' => PhpGdContext::FORMAT_GIF, 50 | 'image.width' => 339, 51 | 'image.height' => 473, 52 | 'image.resize.width' => 500, 53 | 'image.resize.height' => 500, 54 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_FILL_CROP, 55 | 'final.width' => 500, 56 | 'final.height' => 500, 57 | 'final.format' => PhpGdContext::FORMAT_PNG, 58 | ]); 59 | $layers1[1] = new ImageLayer(); 60 | $layers1[1]->add([ 61 | 'image.contents' => file_get_contents(__DIR__.'/../../../../Fixtures/gif_89a_palette_animated_375x225.gif'), 62 | 'image.width' => 375, 63 | 'image.height' => 225, 64 | 'image.format' => PhpGdContext::FORMAT_GIF, 65 | 'image.resize.width' => 50, 66 | 'image.resize.height' => 50, 67 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_FILL_CROP, 68 | 'regular.move.x' => 10, 69 | 'regular.move.y' => 10, 70 | 'regular.move.gravity' => RegularLayerInterface::MOVE_TOP_LEFT, 71 | ]); 72 | $layers1[2] = new TextLayer(); 73 | $layers1[2]->add([ 74 | 'text.font.filename' => __DIR__.'/../../../../Fixtures/pfa_truecolor_alpha.pfa', 75 | 'text.font.size' => 25, 76 | 'text.font.rgb_color' => [255, 255, 255], 77 | 'text.label' => 'Hello World', 78 | 'text.angle' => 30, 79 | 'text.lineSpacing' => 1, 80 | 'text.box.paddings' => [0, 0, 0, 0], 81 | 'regular.move.x' => 0, 82 | 'regular.move.y' => 0, 83 | 'regular.move.gravity' => RegularLayerInterface::MOVE_CENTER, 84 | ]); 85 | 86 | $outputName2 = 'image_factory_image_should_be_valid_02.gif'; 87 | $options2 = ['png_quality' => 100, 'jpeg_compression' => 100]; 88 | $layers2 = []; 89 | $layers2[0] = new BackgroundLayer(); 90 | $layers2[0]->add([ 91 | 'image.imc_uri' => __DIR__.'/../../../../Fixtures/jpeg_exif_truecolor_480x360.jpg', 92 | 'image.format' => PhpGdContext::FORMAT_JPEG, 93 | 'image.width' => 480, 94 | 'image.height' => 360, 95 | 'image.resize.width' => 432, 96 | 'image.resize.height' => 324, 97 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_SHRINK, 98 | 'final.width' => 432, 99 | 'final.height' => 324, 100 | 'final.format' => PhpGdContext::FORMAT_GIF, 101 | ]); 102 | $layers2[1] = new ImageLayer(); 103 | $layers2[1]->add([ 104 | 'image.contents' => file_get_contents(__DIR__.'/../../../../Fixtures/webp_vp8_lossy_truecolor_550x368.webp'), 105 | 'image.width' => 550, 106 | 'image.height' => 368, 107 | 'image.format' => PhpGdContext::FORMAT_WEBP, 108 | 'image.resize.width' => 50, 109 | 'image.resize.height' => 50, 110 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_FILL_CROP, 111 | 'regular.move.x' => 10, 112 | 'regular.move.y' => 10, 113 | 'regular.move.gravity' => RegularLayerInterface::MOVE_TOP_LEFT, 114 | ]); 115 | $layers2[2] = new TextLayer(); 116 | $layers2[2]->add([ 117 | 'text.font.filename' => __DIR__.'/../../../../Fixtures/pfa_truecolor_alpha.pfa', 118 | 'text.font.size' => 25, 119 | 'text.font.rgb_color' => [255, 255, 255], 120 | 'text.label' => 'Hello World', 121 | 'text.angle' => 30, 122 | 'text.lineSpacing' => 1, 123 | 'text.box.paddings' => [0, 0, 0, 0], 124 | 'regular.move.x' => 0, 125 | 'regular.move.y' => 0, 126 | 'regular.move.gravity' => RegularLayerInterface::MOVE_CENTER, 127 | ]); 128 | 129 | return [ 130 | [$layers1, $options1, $outputName1], 131 | [$layers2, $options2, $outputName2], 132 | ]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/ImageInfoTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', []); 19 | $this->info = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Core\\ImageInfo', null, [$this->context]); 20 | } 21 | 22 | /** 23 | * @dataProvider imageDataProvider 24 | */ 25 | public function testResolveFromFilePointer($filename, $format, $width, $height) 26 | { 27 | $this->context 28 | ->expects($this->atLeastOnce()) 29 | ->method('isImageFormatSupported') 30 | ->will($this->returnValue(true)) 31 | ; 32 | $fp = fopen($filename, 'rb'); 33 | $info = $this->info->resolveFromFilePointer($fp); 34 | $this->assertEquals($format, $info['format']); 35 | $this->assertEquals($width, $info['width']); 36 | $this->assertEquals($height, $info['height']); 37 | } 38 | 39 | /** 40 | * @depends testResolveFromFilePointer 41 | * @dataProvider imageDataProvider 42 | */ 43 | public function testResolveFromStream($filename, $format, $width, $height) 44 | { 45 | $this->context 46 | ->expects($this->atLeastOnce()) 47 | ->method('isImageFormatSupported') 48 | ->will($this->returnValue(true)) 49 | ; 50 | $info = $this->info->resolveFromStream($filename); 51 | $this->assertEquals($format, $info['format']); 52 | $this->assertEquals($width, $info['width']); 53 | $this->assertEquals($height, $info['height']); 54 | } 55 | 56 | /** 57 | * @depends testResolveFromFilePointer 58 | * @dataProvider imageDataProvider 59 | */ 60 | public function testResolveFromContents($filename, $format, $width, $height) 61 | { 62 | $this->context 63 | ->expects($this->atLeastOnce()) 64 | ->method('isImageFormatSupported') 65 | ->will($this->returnValue(true)) 66 | ; 67 | $info = $this->info->resolveFromContents(file_get_contents($filename)); 68 | $this->assertEquals($format, $info['format']); 69 | $this->assertEquals($width, $info['width']); 70 | $this->assertEquals($height, $info['height']); 71 | } 72 | 73 | /** 74 | * @requires extension fileinfo 75 | * @depends testResolveFromFilePointer 76 | * @expectedException Imagecraft\Exception\InvalidImageException 77 | */ 78 | public function testResolveInvalidImage() 79 | { 80 | $this->context 81 | ->expects($this->atLeastOnce()) 82 | ->method('isFileinfoExtensionEnabled') 83 | ->will($this->returnValue(true)) 84 | ; 85 | $fp = fopen(__FILE__, 'r'); 86 | $this->info->resolveFromFilePointer($fp); 87 | } 88 | 89 | /** 90 | * @depends testResolveFromFilePointer 91 | * @dataProvider invalidWebpDataProvider 92 | * @expectedException Imagecraft\Exception\InvalidImageException 93 | */ 94 | public function testResolveInvalidWebp($filename) 95 | { 96 | $this->info->resolveFromFilePointer(fopen($filename, 'rb')); 97 | } 98 | 99 | public function imageDataProvider() 100 | { 101 | return [ 102 | [__DIR__.'/../../../../Fixtures/webp_vp8_lossy_truecolor_550x368.webp', PhpGdContext::FORMAT_WEBP, 550, 368], 103 | [__DIR__.'/../../../../Fixtures/gif_87a_palette_250x297.gif', PhpGdContext::FORMAT_GIF, 250, 297], 104 | [__DIR__.'/../../../../Fixtures/png_truecolor_alpha_300x395.png', PhpGdContext::FORMAT_PNG, 300, 395], 105 | [__DIR__.'/../../../../Fixtures/png_palette_alpha_3000x1174.png', PhpGdContext::FORMAT_PNG, 3000, 1174], 106 | [__DIR__.'/../../../../Fixtures/jpeg_jfjf_truecolor_1920x758.jpg', PhpGdContext::FORMAT_JPEG, 1920, 758], 107 | [__DIR__.'/../../../../Fixtures/jpeg_jfjf_grayscale_480x361.jpg', PhpGdContext::FORMAT_JPEG, 480, 361], 108 | [__DIR__.'/../../../../Fixtures/jpeg_exif_truecolor_480x360.jpg', PhpGdContext::FORMAT_JPEG, 480, 360], 109 | [__DIR__.'/../../../../Fixtures/jpeg_jfjf_sos_truecolor_1920x1200.jpg', PhpGdContext::FORMAT_JPEG, 1920, 1200], 110 | ]; 111 | } 112 | 113 | public function invalidWebpDataProvider() 114 | { 115 | return [ 116 | [__DIR__.'/../../../../Fixtures/webp_vp8l_lossless_truecolor_alpha_800x600.webp'], 117 | [__DIR__.'/../../../../Fixtures/webp_vp8x_lossy_truecolor_alpha_421x163.webp'], 118 | ]; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Core/TestOutput/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/EventListener/GifExtractorListenerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtractor', null); 23 | $this->listener = $this->getMock( 24 | 'Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\EventListener\\GifExtractorListener', 25 | null, 26 | [$extractor] 27 | ); 28 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 29 | $this->layer->add(['final.format' => PhpGdContext::FORMAT_GIF, 'image.format' => PhpGdContext::FORMAT_GIF]); 30 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 31 | $this->event 32 | ->method('getLayers') 33 | ->will($this->returnValue([$this->layer])) 34 | ; 35 | $this->image = $this->getMock('Imagecraft\\Image', null); 36 | $this->event 37 | ->method('getImage') 38 | ->will($this->returnValue($this->image)) 39 | ; 40 | } 41 | 42 | /** 43 | * @dataProvider validAnimatedGifDataProvider 44 | */ 45 | public function testInitExtracted($filename) 46 | { 47 | $this->event 48 | ->method('getOptions') 49 | ->will($this->returnValue(['gif_animation' => true])) 50 | ; 51 | $this->layer->set('image.fp', fopen($filename, 'rb')); 52 | $this->listener->initExtracted($this->event); 53 | $this->assertTrue($this->layer->has('gif.extracted')); 54 | } 55 | 56 | /** 57 | * @dataProvider invalidAnimatedGifDataProvider 58 | */ 59 | public function testInitExtractedWhenGifIsInvalid($filename) 60 | { 61 | $this->event 62 | ->method('getOptions') 63 | ->will($this->returnValue(['gif_animation' => true])) 64 | ; 65 | $this->layer->set('image.fp', fopen($filename, 'rb')); 66 | $this->listener->initExtracted($this->event); 67 | $this->assertFalse($this->layer->has('gif.extracted')); 68 | } 69 | 70 | /** 71 | * @depends testInitExtractedWhenGifIsInvalid 72 | * @dataProvider corruptedAnimatedGifDataProvider 73 | */ 74 | public function testAddImageExtras($filename) 75 | { 76 | $this->event 77 | ->method('getOptions') 78 | ->will($this->returnValue(['gif_animation' => true])) 79 | ; 80 | $this->layer->set('image.fp', fopen($filename, 'rb')); 81 | $this->listener->initExtracted($this->event); 82 | $this->listener->addImageExtras($this->event); 83 | $this->assertNotEmpty($this->image->getExtras()['gif_error']); 84 | } 85 | 86 | public function validAnimatedGifDataProvider() 87 | { 88 | return [ 89 | [__DIR__.'/../../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif'], 90 | ]; 91 | } 92 | 93 | public function invalidAnimatedGifDataProvider() 94 | { 95 | return [ 96 | [__DIR__.'/../../../../../Fixtures/gif_87a_palette_250x297.gif'], 97 | [__DIR__.'/../../../../../Fixtures/gif_89a_palette_alpha_206x205.gif'], 98 | [__DIR__.'/../../../../../Fixtures/zz_gif_89a_palette_animated_corrupted_data_153x120.gif'], 99 | [__DIR__.'/../../../../../Fixtures/jpeg_jfjf_truecolor_1920x758.jpg'], 100 | ]; 101 | } 102 | 103 | public function corruptedAnimatedGifDataProvider() 104 | { 105 | return [ 106 | [__DIR__.'/../../../../../Fixtures/zz_gif_89a_palette_animated_corrupted_data_153x120.gif'], 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/EventListener/ImageFactoryListenerTest.php: -------------------------------------------------------------------------------- 1 | factory = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\ImageFactory', [], [], '', false); 23 | $this->listener = $this->getMock( 24 | 'Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\EventListener\\ImageFactoryListener', 25 | null, 26 | [$this->factory] 27 | ); 28 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 29 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 30 | $this->event 31 | ->method('getLayers') 32 | ->will($this->returnValue([$this->layer])) 33 | ; 34 | $this->image = $this->getMock('Imagecraft\\Image', null); 35 | $this->event 36 | ->method('getImage') 37 | ->will($this->returnValue($this->image)) 38 | ; 39 | } 40 | 41 | public function testCreateImage() 42 | { 43 | $this->event 44 | ->expects($this->once()) 45 | ->method('getOptions') 46 | ->will($this->returnValue([])) 47 | ; 48 | $this->factory 49 | ->expects($this->once()) 50 | ->method('createImage') 51 | ->will($this->returnValue($this->image)) 52 | ; 53 | $this->event 54 | ->expects($this->once()) 55 | ->method('setImage') 56 | ->with($this->image) 57 | ; 58 | $this->layer->add(['gif.extracted' => []]); 59 | $this->listener->createImage($this->event); 60 | } 61 | 62 | public function testAddImageExtras() 63 | { 64 | $this->event 65 | ->expects($this->once()) 66 | ->method('getOptions') 67 | ->will($this->returnValue([])) 68 | ; 69 | $this->factory 70 | ->expects($this->once()) 71 | ->method('createImage') 72 | ->will($this->throwException(new \Exception())) 73 | ; 74 | $this->layer->add(['gif.extracted' => []]); 75 | $this->listener->createImage($this->event); 76 | $this->listener->addImageExtras($this->event); 77 | $this->assertNotEmpty($this->image->getExtras()['gif_error']); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/EventListener/MemoryRequirementListenerTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', null); 23 | $this->listener = $this->getMock( 24 | 'Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\EventListener\\MemoryRequirementListener', 25 | null, 26 | [$this->context] 27 | ); 28 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 29 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 30 | $this->event 31 | ->method('getLayers') 32 | ->will($this->returnValue([$this->layer])) 33 | ; 34 | $this->image = $this->getMock('Imagecraft\\Image', null); 35 | $this->event 36 | ->method('getImage') 37 | ->will($this->returnValue($this->image)) 38 | ; 39 | } 40 | 41 | public function testVerifyMemoryLimitWhenImageIsCompatible() 42 | { 43 | $this->event 44 | ->expects($this->once()) 45 | ->method('getOptions') 46 | ->will($this->returnValue(['memory_limit' => '120'])) 47 | ; 48 | $this->layer->add([ 49 | 'gif.extracted' => [0, 0, 0, 0], 50 | 'image.width' => 100, 51 | 'image.height' => 100, 52 | 'final.width' => 10, 53 | 'final.height' => 10, 54 | ]); 55 | $this->listener->verifyMemoryLimit($this->event); 56 | $this->listener->addImageExtras($this->event); 57 | $this->assertArrayHasKey('memory_approx', $this->image->getExtras()); 58 | } 59 | 60 | public function testVerifyMemoryLimitWhenImageIsIncompatible() 61 | { 62 | $this->event 63 | ->expects($this->once()) 64 | ->method('getOptions') 65 | ->will($this->returnValue(['memory_limit' => '120'])) 66 | ; 67 | $this->layer->add([ 68 | 'gif.extracted' => [0, 0, 0, 0], 69 | 'image.width' => 10000000000000000000000, 70 | 'image.height' => 10000000000000000000000, 71 | 'final.width' => 10, 72 | 'final.height' => 10, 73 | ]); 74 | $this->listener->verifyMemoryLimit($this->event); 75 | $this->listener->addImageExtras($this->event); 76 | $this->assertArrayHasKey('gif_error', $this->image->getExtras()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifBuilderPlusTest.php: -------------------------------------------------------------------------------- 1 | extractor = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtractor', null); 17 | $this->builder = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifBuilderPlus', null); 18 | } 19 | 20 | /** 21 | * @dataProvider gifDataProvider 22 | */ 23 | public function testGetContents($filename, $outputName) 24 | { 25 | $extracted = $this->extractor->extractFromStream($filename); 26 | 27 | $this->builder 28 | ->canvasWidth($extracted->getCanvasWidth()) 29 | ->canvasHeight($extracted->getCanvasHeight()) 30 | ->loop($extracted->getTotalLoops()) 31 | ; 32 | for ($i = 0; $i < count($extracted); $i++) { 33 | $this->builder->addFrame(); 34 | $this->builder 35 | ->imageWidth($extracted->getImageWidth()) 36 | ->imageHeight($extracted->getImageHeight()) 37 | ->imageLeft($extracted->getImageLeft()) 38 | ->imageTop($extracted->getImageTop()) 39 | ->dispose($extracted->getDisposalMethod()) 40 | ->delayTime($extracted->getDelayTime()) 41 | ->interlaceFlag($extracted->getInterlaceFlag()) 42 | ->colorTable($extracted->getColorTable()) 43 | ->imageData($extracted->getImageData()) 44 | ; 45 | if ($extracted->getTransparentColorFlag()) { 46 | $this->builder->transparentColorIndex($extracted->getTransparentColorIndex()); 47 | } 48 | $extracted->next(); 49 | } 50 | file_put_contents(__DIR__.'/TestOutput/'.$outputName, $this->builder->getContents()); 51 | } 52 | 53 | public function gifDataProvider() 54 | { 55 | return [ 56 | [__DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif', 'gif_builder_plus_image_should_be_animated_01.gif'], 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifBuilderTest.php: -------------------------------------------------------------------------------- 1 | extractor = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtractor', null); 15 | $this->builder = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifBuilder', null); 16 | } 17 | 18 | /** 19 | * @dataProvider gifDataProvider 20 | */ 21 | public function testGetContents($filename, $outputName) 22 | { 23 | @mkdir(__DIR__.'/TestOutput/'.$outputName); 24 | $extracted = $this->extractor->extractFromStream($filename); 25 | foreach ($extracted as $key => $frame) { 26 | $this->builder 27 | ->imageWidth($extracted->getImageWidth()) 28 | ->imageHeight($extracted->getImageHeight()) 29 | ->colorTable($extracted->getColorTable()) 30 | ->interlaceFlag($extracted->getInterlaceFlag()) 31 | ->imageData($extracted->getImageData()) 32 | ; 33 | if ($extracted->getTransparentColorFlag()) { 34 | $index = $extracted->getTransparentColorIndex(); 35 | $this->builder->transparentColorIndex($index); 36 | } 37 | $contents = $this->builder->getContents(); 38 | file_put_contents(__DIR__.'/TestOutput/'.$outputName.'/'.$key.'.gif', $contents); 39 | } 40 | } 41 | 42 | public function gifDataProvider() 43 | { 44 | return [ 45 | [__DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif', 'gif_builder_frames_should_be_valid_01'], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifExtensionTest.php: -------------------------------------------------------------------------------- 1 | getMock('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface'); 13 | $dispatcher 14 | ->expects($this->atLeastOnce()) 15 | ->method('addSubscriber') 16 | ; 17 | $extension = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtension', null); 18 | $extension->boot($dispatcher); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifExtractedTest.php: -------------------------------------------------------------------------------- 1 | extractor = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtractor', null); 15 | } 16 | 17 | /** 18 | * @dataProvider gifDataProvider 19 | */ 20 | public function testGettersForAnimatedGif($filename, $specs) 21 | { 22 | $fp = fopen($filename, 'rb'); 23 | $extracted = $this->extractor->extractFromFilePointer($fp); 24 | $this->assertTrue($extracted->isValid()); 25 | $this->assertTrue($extracted->isAnimated()); 26 | 27 | foreach ($extracted as $key => $frame) { 28 | if (0 === $key) { 29 | continue; 30 | } 31 | $this->assertNotEmpty($extracted->getImageData()); 32 | $this->assertNotEmpty($extracted->getColorTable()); 33 | } 34 | 35 | $extracted->seek(2); 36 | $this->assertEquals($specs['total_frames'], count($extracted)); 37 | $this->assertEquals($specs['canvas_width'], $extracted->getCanvasWidth()); 38 | $this->assertEquals($specs['canvas_height'], $extracted->getCanvasHeight()); 39 | $this->assertEquals($specs['global_flag'], $extracted->getGlobalColorTableFlag()); 40 | if ($specs['global_flag']) { 41 | $this->assertEquals($specs['total_globals'], $extracted->getTotalGlobalColors()); 42 | } 43 | $this->assertEquals($specs['total_loops'], $extracted->getTotalLoops()); 44 | $this->assertEquals($specs['disposal_method'], $extracted->getDisposalMethod()); 45 | $this->assertEquals($specs['transparent_flag'], $extracted->getTransparentColorFlag()); 46 | if ($specs['transparent_flag']) { 47 | $this->assertEquals($specs['transparent_index'], $extracted->getTransparentColorIndex()); 48 | } 49 | $this->assertEquals($specs['delay_time'], $extracted->getDelayTime()); 50 | $this->assertEquals($specs['image_left'], $extracted->getImageLeft()); 51 | $this->assertEquals($specs['image_top'], $extracted->getImageTop()); 52 | $this->assertEquals($specs['image_width'], $extracted->getImageWidth()); 53 | $this->assertEquals($specs['image_height'], $extracted->getImageHeight()); 54 | $this->assertEquals($specs['local_flag'], $extracted->getLocalColorTableFlag()); 55 | if ($specs['local_flag']) { 56 | $this->assertEquals($specs['total_locals'], $extracted->getTotalLocalColors()); 57 | } 58 | $this->assertEquals($specs['interlace_flag'], $extracted->getInterlaceFlag()); 59 | $this->assertNotEmpty($extracted->getLinkedKey()); 60 | $this->assertNotEmpty($extracted->getLinkedDisposalMethod()); 61 | } 62 | 63 | public function gifDataProvider() 64 | { 65 | $filename1 = __DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif'; 66 | $specs1 = [ 67 | 'total_frames' => 19, 68 | 'canvas_width' => 339, 69 | 'canvas_height' => 473, 70 | 'global_flag' => true, 71 | 'total_globals' => 256, 72 | 'total_loops' => 0, 73 | 'disposal_method' => 2, 74 | 'transparent_flag' => true, 75 | 'transparent_index' => 53, 76 | 'delay_time' => 10, 77 | 'image_left' => 0, 78 | 'image_top' => 0, 79 | 'image_width' => 339, 80 | 'image_height' => 473, 81 | 'local_flag' => false, 82 | 'interlace_flag' => false, 83 | ]; 84 | return [ 85 | [$filename1, $specs1], 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifExtractor', null); 15 | } 16 | 17 | /** 18 | * @dataProvider imageDataProvider 19 | */ 20 | public function testExtractFromFilePointer($filename, $validity) 21 | { 22 | $fp = fopen($filename, 'rb'); 23 | $extracted = $this->extractor->extractFromFilePointer($fp); 24 | $this->assertEquals($validity, $extracted->isValid()); 25 | } 26 | 27 | /** 28 | * @depends testExtractFromFilePointer 29 | * @dataProvider imageDataProvider 30 | */ 31 | public function testExtractFromStream($filename, $validity) 32 | { 33 | $extracted = $this->extractor->extractFromStream($filename, $validity); 34 | $this->assertEquals($validity, $extracted->isValid()); 35 | } 36 | 37 | /** 38 | * @depends testExtractFromFilePointer 39 | * @dataProvider imageDataProvider 40 | */ 41 | public function testExtractFromContents($filename, $validity) 42 | { 43 | $extracted = $this->extractor->extractFromContents(file_get_contents($filename), $validity); 44 | $this->assertEquals($validity, $extracted->isValid()); 45 | } 46 | 47 | public function imageDataProvider() 48 | { 49 | return [ 50 | [__DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif', true], 51 | [__DIR__.'/../../../../Fixtures/gif_87a_palette_250x297.gif', true], 52 | [__DIR__.'/../../../../Fixtures/zz_gif_89a_palette_animated_no_graphic_control_550x296.gif', true], 53 | [__DIR__.'/../../../../Fixtures/zz_gif_89a_palette_animated_corrupted_data_153x120.gif', false], 54 | [__DIR__.'/../../../../Fixtures/png_palette_alpha_3000x1174.png', false], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/GifOptimizerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\Helper\\ResourceHelper', null); 17 | $optimizer = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Gif\\GifOptimizer', null, [$rh]); 18 | $resource1 = imagecreatefromjpeg($image); 19 | $resource2 = imagecreatefromjpeg($image); 20 | $resource = $optimizer->getOptimizedGdResource($resource1, $resource2); 21 | imagegif($resource, __DIR__.'/TestOutput/'.$outputName); 22 | imagedestroy($resource); 23 | imagedestroy($resource2); 24 | } 25 | 26 | public function imageDataProvider() 27 | { 28 | return [ 29 | [__DIR__.'/../../../../Fixtures/jpeg_exif_truecolor_480x360.jpg', 'gif_optimizer_image_should_be_blank_01.gif'], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Gif/TestOutput/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Save/EventListener/ImageFactoryListenerTest.php: -------------------------------------------------------------------------------- 1 | factory = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Save\\ImageFactory', [], [], '', false); 21 | $this->listener = $this->getMock( 22 | 'Imagecraft\\Engine\PhpGd\\Extension\\Save\\EventListener\\ImageFactoryListener', 23 | null, 24 | [$this->factory] 25 | ); 26 | $this->layer = $this->getMock('Imagecraft\\Layer\\ImageLayer', null); 27 | $this->event = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdEvent', [], [], '', false); 28 | $this->event 29 | ->method('getLayers') 30 | ->will($this->returnValue([$this->layer])) 31 | ; 32 | $this->event 33 | ->expects($this->once()) 34 | ->method('getOptions') 35 | ->will($this->returnValue([])) 36 | ; 37 | } 38 | 39 | public function testCreateImage() 40 | { 41 | $this->layer->add([ 42 | 'image.width' => 10, 43 | 'image.height' => 100, 44 | 'final.width' => 10, 45 | 'final.height' => 100, 46 | 'image.format' => 'foo', 47 | 'final.format' => 'foo', 48 | ]); 49 | $image = $this->getMock('Imagecraft\\Image', null); 50 | $this->factory 51 | ->expects($this->once()) 52 | ->method('createImage') 53 | ->will($this->returnValue($image)) 54 | ; 55 | $this->event 56 | ->expects($this->once()) 57 | ->method('setImage') 58 | ->with($image) 59 | ; 60 | $this->listener->createImage($this->event); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Save/ImageFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\Engine\\PhpGd\\Helper\\ResourceHelper', null); 24 | $this->factory = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Save\\ImageFactory', null, [$rh]); 25 | } 26 | 27 | /** 28 | * @dataProvider layerDataProvider 29 | */ 30 | public function testCreateImage(array $layers, array $options, $outputName) 31 | { 32 | $image = $this->factory->createImage($layers, $options); 33 | file_put_contents(__DIR__.'/TestOutput/'.$outputName, $image->getContents()); 34 | } 35 | 36 | public function layerDataProvider() 37 | { 38 | $outputName1 = 'image_factory_image_should_be_valid_01.gif'; 39 | $options1 = []; 40 | $layers1 = []; 41 | $layers1[0] = new BackgroundLayer(); 42 | $layers1[0]->add([ 43 | 'image.imc_uri' => __DIR__.'/../../../../Fixtures/gif_89a_palette_alpha_animated_339x473.gif', 44 | 'image.format' => PhpGdContext::FORMAT_GIF, 45 | ]); 46 | 47 | $outputName2 = 'image_factory_image_should_be_valid_02.jpg'; 48 | $options2 = []; 49 | $layers2 = []; 50 | $layers2[0] = new BackgroundLayer(); 51 | $layers2[0]->add([ 52 | 'image.imc_uri' => __DIR__.'/../../../../Fixtures/jpeg_exif_truecolor_480x360.jpg', 53 | 'image.format' => PhpGdContext::FORMAT_JPEG, 54 | ]); 55 | 56 | return [ 57 | [$layers1, $options1, $outputName1], 58 | [$layers2, $options2, $outputName2], 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Save/SaveExtensionTest.php: -------------------------------------------------------------------------------- 1 | getMock('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface'); 13 | $dispatcher 14 | ->expects($this->atLeastOnce()) 15 | ->method('addSubscriber') 16 | ; 17 | $extension = $this->getMock('Imagecraft\\Engine\\PhpGd\\Extension\\Save\\SaveExtension', null); 18 | $extension->boot($dispatcher); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Extension/Save/TestOutput/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/Helper/TestOutput/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Engine/PhpGd/PhpGdContextTest.php: -------------------------------------------------------------------------------- 1 | context = $this->getMock('Imagecraft\\Engine\\PhpGd\\PhpGdContext', null); 22 | } 23 | 24 | public function testIsImageFormatSupported() 25 | { 26 | $this->assertTrue($this->context->isImageFormatSupported(PhpGdContext::FORMAT_WEBP)); 27 | $this->assertTrue($this->context->isImageFormatSupported(PhpGdContext::FORMAT_PNG)); 28 | $this->assertTrue($this->context->isImageFormatSupported(PhpGdContext::FORMAT_JPEG)); 29 | $this->assertTrue($this->context->isImageFormatSupported(PhpGdContext::FORMAT_GIF)); 30 | $this->assertFalse($this->context->isImageFormatSupported('foo')); 31 | } 32 | 33 | public function testGetSupportedImageFormatsToString() 34 | { 35 | $this->assertEquals('"WEBP (VP8)", "PNG", "JPEG", "GIF"', $this->context->getSupportedImageFormatsToString()); 36 | } 37 | 38 | public function testGetImageMime() 39 | { 40 | $this->assertEquals('image/webp', $this->context->getImageMime(PhpGdContext::FORMAT_WEBP)); 41 | } 42 | 43 | public function testGetImageExtension() 44 | { 45 | $this->assertEquals('webp', $this->context->getImageExtension(PhpGdContext::FORMAT_WEBP)); 46 | } 47 | 48 | public function testIsEngineSupported() 49 | { 50 | $this->assertTrue($this->context->isEngineSupported()); 51 | } 52 | 53 | public function testIsFreeTypeSupported() 54 | { 55 | $this->assertTrue($this->context->isFreeTypeSupported()); 56 | } 57 | 58 | public function testGetSupportedFontFormatsToString() 59 | { 60 | $this->assertInternalType('string', $this->context->getSupportedFontFormatsToString()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Fixtures/gif_87a_palette_250x297.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/gif_87a_palette_250x297.gif -------------------------------------------------------------------------------- /tests/Fixtures/gif_89a_palette_alpha_206x205.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/gif_89a_palette_alpha_206x205.gif -------------------------------------------------------------------------------- /tests/Fixtures/gif_89a_palette_alpha_animated_339x473.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/gif_89a_palette_alpha_animated_339x473.gif -------------------------------------------------------------------------------- /tests/Fixtures/gif_89a_palette_animated_375x225.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/gif_89a_palette_animated_375x225.gif -------------------------------------------------------------------------------- /tests/Fixtures/jpeg_exif_truecolor_480x360.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/jpeg_exif_truecolor_480x360.jpg -------------------------------------------------------------------------------- /tests/Fixtures/jpeg_jfjf_grayscale_480x361.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/jpeg_jfjf_grayscale_480x361.jpg -------------------------------------------------------------------------------- /tests/Fixtures/jpeg_jfjf_sos_truecolor_1920x1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/jpeg_jfjf_sos_truecolor_1920x1200.jpg -------------------------------------------------------------------------------- /tests/Fixtures/jpeg_jfjf_truecolor_1920x758.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/jpeg_jfjf_truecolor_1920x758.jpg -------------------------------------------------------------------------------- /tests/Fixtures/pfa_truecolor_alpha.pfa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/pfa_truecolor_alpha.pfa -------------------------------------------------------------------------------- /tests/Fixtures/png_palette_alpha_3000x1174.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/png_palette_alpha_3000x1174.png -------------------------------------------------------------------------------- /tests/Fixtures/png_truecolor_alpha_300x395.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/png_truecolor_alpha_300x395.png -------------------------------------------------------------------------------- /tests/Fixtures/webp_vp8_lossy_truecolor_550x368.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/webp_vp8_lossy_truecolor_550x368.webp -------------------------------------------------------------------------------- /tests/Fixtures/webp_vp8l_lossless_truecolor_alpha_800x600.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/webp_vp8l_lossless_truecolor_alpha_800x600.webp -------------------------------------------------------------------------------- /tests/Fixtures/webp_vp8x_lossy_truecolor_alpha_421x163.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/webp_vp8x_lossy_truecolor_alpha_421x163.webp -------------------------------------------------------------------------------- /tests/Fixtures/zz_gif_89a_palette_animated_corrupted_data_153x120.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/zz_gif_89a_palette_animated_corrupted_data_153x120.gif -------------------------------------------------------------------------------- /tests/Fixtures/zz_gif_89a_palette_animated_no_graphic_control_550x296.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldume/imagecraft/2c9ab5174ebae6733673f14b01a776c1eda66489/tests/Fixtures/zz_gif_89a_palette_animated_no_graphic_control_550x296.gif -------------------------------------------------------------------------------- /tests/ImageBuilderTest.php: -------------------------------------------------------------------------------- 1 | getMock('Imagecraft\\ImageBuilder', null, [['engine' => 'php_gd']]); 22 | $context = $builder->about(); 23 | $this->assertInstanceOf('Imagecraft\\AbstractContext', $context); 24 | $image = $builder 25 | ->addBackgroundLayer() 26 | ->filename(__DIR__.'/Fixtures/jpeg_exif_truecolor_480x360.jpg') 27 | ->resize(400, 400, 'shrink') 28 | ->done() 29 | ->addImageLayer() 30 | ->filename(__DIR__.'/Fixtures/gif_87a_palette_250x297.gif') 31 | ->resize(100, 100) 32 | ->move(100, 100, 'top_left') 33 | ->done() 34 | ->addTextLayer() 35 | ->font(__DIR__.'/Fixtures/pfa_truecolor_alpha.pfa', 12, '#FFF') 36 | ->label('Hello World') 37 | ->box([0, 0, 0, 0], '#000') 38 | ->done() 39 | ->save() 40 | ; 41 | $this->assertTrue($image->isValid()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/LayerPass/AbstractLayerPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMockForAbstractClass('Imagecraft\\LayerPass\\AbstractLayerPass'); 15 | } 16 | 17 | public function testSanitizeHexColor() 18 | { 19 | $this->assertEquals(['hex' => '#000', 'rgb' => [0, 0, 0]], $this->pass->sanitizeHexColor('000')); 20 | $this->assertEquals(['hex' => '#C0C0C0', 'rgb' => [192, 192, 192]], $this->pass->sanitizeHexColor('#c0c0c0')); 21 | } 22 | 23 | /** 24 | * @expectedException Imagecraft\Exception\InvalidArgumentException 25 | */ 26 | public function testSanitizeHexColorWhenColorIsInvalid() 27 | { 28 | $this->pass->sanitizeHexColor('foo'); 29 | } 30 | 31 | public function testSanitizeEnumeration() 32 | { 33 | $this->assertEquals(1, $this->pass->sanitizeEnumeration(1, [1, 2, 3])); 34 | } 35 | 36 | /** 37 | * @expectedException Imagecraft\Exception\InvalidArgumentException 38 | */ 39 | public function testSanitizeEnumerationWhenEnumerationIsInvalid() 40 | { 41 | $this->pass->sanitizeEnumeration(1, [2, 3, 4]); 42 | } 43 | 44 | public function testSanitizeUrl() 45 | { 46 | $this->assertEquals('http://www.example.com', $this->pass->sanitizeUrl('www.example.com')); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/LayerPass/BackgroundLayerPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\LayerPass\\BackgroundLayerPass', null); 17 | $this->layer = $this->getMock('Imagecraft\\Layer\\BackgroundLayer', null); 18 | } 19 | 20 | /** 21 | * @expectedException Imagecraft\Exception\BadMethodCallException 22 | */ 23 | public function testProcessWhenNoBackgroundLayerIsFound() 24 | { 25 | $this->pass->process([]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/LayerPass/ImageAwareLayerPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\LayerPass\\ImageAwareLayerPass', null); 19 | $this->layer = $this->getMock('Imagecraft\\Layer\\ImageLayer', null); 20 | } 21 | 22 | public function testProcessResource() 23 | { 24 | $this->layer->add(['image.http.url' => 'www.example.com', 'image.http.data_limit' => -10, 'timeout' => -20]); 25 | $this->pass->processResource($this->layer); 26 | $this->assertEquals($this->layer->get('image.http.url'), 'http://www.example.com'); 27 | $this->assertEquals($this->layer->get('image.http.data_limit'), -1); 28 | $this->assertEquals($this->layer->get('image.http.timeout'), -1); 29 | 30 | $this->layer->clear(); 31 | $this->layer->set('image.filename', 'foo'); 32 | $this->pass->processResource($this->layer); 33 | $this->assertEquals($this->layer->get('image.filename'), 'foo'); 34 | 35 | $this->layer->clear(); 36 | $this->layer->set('image.contents', 'foo'); 37 | $this->pass->processResource($this->layer); 38 | $this->assertEquals($this->layer->get('image.contents'), 'foo'); 39 | } 40 | 41 | /** 42 | * @expectedException Imagecraft\Exception\BadMethodCallException 43 | */ 44 | public function testProcessWhenNoResourceIsFound() 45 | { 46 | $this->pass->processResource($this->layer); 47 | } 48 | 49 | public function testProcessResize() 50 | { 51 | $this->layer->add([ 52 | 'image.resize.width' => -5, 53 | 'image.resize.height' => 0, 54 | 'image.resize.option' => ImageAwareLayerInterface::RESIZE_SHRINK, 55 | ]); 56 | $this->pass->processResize($this->layer); 57 | $this->assertEquals(1, $this->layer->get('image.resize.width')); 58 | $this->assertEquals(1, $this->layer->get('image.resize.height')); 59 | $this->assertEquals(ImageAwareLayerInterface::RESIZE_SHRINK, $this->layer->get('image.resize.option')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/LayerPass/RegularLayerPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\LayerPass\\RegularLayerPass', null); 19 | $this->layer = $this->getMock('Imagecraft\\Layer\\ImageLayer', null); 20 | } 21 | 22 | public function testProcessMove() 23 | { 24 | $this->pass->processMove($this->layer); 25 | $this->assertTrue($this->layer->has('regular.move.x')); 26 | $this->assertTrue($this->layer->has('regular.move.y')); 27 | $this->assertTrue($this->layer->has('regular.move.gravity')); 28 | 29 | $this->layer->clear(); 30 | $this->layer->add([ 31 | 'regular.move.x' => 11.1, 32 | 'regular.move.y' => -10, 33 | 'regular.move.gravity' => RegularLayerInterface::MOVE_TOP_LEFT, 34 | 35 | ]); 36 | $this->pass->processMove($this->layer); 37 | $this->assertEquals(11, $this->layer->get('regular.move.x')); 38 | $this->assertEquals(-10, $this->layer->get('regular.move.y')); 39 | $this->assertEquals(RegularLayerInterface::MOVE_TOP_LEFT, $this->layer->get('regular.move.gravity')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/LayerPass/TextLayerPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\LayerPass\\TextLayerPass', null); 17 | $this->layer = $this->getMock('Imagecraft\\Layer\\TextLayer', null); 18 | } 19 | 20 | public function testProcessFont() 21 | { 22 | $this->layer->add([ 23 | 'text.font.filename' => 'foo', 24 | 'text.font.size' => 15, 25 | 'text.font.hex_color' => '#000', 26 | ]); 27 | $this->pass->processFont($this->layer); 28 | $this->assertEquals('foo', $this->layer->get('text.font.filename')); 29 | $this->assertEquals(15, $this->layer->get('text.font.size')); 30 | $this->assertEquals('#000', $this->layer->get('text.font.hex_color')); 31 | $this->assertEquals([0, 0, 0], $this->layer->get('text.font.rgb_color')); 32 | } 33 | 34 | /** 35 | * @expectedException Imagecraft\Exception\BadMethodCallException 36 | */ 37 | public function testProcessFontWhenNoFontIsAdded() 38 | { 39 | $this->pass->processFont($this->layer); 40 | } 41 | 42 | public function testProcessLabel() 43 | { 44 | $this->pass->processLabel($this->layer); 45 | $this->assertEquals('', $this->layer->get('text.label')); 46 | 47 | $this->layer->clear(); 48 | $this->layer->set('text.label', 'foo'); 49 | $this->pass->processLabel($this->layer); 50 | $this->assertEquals('foo', $this->layer->get('text.label')); 51 | } 52 | 53 | public function testProcessAngle() 54 | { 55 | $this->pass->processAngle($this->layer); 56 | $this->assertEquals(0, $this->layer->get('text.angle')); 57 | 58 | $this->layer->clear(); 59 | $this->layer->set('text.angle', 10.1); 60 | $this->pass->processAngle($this->layer); 61 | $this->assertEquals(10, $this->layer->get('text.angle')); 62 | } 63 | 64 | public function testProcessLineSpacing() 65 | { 66 | $this->pass->processLineSpacing($this->layer); 67 | $this->assertEquals(0.5, $this->layer->get('text.line_spacing')); 68 | 69 | $this->layer->clear(); 70 | $this->layer->set('text.line_spacing', 10.1); 71 | $this->pass->processLineSpacing($this->layer); 72 | $this->assertEquals(10.1, $this->layer->get('text.line_spacing')); 73 | } 74 | 75 | public function testProcessBox() 76 | { 77 | $this->pass->processBox($this->layer); 78 | $this->assertEquals([0, 0, 0, 0], $this->layer->get('text.box.paddings')); 79 | $this->assertEquals(null, $this->layer->get('text.box.hex_color')); 80 | 81 | $this->layer->clear(); 82 | $this->layer->add([ 83 | 'text.box.paddings' => [10, 1], 84 | 'text.box.hex_color' => '#000', 85 | ]); 86 | $this->pass->processBox($this->layer); 87 | $this->assertEquals([10, 1, 0, 0], $this->layer->get('text.box.paddings')); 88 | $this->assertEquals('#000', $this->layer->get('text.box.hex_color')); 89 | $this->assertEquals([0, 0, 0], $this->layer->get('text.box.rgb_color')); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/OptionPass/CacheDirOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\CacheDirOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals(null, $option['cache_dir']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/DebugOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\DebugOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertTrue($option['debug']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/DelegatingOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\DelegatingOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertInternalType('array', $option); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/EngineOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\EngineOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals('php_gd', $option['engine']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/GifAnimationOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\GifAnimationOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertTrue($option['gif_animation']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/JpegQualityOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\JpegQualityOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals(100, $option['jpeg_quality']); 21 | 22 | $option = $this->pass->process(['jpeg_quality' => 200]); 23 | $this->assertEquals(100, $option['jpeg_quality']); 24 | 25 | $option = $this->pass->process(['jpeg_quality' => -100]); 26 | $this->assertEquals(100, $option['jpeg_quality']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/OptionPass/LocaleOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\LocaleOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals('en', $option['locale']); 21 | 22 | $option = $this->pass->process(['locale' => 'foo']); 23 | $this->assertEquals('en', $option['locale']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/OptionPass/MemoryLimitOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\MemoryLimitOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals(-10, $option['memory_limit']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/OutputFormatOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\OutputFormatOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals('default', $option['output_format']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/OptionPass/PngCompressionOptionPassTest.php: -------------------------------------------------------------------------------- 1 | pass = $this->getMock('Imagecraft\\OptionPass\\PngCompressionOptionPass', null); 15 | } 16 | 17 | public function testProcess() 18 | { 19 | $option = $this->pass->process([]); 20 | $this->assertEquals(100, $option['png_compression']); 21 | 22 | $option = $this->pass->process(['png_compression' => 200]); 23 | $this->assertEquals(100, $option['png_compression']); 24 | 25 | $option = $this->pass->process(['png_compression' => -100]); 26 | $this->assertEquals(100, $option['png_compression']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Imagecraft\\', __DIR__); 5 | 6 | \TranslatedException\TranslatedException::init(); 7 | \ImcStream\ImcStream::register(); 8 | 9 | date_default_timezone_set('UTC'); 10 | --------------------------------------------------------------------------------