├── Helper └── ImageHelper.php ├── Model ├── AssetFactory.php ├── File │ ├── Uploader.php │ └── UploaderFactory.php └── Image │ └── Adapter │ └── Gd2.php ├── Plugin ├── App │ └── MediaPlugin.php ├── Controller │ └── Adminhtml │ │ └── Wysiwyg │ │ ├── DirectivePlugin.php │ │ └── Images │ │ └── ThumbnailPlugin.php ├── Design │ └── Backend │ │ └── ImagePlugin.php ├── File │ ├── UploaderPlugin.php │ └── Validator │ │ └── NotProtectedExtensionPlugin.php ├── Product │ └── Gallery │ │ └── ProcessorPlugin.php ├── Swatches │ └── Helper │ │ └── MediaPlugin.php └── Wysiwyg │ └── Images │ └── StoragePlugin.php ├── README.md ├── composer.json ├── etc ├── config.xml ├── di.xml └── module.xml ├── registration.php └── view └── adminhtml ├── requirejs-config.js └── web ├── css └── source │ └── _module.less └── js ├── form └── element │ └── image-uploader-mixin.js └── media-uploader.js /Helper/ImageHelper.php: -------------------------------------------------------------------------------- 1 | getVectorExtensions()); 31 | } 32 | 33 | /** 34 | * Get vector image extensions 35 | * 36 | * @return array 37 | */ 38 | public function getVectorExtensions() 39 | { 40 | return $this->scopeConfig->getValue(self::XML_PATH_VECTOR_EXTENSIONS, 'store') ?: []; 41 | } 42 | 43 | /** 44 | * Get web image extensions 45 | * 46 | * @return array 47 | */ 48 | public function getWebImageExtensions() 49 | { 50 | return $this->scopeConfig->getValue(self::XML_PATH_WEB_IMAGE_EXTENSIONS, 'store') ?: []; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Model/AssetFactory.php: -------------------------------------------------------------------------------- 1 | assetFactory = $assetFactory; 44 | $this->filesystem = $filesystem; 45 | $this->imageHelper = $imageHelper; 46 | } 47 | 48 | /** 49 | * Set height and width for SVG images when saving to DB 50 | * 51 | * @param array $data 52 | * @return mixed 53 | */ 54 | public function create(array $data = []) 55 | { 56 | if ((empty($data['width']) || empty($data['height'])) 57 | && isset($data['path']) 58 | && $this->imageHelper->isVectorImage($data['path']) 59 | ) { 60 | $absolutePath = $this->getMediaDirectory()->getAbsolutePath($data['path']); 61 | $width = 300; 62 | $height = 150; 63 | 64 | $svg = simplexml_load_file($absolutePath); 65 | if (!empty($svg['width']) && !empty($svg['height'])) { 66 | $width = intval($svg['width']); 67 | $height = intval($svg['height']); 68 | } 69 | 70 | $data['width'] = $width; 71 | $data['height'] = $height; 72 | } 73 | 74 | return $this->assetFactory->create($data); 75 | } 76 | 77 | /** 78 | * Retrieve media directory instance with read access 79 | * 80 | * @return ReadInterface 81 | */ 82 | private function getMediaDirectory() 83 | { 84 | return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Model/File/Uploader.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 52 | } 53 | 54 | /** 55 | * Add web images to the list of allowed Mime-Types 56 | * 57 | * @param array $validTypes 58 | * @return bool 59 | */ 60 | public function checkMimeType($validTypes = []) 61 | { 62 | foreach ($this->imageHelper->getVectorExtensions() as $extension) { 63 | $validTypes[] = 'image/' . $extension; 64 | } 65 | 66 | foreach ($this->imageHelper->getWebImageExtensions() as $extension) { 67 | $validTypes[] = 'image/' . $extension; 68 | } 69 | 70 | return parent::checkMimeType($validTypes); 71 | } 72 | 73 | /** 74 | * Add web images to the list of allowed extensions 75 | * 76 | * @param array $extensions 77 | * @return Uploader 78 | */ 79 | public function setAllowedExtensions($extensions = []) 80 | { 81 | $extensions = array_merge( 82 | $extensions, 83 | $this->imageHelper->getVectorExtensions(), 84 | $this->imageHelper->getWebImageExtensions() 85 | ); 86 | 87 | return parent::setAllowedExtensions($extensions); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Model/File/UploaderFactory.php: -------------------------------------------------------------------------------- 1 | _objectManager = $objectManager; 26 | } 27 | 28 | /** 29 | * Create new uploader instance 30 | * 31 | * @param array $data 32 | * @return Uploader 33 | */ 34 | public function create(array $data = []) 35 | { 36 | return $this->_objectManager->create(Uploader::class, $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Model/Image/Adapter/Gd2.php: -------------------------------------------------------------------------------- 1 | ['output' => 'imagegif', 'create' => 'imagecreatefromgif'], 40 | IMAGETYPE_JPEG => ['output' => 'imagejpeg', 'create' => 'imagecreatefromjpeg'], 41 | IMAGETYPE_PNG => ['output' => 'imagepng', 'create' => 'imagecreatefrompng'], 42 | IMAGETYPE_XBM => ['output' => 'imagexbm', 'create' => 'imagecreatefromxbm'], 43 | IMAGETYPE_WBMP => ['output' => 'imagewbmp', 'create' => 'imagecreatefromxbm'], 44 | IMAGETYPE_WEBP => ['output' => 'imagewebp', 'create' => 'imagecreatefromwebp'], 45 | ]; 46 | 47 | /** 48 | * Whether image was resized or not 49 | * 50 | * @var bool 51 | */ 52 | protected $_resized = false; 53 | 54 | /** 55 | * @var ImageHelper 56 | */ 57 | private $helper; 58 | 59 | /** 60 | * @param \Magento\Framework\Filesystem $filesystem 61 | * @param \Psr\Log\LoggerInterface $logger 62 | * @param ImageHelper $helper 63 | * @param array $data 64 | */ 65 | public function __construct( 66 | \Magento\Framework\Filesystem $filesystem, 67 | \Psr\Log\LoggerInterface $logger, 68 | ImageHelper $helper, 69 | array $data = [] 70 | ) { 71 | parent::__construct($filesystem, $logger, $data); 72 | 73 | $this->helper = $helper; 74 | } 75 | 76 | /** 77 | * For properties reset, e.g. mimeType caching. 78 | * 79 | * @return void 80 | */ 81 | protected function _reset() 82 | { 83 | $this->_fileMimeType = null; 84 | $this->_fileType = null; 85 | } 86 | 87 | /** 88 | * Open image for processing 89 | * 90 | * @param string $filename 91 | * @return void 92 | * @throws \OverflowException|FileSystemException 93 | */ 94 | public function open($filename) 95 | { 96 | if ($filename === null || !file_exists($filename)) { 97 | throw new FileSystemException( 98 | new Phrase('File "%1" does not exist.', [$filename]) 99 | ); 100 | } 101 | if (!$filename || filesize($filename) === 0 || !$this->validateURLScheme($filename)) { 102 | throw new \InvalidArgumentException('Wrong file'); 103 | } 104 | $this->_fileName = $filename; 105 | $this->_reset(); 106 | $this->getMimeType(); 107 | $this->_getFileAttributes(); 108 | if ($this->_isMemoryLimitReached()) { 109 | throw new \OverflowException('Memory limit has been reached.'); 110 | } 111 | $this->imageDestroy(); 112 | $this->_imageHandler = call_user_func( 113 | $this->_getCallback('create', null, sprintf('Unsupported image format. File: %s', $this->_fileName)), 114 | $this->_fileName 115 | ); 116 | } 117 | 118 | /** 119 | * Checks for invalid URL schema if it exists 120 | * 121 | * @param string $filename 122 | * @return bool 123 | */ 124 | private function validateURLScheme(string $filename) : bool 125 | { 126 | $allowed_schemes = ['ftp', 'ftps', 'http', 'https']; 127 | $url = parse_url($filename); 128 | if ($url && isset($url['scheme']) && !in_array($url['scheme'], $allowed_schemes)) { 129 | return false; 130 | } 131 | 132 | return true; 133 | } 134 | 135 | /** 136 | * @param string $filePath 137 | * @return bool 138 | */ 139 | public function validateUploadFile($filePath) 140 | { 141 | /* 142 | * FIX: Skip validation for vector images 143 | */ 144 | if ($this->helper->isVectorImage($filePath)) { 145 | return file_exists($filePath); 146 | } 147 | 148 | return parent::validateUploadFile($filePath); 149 | } 150 | 151 | /** 152 | * Checks whether memory limit is reached. 153 | * 154 | * @return bool 155 | */ 156 | protected function _isMemoryLimitReached() 157 | { 158 | $limit = $this->_convertToByte(ini_get('memory_limit')); 159 | $requiredMemory = $this->_getImageNeedMemorySize($this->_fileName); 160 | if ($limit === -1) { 161 | // A limit of -1 means no limit: http://www.php.net/manual/en/ini.core.php#ini.memory-limit 162 | return false; 163 | } 164 | return memory_get_usage(true) + $requiredMemory > $limit; 165 | } 166 | 167 | /** 168 | * Get image needed memory size 169 | * 170 | * @param string $file 171 | * @return float|int 172 | */ 173 | protected function _getImageNeedMemorySize($file) 174 | { 175 | $imageInfo = getimagesize($file); 176 | if (!isset($imageInfo[0]) || !isset($imageInfo[1])) { 177 | return 0; 178 | } 179 | if (!isset($imageInfo['channels'])) { 180 | // if there is no info about this parameter lets set it for maximum 181 | $imageInfo['channels'] = 4; 182 | } 183 | if (!isset($imageInfo['bits'])) { 184 | // if there is no info about this parameter lets set it for maximum 185 | $imageInfo['bits'] = 8; 186 | } 187 | 188 | return round( 189 | ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + pow(2, 16)) * 1.65 190 | ); 191 | } 192 | 193 | /** 194 | * Converts memory value (e.g. 64M, 129K) to bytes. 195 | * 196 | * Case insensitive value might be used. 197 | * 198 | * @param string $memoryValue 199 | * @return int 200 | */ 201 | protected function _convertToByte($memoryValue) 202 | { 203 | if (stripos($memoryValue, 'G') !== false) { 204 | return (int)$memoryValue * pow(1024, 3); 205 | } elseif (stripos($memoryValue, 'M') !== false) { 206 | return (int)$memoryValue * 1024 * 1024; 207 | } elseif (stripos($memoryValue, 'K') !== false) { 208 | return (int)$memoryValue * 1024; 209 | } 210 | 211 | return (int)$memoryValue; 212 | } 213 | 214 | /** 215 | * Save image to specific path. 216 | * 217 | * If some folders of path does not exist they will be created 218 | * 219 | * @param null|string $destination 220 | * @param null|string $newName 221 | * @return void 222 | * @throws \Exception If destination path is not writable 223 | */ 224 | public function save($destination = null, $newName = null) 225 | { 226 | $fileName = $this->_prepareDestination($destination, $newName); 227 | 228 | if (!$this->_resized) { 229 | // keep alpha transparency 230 | $isAlpha = false; 231 | $isTrueColor = false; 232 | $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha, $isTrueColor); 233 | if ($isAlpha) { 234 | if ($isTrueColor) { 235 | $newImage = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); 236 | } else { 237 | $newImage = imagecreate($this->_imageSrcWidth, $this->_imageSrcHeight); 238 | } 239 | $this->fillBackgroundColor($newImage); 240 | imagecopy($newImage, $this->_imageHandler, 0, 0, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight); 241 | $this->imageDestroy(); 242 | $this->_imageHandler = $newImage; 243 | } 244 | } 245 | 246 | // Enable interlace 247 | imageinterlace($this->_imageHandler, true); 248 | 249 | // Set image quality value 250 | switch ($this->_fileType) { 251 | case IMAGETYPE_PNG: 252 | $quality = 9; // For PNG files compression level must be from 0 (no compression) to 9. 253 | break; 254 | 255 | case IMAGETYPE_JPEG: 256 | $quality = $this->quality(); 257 | break; 258 | 259 | default: 260 | $quality = null; // No compression. 261 | } 262 | 263 | // Prepare callback method parameters 264 | $functionParameters = [$this->_imageHandler, $fileName]; 265 | if ($quality) { 266 | $functionParameters[] = $quality; 267 | } 268 | 269 | call_user_func_array($this->_getCallback('output'), $functionParameters); 270 | } 271 | 272 | /** 273 | * Render image and return its binary contents. 274 | * 275 | * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage 276 | * 277 | * @return string 278 | */ 279 | public function getImage() 280 | { 281 | ob_start(); 282 | call_user_func($this->_getCallback('output'), $this->_imageHandler); 283 | return ob_get_clean(); 284 | } 285 | 286 | /** 287 | * Obtain function name, basing on image type and callback type 288 | * 289 | * @param string $callbackType 290 | * @param null|int $fileType 291 | * @param string $unsupportedText 292 | * @return string 293 | * @throws \InvalidArgumentException 294 | * @throws \BadFunctionCallException 295 | */ 296 | private function _getCallback($callbackType, $fileType = null, $unsupportedText = 'Unsupported image format.') 297 | { 298 | if (null === $fileType) { 299 | $fileType = $this->_fileType; 300 | } 301 | if (empty(self::$_callbacks[$fileType])) { 302 | throw new \InvalidArgumentException($unsupportedText); 303 | } 304 | if (empty(self::$_callbacks[$fileType][$callbackType])) { 305 | throw new \BadFunctionCallException('Callback not found.'); 306 | } 307 | return self::$_callbacks[$fileType][$callbackType]; 308 | } 309 | 310 | /** 311 | * Fill image with main background color. 312 | * 313 | * Returns a color identifier. 314 | * 315 | * @param resource &$imageResourceTo 316 | * 317 | * @return void 318 | * @throws \InvalidArgumentException 319 | */ 320 | private function fillBackgroundColor(&$imageResourceTo): void 321 | { 322 | // try to keep transparency, if any 323 | if ($this->_keepTransparency) { 324 | $isAlpha = false; 325 | $transparentIndex = $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha); 326 | 327 | try { 328 | // fill true color png with alpha transparency 329 | if ($isAlpha) { 330 | $this->applyAlphaTransparency($imageResourceTo); 331 | 332 | return; 333 | } 334 | 335 | if ($transparentIndex !== false) { 336 | $this->applyTransparency($imageResourceTo, $transparentIndex); 337 | 338 | return; 339 | } 340 | // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch 341 | } catch (\Exception $e) { 342 | // fallback to default background color 343 | } 344 | } 345 | list($red, $green, $blue) = $this->_backgroundColor ?: [0, 0, 0]; 346 | $color = imagecolorallocate($imageResourceTo, $red, $green, $blue); 347 | 348 | if (!imagefill($imageResourceTo, 0, 0, $color)) { 349 | throw new \InvalidArgumentException("Failed to fill image background with color {$red} {$green} {$blue}."); 350 | } 351 | } 352 | 353 | /** 354 | * Method to apply alpha transparency for image. 355 | * 356 | * @param resource $imageResourceTo 357 | * 358 | * @return void 359 | * @SuppressWarnings(PHPMD.LongVariable) 360 | */ 361 | private function applyAlphaTransparency(&$imageResourceTo): void 362 | { 363 | if (!imagealphablending($imageResourceTo, false)) { 364 | throw new \InvalidArgumentException('Failed to set alpha blending for PNG image.'); 365 | } 366 | $transparentAlphaColor = imagecolorallocatealpha($imageResourceTo, 0, 0, 0, 127); 367 | 368 | if (false === $transparentAlphaColor) { 369 | throw new \InvalidArgumentException('Failed to allocate alpha transparency for PNG image.'); 370 | } 371 | 372 | if (!imagefill($imageResourceTo, 0, 0, $transparentAlphaColor)) { 373 | throw new \InvalidArgumentException('Failed to fill PNG image with alpha transparency.'); 374 | } 375 | 376 | if (!imagesavealpha($imageResourceTo, true)) { 377 | throw new \InvalidArgumentException('Failed to save alpha transparency into PNG image.'); 378 | } 379 | } 380 | 381 | /** 382 | * Method to apply transparency for image. 383 | * 384 | * @param resource $imageResourceTo 385 | * @param int $transparentIndex 386 | * 387 | * @return void 388 | */ 389 | private function applyTransparency(&$imageResourceTo, $transparentIndex): void 390 | { 391 | // fill image with indexed non-alpha transparency 392 | $transparentColor = false; 393 | 394 | if ($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($this->_imageHandler)) { 395 | list($red, $green, $blue) = array_values(imagecolorsforindex($this->_imageHandler, $transparentIndex)); 396 | $transparentColor = imagecolorallocate($imageResourceTo, (int) $red, (int) $green, (int) $blue); 397 | } 398 | if (false === $transparentColor) { 399 | throw new \InvalidArgumentException('Failed to allocate transparent color for image.'); 400 | } 401 | if (!imagefill($imageResourceTo, 0, 0, $transparentColor)) { 402 | throw new \InvalidArgumentException('Failed to fill image with transparency.'); 403 | } 404 | imagecolortransparent($imageResourceTo, $transparentColor); 405 | } 406 | 407 | /** 408 | * Gives true for a PNG with alpha, false otherwise 409 | * 410 | * @param string $fileName 411 | * @return boolean 412 | */ 413 | public function checkAlpha($fileName) 414 | { 415 | return (ord(file_get_contents((string)$fileName, false, null, 25, 1)) & 6 & 4) == 4; 416 | } 417 | 418 | /** 419 | * Gives true for a WebP with alpha, false otherwise 420 | * 421 | * @param string $filename 422 | * @return bool 423 | */ 424 | public function checkAlphaWebp($filename) 425 | { 426 | $buf = file_get_contents((string) $filename, false, null, 0, 25); 427 | if ($buf[15] === 'L') { 428 | // Simple File Format (Lossless) 429 | return (bool) (!!(ord($buf[24]) & 0x00000010)); 430 | } elseif ($buf[15] === 'X') { 431 | // Extended File Format 432 | return (bool) (!!(ord($buf[20]) & 0x00000010)); 433 | } 434 | 435 | return false; 436 | } 437 | 438 | /** 439 | * Checks if image has alpha transparency 440 | * 441 | * @param resource $imageResource 442 | * @param int $fileType 443 | * @param bool $isAlpha 444 | * @param bool $isTrueColor 445 | * 446 | * @return boolean 447 | * 448 | * @SuppressWarnings(PHPMD.BooleanGetMethodName) 449 | */ 450 | private function _getTransparency($imageResource, $fileType, &$isAlpha = false, &$isTrueColor = false) 451 | { 452 | $isAlpha = false; 453 | $isTrueColor = false; 454 | // assume that transparency is supported by gif/png only 455 | if (IMAGETYPE_GIF === $fileType || IMAGETYPE_PNG === $fileType) { 456 | // check for specific transparent color 457 | $transparentIndex = imagecolortransparent($imageResource); 458 | if ($transparentIndex >= 0) { 459 | return $transparentIndex; 460 | } elseif (IMAGETYPE_PNG === $fileType) { 461 | // assume that truecolor PNG has transparency 462 | $isAlpha = $this->checkAlpha($this->_fileName); 463 | $isTrueColor = true; 464 | // -1 465 | return $transparentIndex; 466 | } 467 | } 468 | if (IMAGETYPE_JPEG === $fileType) { 469 | $isTrueColor = true; 470 | } 471 | 472 | /* 473 | * FIX: prepare transparency data for WebP image 474 | */ 475 | if (IMAGETYPE_WEBP === $fileType) { 476 | $isTrueColor = true; 477 | $isAlpha = $this->checkAlphaWebp($this->_fileName); 478 | } 479 | 480 | return false; 481 | } 482 | 483 | /** 484 | * Change the image size 485 | * 486 | * @param null|int $frameWidth 487 | * @param null|int $frameHeight 488 | * @return void 489 | */ 490 | public function resize($frameWidth = null, $frameHeight = null) 491 | { 492 | $dims = $this->_adaptResizeValues($frameWidth, $frameHeight); 493 | 494 | // create new image 495 | $isAlpha = false; 496 | $isTrueColor = false; 497 | $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha, $isTrueColor); 498 | if ($isTrueColor) { 499 | $newImage = imagecreatetruecolor($dims['frame']['width'], $dims['frame']['height']); 500 | } else { 501 | $newImage = imagecreate($dims['frame']['width'], $dims['frame']['height']); 502 | } 503 | 504 | if ($isAlpha) { 505 | $this->_saveAlpha($newImage); 506 | } 507 | 508 | // fill new image with required color 509 | $this->fillBackgroundColor($newImage); 510 | 511 | if ($this->_imageHandler) { 512 | // resample source image and copy it into new frame 513 | imagecopyresampled( 514 | $newImage, 515 | $this->_imageHandler, 516 | $dims['dst']['x'], 517 | $dims['dst']['y'], 518 | $dims['src']['x'], 519 | $dims['src']['y'], 520 | $dims['dst']['width'], 521 | $dims['dst']['height'], 522 | $this->_imageSrcWidth, 523 | $this->_imageSrcHeight 524 | ); 525 | } 526 | $this->imageDestroy(); 527 | $this->_imageHandler = $newImage; 528 | $this->refreshImageDimensions(); 529 | $this->_resized = true; 530 | } 531 | 532 | /** 533 | * Rotate image on specific angle 534 | * 535 | * @param int $angle 536 | * @return void 537 | */ 538 | public function rotate($angle) 539 | { 540 | $rotatedImage = imagerotate($this->_imageHandler, $angle, $this->imageBackgroundColor); 541 | $this->imageDestroy(); 542 | $this->_imageHandler = $rotatedImage; 543 | $this->refreshImageDimensions(); 544 | } 545 | 546 | /** 547 | * Add watermark to image 548 | * 549 | * @param string $imagePath 550 | * @param int $positionX 551 | * @param int $positionY 552 | * @param int $opacity 553 | * @param bool $tile 554 | * @return void 555 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) 556 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 557 | */ 558 | public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = 30, $tile = false) 559 | { 560 | list($watermarkSrcWidth, $watermarkSrcHeight, $watermarkFileType,) = $this->_getImageOptions($imagePath); 561 | $this->_getFileAttributes(); 562 | $watermark = call_user_func( 563 | $this->_getCallback('create', $watermarkFileType, 'Unsupported watermark image format.'), 564 | $imagePath 565 | ); 566 | 567 | $merged = false; 568 | 569 | $watermark = $this->createWatermarkBasedOnPosition($watermark, $positionX, $positionY, $merged, $tile); 570 | 571 | imagedestroy($watermark); 572 | $this->refreshImageDimensions(); 573 | } 574 | 575 | /** 576 | * Create watermark based on it's image position. 577 | * 578 | * @param resource $watermark 579 | * @param int $positionX 580 | * @param int $positionY 581 | * @param bool $merged 582 | * @param bool $tile 583 | * @return false|resource 584 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) 585 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 586 | * @SuppressWarnings(PHPMD.NPathComplexity) 587 | */ 588 | private function createWatermarkBasedOnPosition( 589 | $watermark, 590 | int $positionX, 591 | int $positionY, 592 | bool $merged, 593 | bool $tile 594 | ) { 595 | if ($this->getWatermarkWidth() && 596 | $this->getWatermarkHeight() && 597 | $this->getWatermarkPosition() != self::POSITION_STRETCH 598 | ) { 599 | $watermark = $this->createWaterMark($watermark, $this->getWatermarkWidth(), $this->getWatermarkHeight()); 600 | } 601 | 602 | /** 603 | * Fixes issue with watermark with transparent background and an image that is not truecolor (e.g GIF). 604 | * blending mode is allowed for truecolor images only. 605 | * @see imagealphablending() 606 | */ 607 | if (!imageistruecolor($this->_imageHandler)) { 608 | $newImage = $this->createTruecolorImageCopy(); 609 | $this->imageDestroy(); 610 | $this->_imageHandler = $newImage; 611 | } 612 | 613 | if ($this->getWatermarkPosition() == self::POSITION_TILE) { 614 | $tile = true; 615 | } elseif ($this->getWatermarkPosition() == self::POSITION_STRETCH) { 616 | $watermark = $this->createWaterMark($watermark, $this->_imageSrcWidth, $this->_imageSrcHeight); 617 | } elseif ($this->getWatermarkPosition() == self::POSITION_CENTER) { 618 | $positionX = (int) ($this->_imageSrcWidth / 2 - imagesx($watermark) / 2); 619 | $positionY = (int) ($this->_imageSrcHeight / 2 - imagesy($watermark) / 2); 620 | $this->imagecopymergeWithAlphaFix( 621 | $this->_imageHandler, 622 | $watermark, 623 | $positionX, 624 | $positionY, 625 | 0, 626 | 0, 627 | imagesx($watermark), 628 | imagesy($watermark), 629 | $this->getWatermarkImageOpacity() 630 | ); 631 | } elseif ($this->getWatermarkPosition() == self::POSITION_TOP_RIGHT) { 632 | $positionX = $this->_imageSrcWidth - imagesx($watermark); 633 | $this->imagecopymergeWithAlphaFix( 634 | $this->_imageHandler, 635 | $watermark, 636 | $positionX, 637 | $positionY, 638 | 0, 639 | 0, 640 | imagesx($watermark), 641 | imagesy($watermark), 642 | $this->getWatermarkImageOpacity() 643 | ); 644 | } elseif ($this->getWatermarkPosition() == self::POSITION_TOP_LEFT) { 645 | $this->imagecopymergeWithAlphaFix( 646 | $this->_imageHandler, 647 | $watermark, 648 | $positionX, 649 | $positionY, 650 | 0, 651 | 0, 652 | imagesx($watermark), 653 | imagesy($watermark), 654 | $this->getWatermarkImageOpacity() 655 | ); 656 | } elseif ($this->getWatermarkPosition() == self::POSITION_BOTTOM_RIGHT) { 657 | $positionX = $this->_imageSrcWidth - imagesx($watermark); 658 | $positionY = $this->_imageSrcHeight - imagesy($watermark); 659 | $this->imagecopymergeWithAlphaFix( 660 | $this->_imageHandler, 661 | $watermark, 662 | $positionX, 663 | $positionY, 664 | 0, 665 | 0, 666 | imagesx($watermark), 667 | imagesy($watermark), 668 | $this->getWatermarkImageOpacity() 669 | ); 670 | } elseif ($this->getWatermarkPosition() == self::POSITION_BOTTOM_LEFT) { 671 | $positionY = $this->_imageSrcHeight - imagesy($watermark); 672 | $this->imagecopymergeWithAlphaFix( 673 | $this->_imageHandler, 674 | $watermark, 675 | $positionX, 676 | $positionY, 677 | 0, 678 | 0, 679 | imagesx($watermark), 680 | imagesy($watermark), 681 | $this->getWatermarkImageOpacity() 682 | ); 683 | } 684 | 685 | if ($tile === false && $merged === false) { 686 | $this->imagecopymergeWithAlphaFix( 687 | $this->_imageHandler, 688 | $watermark, 689 | $positionX, 690 | $positionY, 691 | 0, 692 | 0, 693 | imagesx($watermark), 694 | imagesy($watermark), 695 | $this->getWatermarkImageOpacity() 696 | ); 697 | } else { 698 | $offsetX = $positionX; 699 | $offsetY = $positionY; 700 | while ($offsetY <= $this->_imageSrcHeight + imagesy($watermark)) { 701 | while ($offsetX <= $this->_imageSrcWidth + imagesx($watermark)) { 702 | $this->imagecopymergeWithAlphaFix( 703 | $this->_imageHandler, 704 | $watermark, 705 | $offsetX, 706 | $offsetY, 707 | 0, 708 | 0, 709 | imagesx($watermark), 710 | imagesy($watermark), 711 | $this->getWatermarkImageOpacity() 712 | ); 713 | $offsetX += imagesx($watermark); 714 | } 715 | $offsetX = $positionX; 716 | $offsetY += imagesy($watermark); 717 | } 718 | } 719 | 720 | return $watermark; 721 | } 722 | 723 | /** 724 | * Create watermark. 725 | * 726 | * @param resource $watermark 727 | * @param string $width 728 | * @param string $height 729 | * @return false|resource 730 | */ 731 | private function createWaterMark($watermark, string $width, string $height) 732 | { 733 | $newWatermark = imagecreatetruecolor($width, $height); 734 | imagealphablending($newWatermark, false); 735 | $col = imagecolorallocate($newWatermark, 255, 255, 255); 736 | imagecolortransparent($newWatermark, $col); 737 | imagefilledrectangle($newWatermark, 0, 0, $width, $height, $col); 738 | imagesavealpha($newWatermark, true); 739 | imagecopyresampled( 740 | $newWatermark, 741 | $watermark, 742 | 0, 743 | 0, 744 | 0, 745 | 0, 746 | $width, 747 | $height, 748 | imagesx($watermark), 749 | imagesy($watermark) 750 | ); 751 | 752 | return $newWatermark; 753 | } 754 | 755 | /** 756 | * Crop image 757 | * 758 | * @param int $top 759 | * @param int $left 760 | * @param int $right 761 | * @param int $bottom 762 | * @return bool 763 | */ 764 | public function crop($top = 0, $left = 0, $right = 0, $bottom = 0) 765 | { 766 | if ($left == 0 && $top == 0 && $right == 0 && $bottom == 0) { 767 | return false; 768 | } 769 | 770 | $newWidth = $this->_imageSrcWidth - $left - $right; 771 | $newHeight = $this->_imageSrcHeight - $top - $bottom; 772 | 773 | $canvas = imagecreatetruecolor($newWidth, $newHeight); 774 | 775 | if ($this->_fileType == IMAGETYPE_PNG) { 776 | $this->_saveAlpha($canvas); 777 | } 778 | 779 | imagecopyresampled( 780 | $canvas, 781 | $this->_imageHandler, 782 | 0, 783 | 0, 784 | $left, 785 | $top, 786 | $newWidth, 787 | $newHeight, 788 | $newWidth, 789 | $newHeight 790 | ); 791 | $this->imageDestroy(); 792 | $this->_imageHandler = $canvas; 793 | $this->refreshImageDimensions(); 794 | return true; 795 | } 796 | 797 | /** 798 | * Checks required dependencies 799 | * 800 | * @return void 801 | * @throws \RuntimeException If some of dependencies are missing 802 | */ 803 | public function checkDependencies() 804 | { 805 | foreach ($this->_requiredExtensions as $value) { 806 | if (!extension_loaded($value)) { 807 | throw new \RuntimeException("Required PHP extension '{$value}' was not loaded."); 808 | } 809 | } 810 | } 811 | 812 | /** 813 | * Reassign image dimensions 814 | * 815 | * @return void 816 | */ 817 | public function refreshImageDimensions() 818 | { 819 | $this->_imageSrcWidth = imagesx($this->_imageHandler); 820 | $this->_imageSrcHeight = imagesy($this->_imageHandler); 821 | } 822 | 823 | /** 824 | * Standard destructor. Destroy stored information about image 825 | */ 826 | public function __destruct() 827 | { 828 | $this->imageDestroy(); 829 | } 830 | 831 | /** 832 | * Helper function to free up memory associated with _imageHandler resource 833 | * 834 | * @return void 835 | */ 836 | private function imageDestroy() 837 | { 838 | if (is_resource($this->_imageHandler)) { 839 | imagedestroy($this->_imageHandler); 840 | } 841 | } 842 | 843 | /** 844 | * Fixes saving PNG alpha channel 845 | * 846 | * @param resource $imageHandler 847 | * @return void 848 | */ 849 | private function _saveAlpha($imageHandler) 850 | { 851 | $background = imagecolorallocate($imageHandler, 0, 0, 0); 852 | imagecolortransparent($imageHandler, $background); 853 | imagealphablending($imageHandler, false); 854 | imagesavealpha($imageHandler, true); 855 | } 856 | 857 | /** 858 | * Returns rgba array of the specified pixel 859 | * 860 | * @param int $x 861 | * @param int $y 862 | * @return array 863 | */ 864 | public function getColorAt($x, $y) 865 | { 866 | $colorIndex = imagecolorat($this->_imageHandler, $x, $y); 867 | return imagecolorsforindex($this->_imageHandler, $colorIndex); 868 | } 869 | 870 | /** 871 | * Create Image from string 872 | * 873 | * @param string $text 874 | * @param string $font 875 | * @return \Magento\Framework\Image\Adapter\AbstractAdapter 876 | */ 877 | public function createPngFromString($text, $font = '') 878 | { 879 | $error = false; 880 | $this->_resized = true; 881 | try { 882 | $this->_createImageFromTtfText($text, $font); 883 | } catch (\Exception $e) { 884 | $error = true; 885 | } 886 | 887 | if ($error || empty($this->_imageHandler)) { 888 | $this->_createImageFromText($text); 889 | } 890 | 891 | return $this; 892 | } 893 | 894 | /** 895 | * Create Image using standard font 896 | * 897 | * @param string $text 898 | * @return void 899 | */ 900 | protected function _createImageFromText($text) 901 | { 902 | $width = imagefontwidth($this->_fontSize) * strlen((string)$text); 903 | $height = imagefontheight($this->_fontSize); 904 | 905 | $this->_createEmptyImage($width, $height); 906 | 907 | $black = imagecolorallocate($this->_imageHandler, 0, 0, 0); 908 | imagestring($this->_imageHandler, $this->_fontSize, 0, 0, $text, $black); 909 | } 910 | 911 | /** 912 | * Create Image using ttf font 913 | * 914 | * Note: This function requires both the GD library and the FreeType library 915 | * 916 | * @param string $text 917 | * @param string $font 918 | * @return void 919 | * @throws \InvalidArgumentException 920 | */ 921 | protected function _createImageFromTtfText($text, $font) 922 | { 923 | $boundingBox = imagettfbbox($this->_fontSize, 0, $font, $text); 924 | $width = abs($boundingBox[4] - $boundingBox[0]); 925 | $height = abs($boundingBox[5] - $boundingBox[1]); 926 | 927 | $this->_createEmptyImage($width, $height); 928 | 929 | $black = imagecolorallocate($this->_imageHandler, 0, 0, 0); 930 | $result = imagettftext( 931 | $this->_imageHandler, 932 | $this->_fontSize, 933 | 0, 934 | 0, 935 | $height - $boundingBox[1], 936 | $black, 937 | $font, 938 | $text 939 | ); 940 | if ($result === false) { 941 | throw new \InvalidArgumentException('Unable to create TTF text'); 942 | } 943 | } 944 | 945 | /** 946 | * Create empty image with transparent background 947 | * 948 | * @param int $width 949 | * @param int $height 950 | * @return void 951 | */ 952 | protected function _createEmptyImage($width, $height) 953 | { 954 | $this->_fileType = IMAGETYPE_PNG; 955 | $image = imagecreatetruecolor($width, $height); 956 | $colorWhite = imagecolorallocatealpha($image, 255, 255, 255, 127); 957 | 958 | imagealphablending($image, true); 959 | imagesavealpha($image, true); 960 | 961 | imagefill($image, 0, 0, $colorWhite); 962 | $this->imageDestroy(); 963 | $this->_imageHandler = $image; 964 | } 965 | 966 | /** 967 | * Fix an issue with the usage of imagecopymerge where the alpha channel is lost 968 | * 969 | * @param resource $dst_im 970 | * @param resource $src_im 971 | * @param int $dst_x 972 | * @param int $dst_y 973 | * @param int $src_x 974 | * @param int $src_y 975 | * @param int $src_w 976 | * @param int $src_h 977 | * @param int $pct 978 | * @return bool 979 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 980 | * @SuppressWarnings(PHPMD.NPathComplexity) 981 | */ 982 | private function imagecopymergeWithAlphaFix( 983 | $dst_im, 984 | $src_im, 985 | $dst_x, 986 | $dst_y, 987 | $src_x, 988 | $src_y, 989 | $src_w, 990 | $src_h, 991 | $pct 992 | ) { 993 | if ($pct >= 100) { 994 | if (false === imagealphablending($dst_im, true)) { 995 | return false; 996 | } 997 | return imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); 998 | } 999 | 1000 | if ($pct < 0) { 1001 | return false; 1002 | } 1003 | 1004 | $sizeX = imagesx($src_im); 1005 | $sizeY = imagesy($src_im); 1006 | if (false === $sizeX || false === $sizeY) { 1007 | return false; 1008 | } 1009 | 1010 | $tmpImg = imagecreatetruecolor($src_w, $src_h); 1011 | if (false === $tmpImg) { 1012 | return false; 1013 | } 1014 | 1015 | if (false === imagealphablending($tmpImg, false)) { 1016 | return false; 1017 | } 1018 | 1019 | if (false === imagesavealpha($tmpImg, true)) { 1020 | return false; 1021 | } 1022 | 1023 | if (false === imagecopy($tmpImg, $src_im, 0, 0, 0, 0, $sizeX, $sizeY)) { 1024 | return false; 1025 | } 1026 | 1027 | $transparency = (int) (127 - (($pct * 127) / 100)); 1028 | if (false === imagefilter($tmpImg, IMG_FILTER_COLORIZE, 0, 0, 0, $transparency)) { 1029 | return false; 1030 | } 1031 | 1032 | if (false === imagealphablending($dst_im, true)) { 1033 | return false; 1034 | } 1035 | 1036 | if (false === imagesavealpha($dst_im, true)) { 1037 | return false; 1038 | } 1039 | 1040 | $result = imagecopy($dst_im, $tmpImg, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); 1041 | imagedestroy($tmpImg); 1042 | 1043 | return $result; 1044 | } 1045 | 1046 | /** 1047 | * Create truecolor image copy of current image 1048 | * 1049 | * @return resource 1050 | */ 1051 | private function createTruecolorImageCopy() 1052 | { 1053 | $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha); 1054 | 1055 | $newImage = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); 1056 | 1057 | if ($isAlpha) { 1058 | $this->_saveAlpha($newImage); 1059 | } 1060 | 1061 | imagecopy($newImage, $this->_imageHandler, 0, 0, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight); 1062 | 1063 | return $newImage; 1064 | } 1065 | } 1066 | -------------------------------------------------------------------------------- /Plugin/App/MediaPlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 63 | $this->imageConfig = $imageConfig; 64 | $this->request = $request; 65 | $this->logger = $logger; 66 | 67 | $this->directoryPub = $filesystem->getDirectoryWrite( 68 | DirectoryList::PUB, 69 | Filesystem\DriverPool::FILE 70 | ); 71 | $this->directoryMedia = $filesystem->getDirectoryWrite( 72 | DirectoryList::MEDIA, 73 | Filesystem\DriverPool::FILE 74 | ); 75 | } 76 | 77 | /** 78 | * When trying to process a vector image, just copy it to the cache folder instead of resizing 79 | * 80 | * @param Media $subject 81 | * @return array 82 | */ 83 | public function beforeLaunch(Media $subject) 84 | { 85 | try { 86 | $relativeFileName = $this->getRelativeFileName(); 87 | 88 | if ($this->imageHelper->isVectorImage($relativeFileName)) { 89 | $originalImage = $this->getOriginalImage($relativeFileName); 90 | $originalImagePath = $this->directoryMedia->getAbsolutePath( 91 | $this->imageConfig->getMediaPath($originalImage) 92 | ); 93 | 94 | $this->directoryMedia->copyFile( 95 | $originalImagePath, 96 | $this->directoryPub->getAbsolutePath($relativeFileName) 97 | ); 98 | } 99 | } catch (\Exception $e) { 100 | $this->logger->error('Could not process vector image', [ 101 | 'message' => $e->getMessage() 102 | ]); 103 | } 104 | 105 | return []; 106 | } 107 | 108 | /** 109 | * Get relative file name 110 | * 111 | * @return string 112 | */ 113 | private function getRelativeFileName() 114 | { 115 | return str_replace('..', '', ltrim($this->request->getPathInfo(), '/')); 116 | } 117 | 118 | /** 119 | * Find the path to the original image of the cache path 120 | * 121 | * @param string $resizedImagePath 122 | * @return string 123 | */ 124 | private function getOriginalImage(string $resizedImagePath): string 125 | { 126 | return preg_replace('|^.*?((?:/([^/])/([^/])/\2\3)?/?[^/]+$)|', '$1', $resizedImagePath); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Plugin/Controller/Adminhtml/Wysiwyg/DirectivePlugin.php: -------------------------------------------------------------------------------- 1 | urlDecoder = $urlDecoder; 52 | $this->filter = $filter; 53 | $this->resultRawFactory = $resultRawFactory; 54 | $this->imageHelper = $imageHelper; 55 | } 56 | 57 | /** 58 | * Handle vector images for media storage thumbnails 59 | * 60 | * @param Directive $subject 61 | * @param callable $proceed 62 | * @return Raw 63 | */ 64 | public function aroundExecute(Directive $subject, callable $proceed) 65 | { 66 | try { 67 | $directive = $subject->getRequest()->getParam('___directive'); 68 | $directive = $this->urlDecoder->decode($directive); 69 | $imagePath = $this->filter->filter($directive); 70 | 71 | if (!$this->imageHelper->isVectorImage($imagePath)) { 72 | throw new LocalizedException(__('This is not a vector image')); 73 | } 74 | 75 | /** @var Raw $resultRaw */ 76 | $resultRaw = $this->resultRawFactory->create(); 77 | $resultRaw->setHeader('Content-Type', 'image/svg+xml'); 78 | $resultRaw->setContents(file_get_contents($imagePath)); 79 | 80 | return $resultRaw; 81 | } catch (\Exception $e) { 82 | return $proceed(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Plugin/Controller/Adminhtml/Wysiwyg/Images/ThumbnailPlugin.php: -------------------------------------------------------------------------------- 1 | wysiwygImages = $wysiwygImages; 45 | $this->resultRawFactory = $resultRawFactory; 46 | $this->imageHelper = $imageHelper; 47 | } 48 | 49 | /** 50 | * Handle vector images for media storage thumbnails 51 | * 52 | * @param Thumbnail $subject 53 | * @param callable $proceed 54 | * @return Raw 55 | */ 56 | public function aroundExecute(Thumbnail $subject, callable $proceed) 57 | { 58 | try { 59 | $file = $subject->getRequest()->getParam('file'); 60 | $file = $this->wysiwygImages->idDecode($file); 61 | $thumb = $subject->getStorage()->resizeOnTheFly($file); 62 | 63 | if (!$this->imageHelper->isVectorImage($thumb)) { 64 | throw new LocalizedException(__('This is not a vector image')); 65 | } 66 | 67 | /** @var Raw $resultRaw */ 68 | $resultRaw = $this->resultRawFactory->create(); 69 | $resultRaw->setHeader('Content-Type', 'image/svg+xml'); 70 | $resultRaw->setContents(file_get_contents($thumb)); 71 | 72 | return $resultRaw; 73 | } catch (\Exception $e) { 74 | return $proceed(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Plugin/Design/Backend/ImagePlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 27 | } 28 | 29 | /** 30 | * Extend allowed extensions for theme files (logo, favicon, etc.) 31 | * 32 | * @param Image $subject 33 | * @param $extensions 34 | * @return array 35 | */ 36 | public function afterGetAllowedExtensions(Image $subject, $extensions) 37 | { 38 | $extensions = array_merge( 39 | $extensions, 40 | array_values($this->imageHelper->getVectorExtensions()), 41 | array_values($this->imageHelper->getWebImageExtensions()) 42 | ); 43 | 44 | return $extensions; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Plugin/File/UploaderPlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 27 | } 28 | 29 | /** 30 | * Add web images to the list ollowed extension for media storage 31 | * 32 | * @param Uploader $uploader 33 | * @param array $extensions 34 | * @return array 35 | */ 36 | public function beforeSetAllowedExtensions(Uploader $uploader, $extensions = []) 37 | { 38 | $extensions = array_merge( 39 | $extensions, 40 | array_values($this->imageHelper->getVectorExtensions()), 41 | array_values($this->imageHelper->getWebImageExtensions()) 42 | ); 43 | 44 | return [$extensions]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Plugin/File/Validator/NotProtectedExtensionPlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 27 | } 28 | 29 | /** 30 | * Remove vector images from protected extensions list 31 | * 32 | * @param NotProtectedExtension $subject 33 | * @param $result 34 | * @return string|string[] 35 | */ 36 | public function afterGetProtectedFileExtensions(NotProtectedExtension $subject, $result) 37 | { 38 | $vectorExtensions = $this->imageHelper->getVectorExtensions(); 39 | 40 | if (is_string($result)) { 41 | $result = explode(',', $result); 42 | } 43 | 44 | foreach (array_keys($result) as $extension) { 45 | if (in_array($extension, $vectorExtensions)) { 46 | unset($result[$extension]); 47 | } 48 | } 49 | 50 | return $result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Plugin/Product/Gallery/ProcessorPlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 63 | $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); 64 | $this->mediaConfig = $mediaConfig; 65 | $this->fileStorageDb = $fileStorageDb; 66 | $this->attributeRepository = $attributeRepository; 67 | $this->mime = $mime; 68 | } 69 | 70 | /** 71 | * We have to duplicate so many code lines because Magento hardcoded allowed image extensions 72 | * 73 | * @param Product\Gallery\Processor $subject 74 | * @param callable $proceed 75 | * @param Product $product 76 | * @param $file 77 | * @param $mediaAttribute 78 | * @param $move 79 | * @param $exclude 80 | * @return string 81 | */ 82 | public function aroundAddImage( 83 | Product\Gallery\Processor $subject, 84 | callable $proceed, 85 | Product $product, 86 | $file, 87 | $mediaAttribute = null, 88 | $move = false, 89 | $exclude = true 90 | ) { 91 | $pathinfo = pathinfo($file); 92 | if (!in_array($pathinfo['extension'], $this->imageHelper->getWebImageExtensions())) { 93 | return $proceed($product, $file, $mediaAttribute, $move, $exclude); 94 | } 95 | 96 | $file = $this->mediaDirectory->getRelativePath($file); 97 | if (!$this->mediaDirectory->isFile($file)) { 98 | throw new LocalizedException(__("The image doesn't exist.")); 99 | } 100 | 101 | $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($pathinfo['basename']); 102 | $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); 103 | $fileName = $dispersionPath . '/' . $fileName; 104 | 105 | $fileName = $this->getNotDuplicatedFilename($fileName, $dispersionPath); 106 | 107 | $destinationFile = $this->mediaConfig->getTmpMediaPath($fileName); 108 | 109 | try { 110 | /** @var $storageHelper \Magento\MediaStorage\Helper\File\Storage\Database */ 111 | $storageHelper = $this->fileStorageDb; 112 | if ($move) { 113 | $this->mediaDirectory->renameFile($file, $destinationFile); 114 | 115 | //If this is used, filesystem should be configured properly 116 | $storageHelper->saveFile($this->mediaConfig->getTmpMediaShortUrl($fileName)); 117 | } else { 118 | $this->mediaDirectory->copyFile($file, $destinationFile); 119 | 120 | $storageHelper->saveFile($this->mediaConfig->getTmpMediaShortUrl($fileName)); 121 | } 122 | } catch (\Exception $e) { 123 | throw new LocalizedException(__('The "%1" file couldn\'t be moved.', $e->getMessage())); 124 | } 125 | 126 | $fileName = str_replace('\\', '/', $fileName); 127 | 128 | $attrCode = $this->getAttribute()->getAttributeCode(); 129 | $mediaGalleryData = $product->getData($attrCode); 130 | $position = 0; 131 | 132 | $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($destinationFile); 133 | $imageMimeType = $this->mime->getMimeType($absoluteFilePath); 134 | $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); 135 | $imageBase64 = base64_encode($imageContent); 136 | $imageName = $pathinfo['filename']; 137 | 138 | if (!is_array($mediaGalleryData)) { 139 | $mediaGalleryData = ['images' => []]; 140 | } 141 | 142 | foreach ($mediaGalleryData['images'] as &$image) { 143 | if (isset($image['position']) && $image['position'] > $position) { 144 | $position = $image['position']; 145 | } 146 | } 147 | 148 | $position++; 149 | $mediaGalleryData['images'][] = [ 150 | 'file' => $fileName, 151 | 'position' => $position, 152 | 'label' => '', 153 | 'disabled' => (int)$exclude, 154 | 'media_type' => 'image', 155 | 'types' => $mediaAttribute, 156 | 'content' => [ 157 | 'data' => [ 158 | ImageContentInterface::NAME => $imageName, 159 | ImageContentInterface::BASE64_ENCODED_DATA => $imageBase64, 160 | ImageContentInterface::TYPE => $imageMimeType, 161 | ] 162 | ] 163 | ]; 164 | 165 | $product->setData($attrCode, $mediaGalleryData); 166 | 167 | if ($mediaAttribute !== null) { 168 | $this->setMediaAttribute($product, $mediaAttribute, $fileName); 169 | } 170 | 171 | return $fileName; 172 | } 173 | 174 | /** 175 | * Get filename which is not duplicated with other files in media temporary and media directories 176 | * 177 | * @param string $fileName 178 | * @param string $dispersionPath 179 | * @return string 180 | * @since 101.0.0 181 | */ 182 | protected function getNotDuplicatedFilename($fileName, $dispersionPath) 183 | { 184 | $fileMediaName = $dispersionPath . '/' 185 | . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getMediaPath($fileName)); 186 | $fileTmpMediaName = $dispersionPath . '/' 187 | . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getTmpMediaPath($fileName)); 188 | 189 | if ($fileMediaName != $fileTmpMediaName) { 190 | if ($fileMediaName != $fileName) { 191 | return $this->getNotDuplicatedFilename( 192 | $fileMediaName, 193 | $dispersionPath 194 | ); 195 | } elseif ($fileTmpMediaName != $fileName) { 196 | return $this->getNotDuplicatedFilename( 197 | $fileTmpMediaName, 198 | $dispersionPath 199 | ); 200 | } 201 | } 202 | 203 | return $fileMediaName; 204 | } 205 | 206 | /** 207 | * Return media_gallery attribute 208 | * 209 | * @return \Magento\Catalog\Api\Data\ProductAttributeInterface 210 | * @since 101.0.0 211 | */ 212 | public function getAttribute() 213 | { 214 | if (!$this->attribute) { 215 | $this->attribute = $this->attributeRepository->get('media_gallery'); 216 | } 217 | 218 | return $this->attribute; 219 | } 220 | 221 | /** 222 | * Set media attribute value 223 | * 224 | * @param \Magento\Catalog\Model\Product $product 225 | * @param string|string[] $mediaAttribute 226 | * @param string $value 227 | * @return $this 228 | * @since 101.0.0 229 | */ 230 | public function setMediaAttribute(\Magento\Catalog\Model\Product $product, $mediaAttribute, $value) 231 | { 232 | $mediaAttributeCodes = $this->mediaConfig->getMediaAttributeCodes(); 233 | 234 | if (is_array($mediaAttribute)) { 235 | foreach ($mediaAttribute as $attribute) { 236 | if (in_array($attribute, $mediaAttributeCodes)) { 237 | $product->setData($attribute, $value); 238 | } 239 | } 240 | } elseif (in_array($mediaAttribute, $mediaAttributeCodes)) { 241 | $product->setData($mediaAttribute, $value); 242 | } 243 | 244 | return $this; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Plugin/Swatches/Helper/MediaPlugin.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 46 | $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); 47 | } 48 | 49 | /** 50 | * Skip resizing SVG images for swatches. Instead, just copy the image to the size folders. 51 | * 52 | * @param Media $subject 53 | * @param callable $proceed 54 | * @param $imageUrl 55 | * @return void 56 | * @throws \Magento\Framework\Exception\FileSystemException 57 | */ 58 | public function aroundGenerateSwatchVariations(Media $subject, callable $proceed, $imageUrl) 59 | { 60 | if ($this->helper->isVectorImage($imageUrl)) { 61 | $this->subject = $subject; 62 | 63 | $absoluteImagePath = $this->getOriginalFilePath($imageUrl); 64 | foreach ($this->swatchImageTypes as $swatchType) { 65 | $imageConfig = $this->subject->getImageConfig(); 66 | $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); 67 | 68 | $this->mediaDirectory->copyFile($absoluteImagePath, $swatchNamePath['path_for_save'] . '/' . $swatchNamePath['name']); 69 | } 70 | } else { 71 | return $proceed($imageUrl); 72 | } 73 | } 74 | 75 | /** 76 | * @param string $file 77 | * @return string 78 | */ 79 | private function getOriginalFilePath($file) 80 | { 81 | return $this->mediaDirectory->getAbsolutePath($this->subject->getAttributeSwatchPath($file)); 82 | } 83 | 84 | /** 85 | * Generate swatch path and name for saving 86 | * 87 | * @param array $imageConfig 88 | * @param string $imageUrl 89 | * @param string $swatchType 90 | * @return array 91 | */ 92 | protected function generateNamePath($imageConfig, $imageUrl, $swatchType) 93 | { 94 | $fileName = $this->prepareFileName($imageUrl); 95 | $absolutePath = $this->mediaDirectory->getAbsolutePath($this->subject->getSwatchCachePath($swatchType)); 96 | return [ 97 | 'path_for_save' => $absolutePath . $this->subject->getFolderNameSize($swatchType, $imageConfig) . $fileName['path'], 98 | 'name' => $fileName['name'] 99 | ]; 100 | } 101 | 102 | /** 103 | * Image url /m/a/magento.png return ['name' => 'magento.png', 'path => '/m/a'] 104 | * 105 | * @param string $imageUrl 106 | * @return array 107 | */ 108 | protected function prepareFileName($imageUrl) 109 | { 110 | $fileArray = explode('/', $imageUrl); 111 | $fileName = array_pop($fileArray); 112 | $filePath = implode('/', $fileArray); 113 | return ['name' => $fileName, 'path' => $filePath]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Plugin/Wysiwyg/Images/StoragePlugin.php: -------------------------------------------------------------------------------- 1 | imageHelper = $imageHelper; 27 | } 28 | 29 | /** 30 | * Skip resizing vector images 31 | * 32 | * @param Storage $storage 33 | * @param callable $proceed 34 | * @param $source 35 | * @param bool $keepRatio 36 | * @return mixed 37 | */ 38 | public function aroundResizeFile(Storage $storage, callable $proceed, $source, $keepRatio = true) 39 | { 40 | if ($this->imageHelper->isVectorImage($source)) { 41 | return $source; 42 | } 43 | 44 | return $proceed($source, $keepRatio); 45 | } 46 | 47 | /** 48 | * Return original file path as thumbnail for vector images 49 | * 50 | * @param Storage $storage 51 | * @param callable $proceed 52 | * @param $filePath 53 | * @param false $checkFile 54 | */ 55 | public function aroundGetThumbnailPath(Storage $storage, callable $proceed, $filePath, $checkFile = false) 56 | { 57 | if ($this->imageHelper->isVectorImage($filePath)) { 58 | return $filePath; 59 | } 60 | 61 | return $proceed($filePath, $checkFile); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Packagist](https://img.shields.io/packagist/v/magestyapps/module-web-images.svg)](https://packagist.org/packages/magestyapps/module-web-images) [![Packagist](https://img.shields.io/packagist/dt/magestyapps/module-web-images.svg)](https://packagist.org/packages/magestyapps/module-web-images) 2 | 3 | # Upload SVG and WebP images in Magento 2 4 | 5 | This extension for Magento 2 allows uploading SVG and WebP images in the following sections: 6 | * Page Builder editor 7 | * Wysiwyg editor 8 | * Theme logo and favicon 9 | * Product media gallery 10 | * Attribute option swatch images 11 | * Category image 12 | * Custom image uploader fields 13 | 14 | **IMPORTANT:** *if you need to upload any other image format or you need to upload it in any other Magento 2 area - please just drop us a line at [alex@magestyapps.com](mailto:alex@magestyapps.com?subject=Extend%20MagestyApps_WebImages%20extension) and we will update the extension* 15 | 16 | **IMPORTANT:** *if you like the extension, could you please add a star to this GitHub repository in the top right corner. This is really important for us. Thanks.* 17 | 18 | ## Magento Version Compatibility 19 | | Supported Magento Version | Compatible Module Version | 20 | |---------------------------|---------------------------| 21 | | 2.4.7 | 1.2.* | 22 | | 2.4.6 | 1.1.* | 23 | | 2.4.5 | 1.1.* | 24 | | 2.4.4 or older | Not supported | 25 | 26 | ## Installation 27 | 28 | ### Using Composer (recommended) 29 | 1) Go to your Magento root folder 30 | 2) Download the extension using composer: 31 | 32 | *For Magento 2.4.7 or newer:* 33 | ``` 34 | composer require magestyapps/module-web-images 35 | ``` 36 | *For Magento 2.4.5 or 2.4.6:* 37 | ``` 38 | composer require magestyapps/module-web-images:^1.1 39 | ``` 40 | 3) Run setup commands: 41 | 42 | ``` 43 | php bin/magento setup:upgrade; 44 | php bin/magento setup:di:compile; 45 | php bin/magento setup:static-content:deploy -f; 46 | ``` 47 | 48 | ### Manually 49 | 1) Go to your Magento root folder: 50 | 51 | ``` 52 | cd 53 | ``` 54 | 55 | 2) Copy extension files to *app/code/MagestyApps/WebImages* folder: 56 | ``` 57 | git clone https://github.com/MagestyApps/module-web-images.git app/code/MagestyApps/WebImages 58 | ``` 59 | ***NOTE:*** *alternatively, you can manually create the folder and copy the extension files there.* 60 | 61 | 3) Run setup commands: 62 | 63 | ``` 64 | php bin/magento setup:upgrade; 65 | php bin/magento setup:di:compile; 66 | php bin/magento setup:static-content:deploy -f; 67 | ``` 68 | 69 | ### Possible issues 70 | *Problem:* An image gets uploaded to the server but not accessible in browser. 71 | 72 | *Solution:* Most likely, this is related to your nginx/apache restrictions. Please, make sure that the requested image extension is allowed by the web server configuration. 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magestyapps/module-web-images", 3 | "description": "Upload SVG and WebP images in Magento 2", 4 | "require": { 5 | "php": ">=7.4", 6 | "magento/module-backend": ">=102.0.7" 7 | }, 8 | "type": "magento2-module", 9 | "version": "1.2.0", 10 | "license": [ 11 | "OSL-3.0", 12 | "AFL-3.0" 13 | ], 14 | "autoload": { 15 | "files": [ "registration.php" ], 16 | "psr-4": { 17 | "MagestyApps\\WebImages\\": "" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | svg 14 | svg+xml 15 | 16 | 17 | webp 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | MagestyApps\WebImages\Model\Image\Adapter\Gd2 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | image/svg 14 | image/svg+xml 15 | image/webp 16 | 17 | 18 | image/svg 19 | image/svg+xml 20 | image/webp 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | svg 29 | webp 30 | 31 | 32 | image/svg 33 | image/svg+xml 34 | image/webp 35 | 36 | 37 | 38 | 39 | 40 | 41 | svg 42 | webp 43 | 44 | 45 | 46 | 47 | 48 | 49 | image/svg 50 | image/svg+xml 51 | image/webp 52 | 53 | 54 | 55 | 56 | 57 | 58 | svg 59 | webp 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | MagestyApps\WebImages\Model\AssetFactory 102 | 103 | 104 | 105 | 106 | MagestyApps\WebImages\Model\File\UploaderFactory 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | { 70 | let fileSize, 71 | tmpl; 72 | 73 | fileSize = typeof currentFile.size == 'undefined' ? 74 | $.mage.__('We could not detect a size.') : 75 | byteConvert(currentFile.size); 76 | 77 | // check if file is allowed to upload and resize 78 | allowedResize = $.inArray(currentFile.extension, allowedExt) !== -1; 79 | 80 | if (!allowedResize) { 81 | fileUploader.aggregateError(currentFile.name, 82 | $.mage.__('Disallowed file type.')); 83 | fileUploader.onLoadingStop(); 84 | return false; 85 | } 86 | 87 | fileId = Math.random().toString(33).substr(2, 18); 88 | 89 | tmpl = progressTmpl({ 90 | data: { 91 | name: currentFile.name, 92 | size: fileSize, 93 | id: fileId 94 | } 95 | }); 96 | 97 | // code to allow duplicate files from same folder 98 | const modifiedFile = { 99 | ...currentFile, 100 | id: currentFile.id + '-' + fileId, 101 | tempFileId: fileId 102 | }; 103 | 104 | $(tmpl).appendTo(self.element); 105 | return modifiedFile; 106 | }, 107 | 108 | meta: { 109 | 'form_key': window.FORM_KEY, 110 | isAjax : true 111 | } 112 | }); 113 | 114 | // initialize Uppy upload 115 | uppy.use(Uppy.Dashboard, options); 116 | 117 | // Resize Image as per configuration 118 | if (this.options.isResizeEnabled) { 119 | uppy.use(Uppy.Compressor, { 120 | maxWidth: this.options.maxWidth, 121 | maxHeight: this.options.maxHeight, 122 | quality: 0.92, 123 | beforeDraw() { 124 | if (!allowedResize) { 125 | this.abort(); 126 | } 127 | } 128 | }); 129 | } 130 | 131 | // drop area for file upload 132 | uppy.use(Uppy.DropTarget, { 133 | target: targetElement, 134 | onDragOver: () => { 135 | // override Array.from method of legacy-build.min.js file 136 | Array.from = null; 137 | }, 138 | onDragLeave: () => { 139 | Array.from = arrayFromObj; 140 | } 141 | }); 142 | 143 | // upload files on server 144 | uppy.use(Uppy.XHRUpload, { 145 | endpoint: uploadUrl, 146 | fieldName: 'image' 147 | }); 148 | 149 | uppy.on('upload-success', (file, response) => { 150 | if (response.body && !response.body.error) { 151 | self.element.trigger('addItem', response.body); 152 | } else { 153 | fileUploader.aggregateError(file.name, response.body.error); 154 | } 155 | 156 | self.element.find('#' + file.tempFileId).remove(); 157 | }); 158 | 159 | uppy.on('upload-progress', (file, progress) => { 160 | let progressWidth = parseInt(progress.bytesUploaded / progress.bytesTotal * 100, 10), 161 | progressSelector = '#' + file.tempFileId + ' .progressbar-container .progressbar'; 162 | 163 | self.element.find(progressSelector).css('width', progressWidth + '%'); 164 | }); 165 | 166 | uppy.on('upload-error', (error, file) => { 167 | let progressSelector = '#' + file.tempFileId; 168 | 169 | self.element.find(progressSelector).removeClass('upload-progress').addClass('upload-failure') 170 | .delay(2000) 171 | .hide('highlight') 172 | .remove(); 173 | }); 174 | 175 | uppy.on('complete', () => { 176 | fileUploader.uploaderConfig.stop(); 177 | $(window).trigger('reload.MediaGallery'); 178 | Array.from = arrayFromObj; 179 | }); 180 | 181 | } 182 | }); 183 | 184 | return $.mage.mediaUploader; 185 | }); 186 | --------------------------------------------------------------------------------