ImageMagick.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Image\Adapter;
  7. class ImageMagick extends \Magento\Framework\Image\Adapter\AbstractAdapter
  8. {
  9. /**
  10. * The blur factor where > 1 is blurry, < 1 is sharp
  11. */
  12. const BLUR_FACTOR = 0.7;
  13. /**
  14. * Error messages
  15. */
  16. const ERROR_WATERMARK_IMAGE_ABSENT = 'Watermark Image absent.';
  17. const ERROR_WRONG_IMAGE = 'Image is not readable or file name is empty.';
  18. /**
  19. * Options Container
  20. *
  21. * @var array
  22. */
  23. protected $_options = [
  24. 'resolution' => ['x' => 72, 'y' => 72],
  25. 'small_image' => ['width' => 300, 'height' => 300],
  26. 'sharpen' => ['radius' => 4, 'deviation' => 1],
  27. ];
  28. /**
  29. * Set/get background color. Check Imagick::COLOR_* constants
  30. *
  31. * @param int|string|array $color
  32. * @return int
  33. */
  34. public function backgroundColor($color = null)
  35. {
  36. if ($color) {
  37. if (is_array($color)) {
  38. $color = "rgb(" . join(',', $color) . ")";
  39. }
  40. $pixel = new \ImagickPixel();
  41. if (is_numeric($color)) {
  42. $pixel->setColorValue($color, 1);
  43. } else {
  44. $pixel->setColor($color);
  45. }
  46. if ($this->_imageHandler) {
  47. $this->_imageHandler->setImageBackgroundColor($color);
  48. }
  49. } else {
  50. $pixel = $this->_imageHandler->getImageBackgroundColor();
  51. }
  52. $this->imageBackgroundColor = $pixel->getColorAsString();
  53. return $this->imageBackgroundColor;
  54. }
  55. /**
  56. * Open image for processing
  57. *
  58. * @param string $filename
  59. * @return void
  60. * @throws \Exception
  61. */
  62. public function open($filename)
  63. {
  64. $this->_fileName = $filename;
  65. $this->_checkCanProcess();
  66. $this->_getFileAttributes();
  67. try {
  68. $this->_imageHandler = new \Imagick($this->_fileName);
  69. } catch (\ImagickException $e) {
  70. throw new \Exception(sprintf('Unsupported image format. File: %s', $this->_fileName), $e->getCode(), $e);
  71. }
  72. $this->backgroundColor();
  73. $this->getMimeType();
  74. }
  75. /**
  76. * Save image to specific path.
  77. * If some folders of path does not exist they will be created
  78. *
  79. * @param null|string $destination
  80. * @param null|string $newName
  81. * @return void
  82. * @throws \Exception If destination path is not writable
  83. */
  84. public function save($destination = null, $newName = null)
  85. {
  86. $fileName = $this->_prepareDestination($destination, $newName);
  87. $this->_applyOptions();
  88. $this->_imageHandler->stripImage();
  89. $this->_imageHandler->writeImage($fileName);
  90. }
  91. /**
  92. * Apply options to image. Will be usable later when create an option container
  93. *
  94. * @return $this
  95. */
  96. protected function _applyOptions()
  97. {
  98. $this->_imageHandler->setImageCompressionQuality($this->quality());
  99. $this->_imageHandler->setImageCompression(\Imagick::COMPRESSION_JPEG);
  100. $this->_imageHandler->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH);
  101. $this->_imageHandler->setImageResolution(
  102. $this->_options['resolution']['x'],
  103. $this->_options['resolution']['y']
  104. );
  105. if (method_exists($this->_imageHandler, 'optimizeImageLayers')) {
  106. $this->_imageHandler->optimizeImageLayers();
  107. }
  108. return $this;
  109. }
  110. /**
  111. * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage
  112. * @return string
  113. */
  114. public function getImage()
  115. {
  116. $this->_applyOptions();
  117. return $this->_imageHandler->getImageBlob();
  118. }
  119. /**
  120. * Change the image size
  121. *
  122. * @param null|int $frameWidth
  123. * @param null|int $frameHeight
  124. * @return void
  125. */
  126. public function resize($frameWidth = null, $frameHeight = null)
  127. {
  128. $this->_checkCanProcess();
  129. $dims = $this->_adaptResizeValues($frameWidth, $frameHeight);
  130. $newImage = new \Imagick();
  131. $newImage->newImage(
  132. $dims['frame']['width'],
  133. $dims['frame']['height'],
  134. $this->_imageHandler->getImageBackgroundColor()
  135. );
  136. $this->_imageHandler->resizeImage(
  137. $dims['dst']['width'],
  138. $dims['dst']['height'],
  139. \Imagick::FILTER_CUBIC,
  140. self::BLUR_FACTOR
  141. );
  142. if ($this->_imageHandler->getImageWidth() < $this->_options['small_image']['width'] ||
  143. $this->_imageHandler->getImageHeight() < $this->_options['small_image']['height']
  144. ) {
  145. $this->_imageHandler->sharpenImage(
  146. $this->_options['sharpen']['radius'],
  147. $this->_options['sharpen']['deviation']
  148. );
  149. }
  150. $newImage->compositeImage(
  151. $this->_imageHandler,
  152. \Imagick::COMPOSITE_COPYOPACITY,
  153. $dims['dst']['x'],
  154. $dims['dst']['y']
  155. );
  156. $newImage->compositeImage(
  157. $this->_imageHandler,
  158. \Imagick::COMPOSITE_OVER,
  159. $dims['dst']['x'],
  160. $dims['dst']['y']
  161. );
  162. $newImage->setImageFormat($this->_imageHandler->getImageFormat());
  163. $this->_imageHandler->clear();
  164. $this->_imageHandler->destroy();
  165. $this->_imageHandler = $newImage;
  166. $this->refreshImageDimensions();
  167. }
  168. /**
  169. * Rotate image on specific angle
  170. *
  171. * @param int $angle
  172. * @return void
  173. */
  174. public function rotate($angle)
  175. {
  176. $this->_checkCanProcess();
  177. // compatibility with GD2 adapter
  178. $angle = 360 - $angle;
  179. $pixel = new \ImagickPixel();
  180. $pixel->setColor("rgb(" . $this->imageBackgroundColor . ")");
  181. $this->_imageHandler->rotateImage($pixel, $angle);
  182. $this->refreshImageDimensions();
  183. }
  184. /**
  185. * Crop image
  186. *
  187. * @param int $top
  188. * @param int $left
  189. * @param int $right
  190. * @param int $bottom
  191. * @return bool
  192. */
  193. public function crop($top = 0, $left = 0, $right = 0, $bottom = 0)
  194. {
  195. if ($left == 0 && $top == 0 && $right == 0 && $bottom == 0 || !$this->_canProcess()) {
  196. return false;
  197. }
  198. $newWidth = $this->_imageSrcWidth - $left - $right;
  199. $newHeight = $this->_imageSrcHeight - $top - $bottom;
  200. $this->_imageHandler->cropImage($newWidth, $newHeight, $left, $top);
  201. $this->refreshImageDimensions();
  202. return true;
  203. }
  204. /**
  205. * Add watermark to image
  206. *
  207. * @param string $imagePath
  208. * @param int $positionX
  209. * @param int $positionY
  210. * @param int $opacity
  211. * @param bool $tile
  212. * @return void
  213. * @throws \LogicException
  214. * @throws \Exception
  215. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  216. * @SuppressWarnings(PHPMD.NPathComplexity)
  217. */
  218. public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = 30, $tile = false)
  219. {
  220. if (empty($imagePath) || !file_exists($imagePath)) {
  221. throw new \LogicException(self::ERROR_WATERMARK_IMAGE_ABSENT);
  222. }
  223. $this->_checkCanProcess();
  224. $opacity = $this->getWatermarkImageOpacity() ? $this->getWatermarkImageOpacity() : $opacity;
  225. $opacity = (double)number_format($opacity / 100, 1);
  226. $watermark = new \Imagick($imagePath);
  227. if ($this->getWatermarkWidth() &&
  228. $this->getWatermarkHeight() &&
  229. $this->getWatermarkPosition() != self::POSITION_STRETCH
  230. ) {
  231. $watermark->resizeImage(
  232. $this->getWatermarkWidth(),
  233. $this->getWatermarkHeight(),
  234. \Imagick::FILTER_CUBIC,
  235. self::BLUR_FACTOR
  236. );
  237. }
  238. if (method_exists($watermark, 'getImageAlphaChannel')) {
  239. // available from imagick 6.4.0
  240. if ($watermark->getImageAlphaChannel() == 0) {
  241. $watermark->setImageAlphaChannel(\Imagick::ALPHACHANNEL_OPAQUE);
  242. }
  243. }
  244. $compositeChannels = \Imagick::CHANNEL_ALL;
  245. $watermark->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_OPACITY);
  246. $compositeChannels &= ~(\Imagick::CHANNEL_OPACITY);
  247. switch ($this->getWatermarkPosition()) {
  248. case self::POSITION_STRETCH:
  249. $watermark->sampleImage($this->_imageSrcWidth, $this->_imageSrcHeight);
  250. break;
  251. case self::POSITION_CENTER:
  252. $positionX = ($this->_imageSrcWidth - $watermark->getImageWidth()) / 2;
  253. $positionY = ($this->_imageSrcHeight - $watermark->getImageHeight()) / 2;
  254. break;
  255. case self::POSITION_TOP_RIGHT:
  256. $positionX = $this->_imageSrcWidth - $watermark->getImageWidth();
  257. break;
  258. case self::POSITION_BOTTOM_RIGHT:
  259. $positionX = $this->_imageSrcWidth - $watermark->getImageWidth();
  260. $positionY = $this->_imageSrcHeight - $watermark->getImageHeight();
  261. break;
  262. case self::POSITION_BOTTOM_LEFT:
  263. $positionY = $this->_imageSrcHeight - $watermark->getImageHeight();
  264. break;
  265. case self::POSITION_TILE:
  266. $positionX = 0;
  267. $positionY = 0;
  268. $tile = true;
  269. break;
  270. }
  271. try {
  272. if ($tile) {
  273. $offsetX = $positionX;
  274. $offsetY = $positionY;
  275. while ($offsetY <= $this->_imageSrcHeight + $watermark->getImageHeight()) {
  276. while ($offsetX <= $this->_imageSrcWidth + $watermark->getImageWidth()) {
  277. $this->_imageHandler->compositeImage(
  278. $watermark,
  279. \Imagick::COMPOSITE_OVER,
  280. $offsetX,
  281. $offsetY,
  282. $compositeChannels
  283. );
  284. $offsetX += $watermark->getImageWidth();
  285. }
  286. $offsetX = $positionX;
  287. $offsetY += $watermark->getImageHeight();
  288. }
  289. } else {
  290. $this->_imageHandler->compositeImage(
  291. $watermark,
  292. \Imagick::COMPOSITE_OVER,
  293. $positionX,
  294. $positionY,
  295. $compositeChannels
  296. );
  297. }
  298. } catch (\ImagickException $e) {
  299. throw new \Exception('Unable to create watermark.', $e->getCode(), $e);
  300. }
  301. // merge layers
  302. $this->_imageHandler->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
  303. $watermark->clear();
  304. $watermark->destroy();
  305. }
  306. /**
  307. * Checks required dependencies
  308. *
  309. * @return void
  310. * @throws \Exception If some of dependencies are missing
  311. */
  312. public function checkDependencies()
  313. {
  314. if (!class_exists('\Imagick', false)) {
  315. throw new \Exception("Required PHP extension 'Imagick' was not loaded.");
  316. }
  317. }
  318. /**
  319. * Reassign image dimensions
  320. *
  321. * @return void
  322. */
  323. public function refreshImageDimensions()
  324. {
  325. $this->_imageSrcWidth = $this->_imageHandler->getImageWidth();
  326. $this->_imageSrcHeight = $this->_imageHandler->getImageHeight();
  327. $this->_imageHandler->setImagePage($this->_imageSrcWidth, $this->_imageSrcHeight, 0, 0);
  328. }
  329. /**
  330. * Standard destructor. Destroy stored information about image
  331. */
  332. public function __destruct()
  333. {
  334. $this->destroy();
  335. }
  336. /**
  337. * Destroy stored information about image
  338. *
  339. * @return $this
  340. */
  341. public function destroy()
  342. {
  343. if (null !== $this->_imageHandler && $this->_imageHandler instanceof \Imagick) {
  344. $this->_imageHandler->clear();
  345. $this->_imageHandler->destroy();
  346. $this->_imageHandler = null;
  347. }
  348. return $this;
  349. }
  350. /**
  351. * Returns rgba array of the specified pixel
  352. *
  353. * @param int $x
  354. * @param int $y
  355. * @return array
  356. */
  357. public function getColorAt($x, $y)
  358. {
  359. $pixel = $this->_imageHandler->getImagePixelColor($x, $y);
  360. $color = $pixel->getColor();
  361. $rgbaColor = [
  362. 'red' => $color['r'],
  363. 'green' => $color['g'],
  364. 'blue' => $color['b'],
  365. 'alpha' => (1 - $color['a']) * 127,
  366. ];
  367. return $rgbaColor;
  368. }
  369. /**
  370. * Check whether the adapter can work with the image
  371. *
  372. * @throws \LogicException
  373. * @return true
  374. */
  375. protected function _checkCanProcess()
  376. {
  377. if (!$this->_canProcess()) {
  378. throw new \LogicException(self::ERROR_WRONG_IMAGE);
  379. }
  380. return true;
  381. }
  382. /**
  383. * Create Image from string
  384. *
  385. * @param string $text
  386. * @param string $font
  387. * @return \Magento\Framework\Image\Adapter\AbstractAdapter
  388. */
  389. public function createPngFromString($text, $font = '')
  390. {
  391. $image = $this->_getImagickObject();
  392. $draw = $this->_getImagickDrawObject();
  393. $color = $this->_getImagickPixelObject('#000000');
  394. $background = $this->_getImagickPixelObject('#ffffff00');
  395. // Transparent
  396. if (!empty($font)) {
  397. if (method_exists($image, 'setFont')) {
  398. $image->setFont($font);
  399. } elseif (method_exists($draw, 'setFont')) {
  400. $draw->setFont($font);
  401. }
  402. }
  403. // Font size for ImageMagick is set in pixels, while the for GD2 it is in points. 3/4 is ratio between them
  404. $draw->setFontSize($this->_fontSize * 4 / 3);
  405. $draw->setFillColor($color);
  406. $draw->setStrokeAntialias(true);
  407. $draw->setTextAntialias(true);
  408. $metrics = $image->queryFontMetrics($draw, $text);
  409. $draw->annotation(0, $metrics['ascender'], $text);
  410. $height = abs($metrics['ascender']) + abs($metrics['descender']);
  411. $image->newImage($metrics['textWidth'], $height, $background);
  412. $this->_fileType = IMAGETYPE_PNG;
  413. $image->setImageFormat('png');
  414. $image->drawImage($draw);
  415. $this->_imageHandler = $image;
  416. return $this;
  417. }
  418. /**
  419. * Get Imagick object
  420. *
  421. * @param mixed $files
  422. * @return \Imagick
  423. */
  424. protected function _getImagickObject($files = null)
  425. {
  426. return new \Imagick($files);
  427. }
  428. /**
  429. * Get ImagickDraw object
  430. *
  431. * @return \ImagickDraw
  432. */
  433. protected function _getImagickDrawObject()
  434. {
  435. return new \ImagickDraw();
  436. }
  437. /**
  438. * Get ImagickPixel object
  439. *
  440. * @param string|null $color
  441. * @return \ImagickPixel
  442. */
  443. protected function _getImagickPixelObject($color = null)
  444. {
  445. return new \ImagickPixel($color);
  446. }
  447. }