├── .gitignore ├── Block └── Cache │ └── Additional.php ├── Controller └── Adminhtml │ └── Cache │ └── CleanResizedImages.php ├── Model ├── Cache.php └── Resizer.php ├── Plugin └── View │ └── Layout │ └── Generator │ └── Block.php ├── README.md ├── composer.json ├── docs └── img │ └── admin-clear-cache.png ├── etc ├── adminhtml │ └── routes.xml ├── di.xml ├── events.xml └── module.xml ├── grumphp.yml ├── registration.php └── view └── adminhtml ├── layout └── adminhtml_cache_index.xml └── templates └── system └── cache └── additional.phtml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store -------------------------------------------------------------------------------- /Block/Cache/Additional.php: -------------------------------------------------------------------------------- 1 | getUrl('staempfli_imageresizer/cache/cleanResizedImages'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Cache/CleanResizedImages.php: -------------------------------------------------------------------------------- 1 | resizerCache = $resizerCache; 48 | } 49 | 50 | /** 51 | * Clean JS/css files cache 52 | * 53 | * @return Redirect 54 | */ 55 | public function execute() 56 | { 57 | try { 58 | $this->resizerCache->clearResizedImagesCache(); 59 | $this->_eventManager->dispatch('staempfli_imageresizer_clean_images_cache_after'); 60 | $this->messageManager->addSuccessMessage(__('The resized images cache was cleaned.')); 61 | } catch (LocalizedException $e) { 62 | $this->messageManager->addErrorMessage($e->getMessage()); 63 | } catch (\Exception $e) { 64 | $this->messageManager->addExceptionMessage($e, __('An error occurred while clearing the resized images cache.')); 65 | } 66 | 67 | /** @var Redirect $resultRedirect */ 68 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 69 | return $resultRedirect->setPath('adminhtml/cache'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Model/Cache.php: -------------------------------------------------------------------------------- 1 | mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); 28 | } 29 | 30 | /** 31 | * Delete Image resizer cache dir 32 | */ 33 | public function clearResizedImagesCache() 34 | { 35 | $this->mediaDirectory->delete(Resizer::IMAGE_RESIZER_CACHE_DIR); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Model/Resizer.php: -------------------------------------------------------------------------------- 1 | true, 69 | 'keepAspectRatio' => true, 70 | 'keepTransparency' => true, 71 | 'keepFrame' => false, 72 | 'backgroundColor' => null, 73 | 'quality' => 85 74 | ]; 75 | /** 76 | * @var array 77 | */ 78 | protected $subPathSettingsMapping = [ 79 | 'constrainOnly' => 'co', 80 | 'keepAspectRatio' => 'ar', 81 | 'keepTransparency' => 'tr', 82 | 'keepFrame' => 'fr', 83 | 'backgroundColor' => 'bc', 84 | ]; 85 | /** 86 | * @var File 87 | */ 88 | protected $fileIo; 89 | /** 90 | * @var LoggerInterface 91 | */ 92 | protected $logger; 93 | 94 | /** 95 | * Resizer constructor. 96 | * @param Filesystem $filesystem 97 | * @param ImageAdapterFactory $imageAdapterFactory 98 | * @param StoreManagerInterface $storeManager 99 | * @param File $fileIo 100 | * @param LoggerInterface $logger 101 | */ 102 | public function __construct( 103 | Filesystem $filesystem, 104 | imageAdapterFactory $imageAdapterFactory, 105 | StoreManagerInterface $storeManager, 106 | File $fileIo, 107 | LoggerInterface $logger 108 | ) { 109 | $this->imageAdapterFactory = $imageAdapterFactory; 110 | $this->mediaDirectoryRead = $filesystem->getDirectoryRead(DirectoryList::MEDIA); 111 | $this->storeManager = $storeManager; 112 | $this->fileIo = $fileIo; 113 | $this->logger = $logger; 114 | } 115 | 116 | /** 117 | * Resized image and return url 118 | * - Return original image url if no success 119 | * 120 | * @param string $imageUrl 121 | * @param null|int $width 122 | * @param null|int $height 123 | * @param array $resizeSettings 124 | * @return bool|string 125 | */ 126 | public function resizeAndGetUrl(string $imageUrl, $width, $height, array $resizeSettings = []) 127 | { 128 | try { 129 | // Set $resultUrl with $fileUrl to return this one in case the resize fails. 130 | $resultUrl = $imageUrl; 131 | $this->initRelativeFilenameFromUrl($imageUrl); 132 | if (!$this->relativeFilename) { 133 | return $resultUrl; 134 | } 135 | 136 | // Check if image is an animated gif return original gif instead of resized still. 137 | if ($this->isAnimatedGif($imageUrl)){ 138 | return $resultUrl; 139 | } 140 | 141 | $this->initSize($width, $height); 142 | $this->initResizeSettings($resizeSettings); 143 | } catch (\Exception $e) { 144 | $this->logger->addError("Staempfli_ImageResizer: could not find image: \n" . $e->getMessage()); 145 | } 146 | try { 147 | // Check if resized image already exists in cache 148 | $resizedUrl = $this->getResizedImageUrl(); 149 | if (!$resizedUrl) { 150 | if ($this->resizeAndSaveImage()) { 151 | $resizedUrl = $this->getResizedImageUrl(); 152 | } 153 | } 154 | if ($resizedUrl) { 155 | $resultUrl = $resizedUrl; 156 | } 157 | } catch (\Exception $e) { 158 | $this->logger->addError("Staempfli_ImageResizer: could not resize image: \n" . $e->getMessage()); 159 | } 160 | 161 | return $resultUrl; 162 | } 163 | 164 | /** 165 | * Prepare and set resize settings for image 166 | * 167 | * @param array $resizeSettings 168 | */ 169 | protected function initResizeSettings(array $resizeSettings) 170 | { 171 | // Init resize settings with default 172 | $this->resizeSettings = $this->defaultSettings; 173 | // Override resizeSettings only if key matches with existing settings 174 | foreach ($resizeSettings as $key => $value) { 175 | if (array_key_exists($key, $this->resizeSettings)) { 176 | $this->resizeSettings[$key] = $value; 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * Init relative filename from original image url to resize 183 | * 184 | * @param string $imageUrl 185 | * @return bool|mixed|string 186 | */ 187 | protected function initRelativeFilenameFromUrl(string $imageUrl) 188 | { 189 | $this->relativeFilename = false; // reset filename in case there was another value defined 190 | $mediaUrl = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA); 191 | $mediaPath = parse_url($mediaUrl, PHP_URL_PATH); 192 | $imagePath = parse_url($imageUrl, PHP_URL_PATH); 193 | 194 | if (0 === strpos($imagePath, $mediaPath)) { 195 | $this->relativeFilename = substr_replace($imagePath, '', 0, strlen($mediaPath)); 196 | } 197 | } 198 | 199 | /** 200 | * Init resize dimensions 201 | * 202 | * @param null|int $width 203 | * @param null|int $height 204 | */ 205 | protected function initSize($width, $height) 206 | { 207 | $this->width = $width; 208 | $this->height = $height; 209 | } 210 | 211 | /** 212 | * Get sub folder name where the resized image will be saved 213 | * 214 | * In order to have unique folders depending on setting, we use the following logic: 215 | * - x_[co]_[ar]_[tr]_[fr]_[quality] 216 | * 217 | * @return string 218 | */ 219 | protected function getResizeSubFolderName() 220 | { 221 | $subPath = $this->width . "x" . $this->height; 222 | foreach ($this->resizeSettings as $key => $value) { 223 | if ($value && isset($this->subPathSettingsMapping[$key])) { 224 | $subPath .= "_" . $this->subPathSettingsMapping[$key]; 225 | } 226 | } 227 | return sprintf('%s_%s',$subPath, $this->resizeSettings['quality']); 228 | } 229 | 230 | /** 231 | * Get relative path where the resized image is saved 232 | * 233 | * In order to have unique paths, we use the original image path plus the ResizeSubFolderName. 234 | * 235 | * @return string 236 | */ 237 | protected function getRelativePathResizedImage() 238 | { 239 | $pathInfo = $this->fileIo->getPathInfo($this->relativeFilename); 240 | $relativePathParts = [ 241 | self::IMAGE_RESIZER_CACHE_DIR, 242 | $pathInfo['dirname'], 243 | $this->getResizeSubFolderName(), 244 | $pathInfo['basename'] 245 | ]; 246 | return implode('/', $relativePathParts); 247 | } 248 | 249 | /** 250 | * Get absolute path from original image 251 | * 252 | * @return string 253 | */ 254 | protected function getAbsolutePathOriginal() 255 | { 256 | return $this->mediaDirectoryRead->getAbsolutePath($this->relativeFilename); 257 | } 258 | 259 | /** 260 | * Get absolute path from resized image 261 | * 262 | * @return string 263 | */ 264 | protected function getAbsolutePathResized() 265 | { 266 | return $this->mediaDirectoryRead->getAbsolutePath($this->getRelativePathResizedImage()); 267 | } 268 | 269 | /** 270 | * Get url of resized image 271 | * 272 | * @return bool|string 273 | */ 274 | protected function getResizedImageUrl() 275 | { 276 | $relativePath = $this->getRelativePathResizedImage(); 277 | if ($this->mediaDirectoryRead->isFile($relativePath)) { 278 | return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $relativePath; 279 | } 280 | return false; 281 | } 282 | 283 | /** 284 | * Resize and save new generated image 285 | * 286 | * @return bool 287 | */ 288 | protected function resizeAndSaveImage() 289 | { 290 | if (!$this->mediaDirectoryRead->isFile($this->relativeFilename)) { 291 | return false; 292 | } 293 | 294 | $imageAdapter = $this->imageAdapterFactory->create(); 295 | $imageAdapter->open($this->getAbsolutePathOriginal()); 296 | $imageAdapter->constrainOnly($this->resizeSettings['constrainOnly']); 297 | $imageAdapter->keepAspectRatio($this->resizeSettings['keepAspectRatio']); 298 | $imageAdapter->keepTransparency($this->resizeSettings['keepTransparency']); 299 | $imageAdapter->keepFrame($this->resizeSettings['keepFrame']); 300 | $imageAdapter->backgroundColor($this->resizeSettings['backgroundColor']); 301 | $imageAdapter->quality($this->resizeSettings['quality']); 302 | $imageAdapter->resize($this->width, $this->height); 303 | $imageAdapter->save($this->getAbsolutePathResized()); 304 | return true; 305 | } 306 | 307 | /** 308 | * Detects animated GIF from given file pointer resource or filename. 309 | * 310 | * @param resource|string $file File pointer resource or filename 311 | * @return bool 312 | */ 313 | protected function isAnimatedGif($file) 314 | { 315 | $filepointer = null; 316 | 317 | if (is_string($file)) { 318 | $filepointer = fopen($file, "rb"); 319 | } else { 320 | $filepointer = $file; 321 | /* Make sure that we are at the beginning of the file */ 322 | fseek($filepointer, 0); 323 | } 324 | 325 | if (fread($filepointer, 3) !== "GIF") { 326 | fclose($filepointer); 327 | 328 | return false; 329 | } 330 | 331 | $frames = 0; 332 | 333 | while (!feof($filepointer) && $frames < 2) { 334 | if (fread($filepointer, 1) === "\x00") { 335 | /* Some of the animated GIFs do not contain graphic control extension (starts with 21 f9) */ 336 | if (fread($filepointer, 1) === "\x21" || fread($filepointer, 2) === "\x21\xf9") { 337 | $frames++; 338 | } 339 | } 340 | } 341 | 342 | fclose($filepointer); 343 | 344 | return $frames > 1; 345 | } 346 | 347 | } -------------------------------------------------------------------------------- /Plugin/View/Layout/Generator/Block.php: -------------------------------------------------------------------------------- 1 | resizer = $resizer; 28 | } 29 | 30 | /** 31 | * Add image resizer object to all template blocks 32 | * 33 | * @param MagentoGeneratorBock $subject 34 | * @param $result 35 | * @return mixed 36 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 37 | */ 38 | public function afterCreateBlock(MagentoGeneratorBock $subject, $result) //@codingStandardsIgnoreLine 39 | { 40 | if (is_a($result, 'Magento\Framework\View\Element\Template')) { 41 | $result->addData(['image_resizer' => $this->resizer]); 42 | } 43 | return $result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Image Resizer 2 | [![Project Status: Abandoned – Initial development has started, but there has not yet been a stable, usable release; the project has been abandoned and the author(s) do not intend on continuing development.](http://www.repostatus.org/badges/latest/abandoned.svg)](http://www.repostatus.org/#abandoned) 3 | 4 | Magento 2 Module to add simple image resizing capabilities in all blocks and .phtml templates 5 | 6 | ## Installation 7 | 8 | ``` 9 | $ composer require "staempfli/magento2-module-image-resizer":"~2.0" 10 | ``` 11 | 12 | ## Usage 13 | 14 | `imageResizer` is automatically available in all frontend Blocks. 15 | You can resize your images just calling a method: 16 | 17 | ```php 18 | /** @var \Staempfli\ImageResizer\Model\Resizer $imageResizer */ 19 | $imageResizer = $block->getImageResizer(); 20 | $resizedImageUrl = $imageResizer->resizeAndGetUrl(, $width, $height, [$resizeSettings]); 21 | ``` 22 | 23 | You can do that directly on the .phtml or in your custom Block. 24 | 25 | ## Cache 26 | 27 | Resized images are saved in cache to improve performance. That way, if an image was already resized, we just use the one in cache. 28 | 29 | If you need to, you can clear the resized images cache on the Admin Cache Management 30 | 31 | ![Admin Clear Resized Images Cache](docs/img/admin-clear-cache.png "Clear Resized Images Cache") 32 | 33 | ## Prerequisites 34 | 35 | - PHP >= 7.0.* 36 | - Magento >= 2.1.* 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "staempfli/magento2-module-image-resizer", 3 | "description": "Magento 2 Module to add simple image resizing capabilities in all blocks and .phtml templates", 4 | "require": { 5 | "php": "^7.0" 6 | }, 7 | "type": "magento2-module", 8 | "license": [ 9 | "OSL-3.0", 10 | "AFL-3.0" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Juan Alonso", 15 | "email": "juan.alonso@staempfli.com" 16 | } 17 | ], 18 | "autoload": { 19 | "files": [ 20 | "registration.php" 21 | ], 22 | "psr-4": { 23 | "Staempfli\\ImageResizer\\": "" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/img/admin-clear-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staempfli/magento2-module-image-resizer/45c9b3da2a30788e18750ba77eb50274eba77ee0/docs/img/admin-clear-cache.png -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /grumphp.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | git_dir: . 3 | bin_dir: ../../../../bin 4 | tasks: 5 | phpcsfixer: 6 | config_file: . 7 | config: default 8 | fixers: [-psr0] 9 | level: psr2 10 | verbose: true 11 | composer: 12 | file: ./composer.json 13 | no_check_all: false 14 | no_check_lock: false 15 | no_check_publish: false 16 | with_dependencies: false 17 | strict: false 18 | git_blacklist: 19 | keywords: 20 | - "die(" 21 | - "var_dump(" 22 | - "exit;" 23 | - "console.log(" 24 | phpcs: 25 | standard: "../../magento-ecg/coding-standard/EcgM2/" 26 | show_warnings: true 27 | tab_width: 4 28 | ignore_patterns: [test] 29 | sniffs: [] 30 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /view/adminhtml/templates/system/cache/additional.phtml: -------------------------------------------------------------------------------- 1 | 9 | 12 |

13 | 16 | 17 |

--------------------------------------------------------------------------------