BaseImage.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. /**
  3. * @link https://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license https://www.yiiframework.com/license/
  6. */
  7. namespace yii\imagine;
  8. use Yii;
  9. use Imagine\Image\Box;
  10. use Imagine\Image\BoxInterface;
  11. use Imagine\Image\ImageInterface;
  12. use Imagine\Image\ImagineInterface;
  13. use Imagine\Image\ManipulatorInterface;
  14. use Imagine\Image\Point;
  15. use Imagine\Image\Palette\RGB;
  16. use Imagine\Filter\Basic\Autorotate;
  17. use yii\base\InvalidConfigException;
  18. use yii\base\InvalidParamException;
  19. use yii\helpers\ArrayHelper;
  20. /**
  21. * BaseImage provides concrete implementation for [[Image]].
  22. *
  23. * Do not use BaseImage. Use [[Image]] instead.
  24. *
  25. * @author Antonio Ramirez <amigo.cobos@gmail.com>
  26. * @author Qiang Xue <qiang.xue@gmail.com>
  27. * @since 2.0
  28. */
  29. class BaseImage
  30. {
  31. /**
  32. * GD2 driver definition for Imagine implementation using the GD library.
  33. */
  34. const DRIVER_GD2 = 'gd2';
  35. /**
  36. * imagick driver definition.
  37. */
  38. const DRIVER_IMAGICK = 'imagick';
  39. /**
  40. * gmagick driver definition.
  41. */
  42. const DRIVER_GMAGICK = 'gmagick';
  43. /**
  44. * @var ImagineInterface instance.
  45. */
  46. private static $_imagine;
  47. /**
  48. * @var array|string the driver to use. This can be either a single driver name or an array of driver names.
  49. * If the latter, the first available driver will be used.
  50. */
  51. public static $driver = [self::DRIVER_GMAGICK, self::DRIVER_IMAGICK, self::DRIVER_GD2];
  52. /**
  53. * @var string background color to use when creating thumbnails in `ImageInterface::THUMBNAIL_INSET` mode with
  54. * both width and height specified. Default is white.
  55. *
  56. * @since 2.0.4
  57. */
  58. public static $thumbnailBackgroundColor = 'FFF';
  59. /**
  60. * @var string background alpha (transparency) to use when creating thumbnails in `ImageInterface::THUMBNAIL_INSET`
  61. * mode with both width and height specified. Default is solid.
  62. *
  63. * @since 2.0.4
  64. */
  65. public static $thumbnailBackgroundAlpha = 100;
  66. /**
  67. * Returns the `Imagine` object that supports various image manipulations.
  68. * @return ImagineInterface the `Imagine` object
  69. */
  70. public static function getImagine()
  71. {
  72. if (self::$_imagine === null) {
  73. self::$_imagine = static::createImagine();
  74. }
  75. return self::$_imagine;
  76. }
  77. /**
  78. * @param ImagineInterface $imagine the `Imagine` object.
  79. */
  80. public static function setImagine($imagine)
  81. {
  82. self::$_imagine = $imagine;
  83. }
  84. /**
  85. * Creates an `Imagine` object based on the specified [[driver]].
  86. * @return ImagineInterface the new `Imagine` object
  87. * @throws InvalidConfigException if [[driver]] is unknown or the system doesn't support any [[driver]].
  88. */
  89. protected static function createImagine()
  90. {
  91. foreach ((array) static::$driver as $driver) {
  92. switch ($driver) {
  93. case self::DRIVER_GMAGICK:
  94. if (class_exists('Gmagick', false)) {
  95. return new \Imagine\Gmagick\Imagine();
  96. }
  97. break;
  98. case self::DRIVER_IMAGICK:
  99. if (class_exists('Imagick', false)) {
  100. return new \Imagine\Imagick\Imagine();
  101. }
  102. break;
  103. case self::DRIVER_GD2:
  104. if (function_exists('gd_info')) {
  105. return new \Imagine\Gd\Imagine();
  106. }
  107. break;
  108. default:
  109. throw new InvalidConfigException("Unknown driver: $driver");
  110. }
  111. }
  112. throw new InvalidConfigException('Your system does not support any of these drivers: ' . implode(',', (array) static::$driver));
  113. }
  114. /**
  115. * Takes either file path or ImageInterface. In case of file path, creates an instance of ImageInterface from it.
  116. *
  117. * @param string|resource|ImageInterface $image
  118. * @return ImageInterface
  119. * @throws \yii\base\InvalidParamException
  120. * @since 2.1.0
  121. */
  122. protected static function ensureImageInterfaceInstance($image)
  123. {
  124. if ($image instanceof ImageInterface) {
  125. return $image;
  126. }
  127. if (is_resource($image)) {
  128. return static::getImagine()->read($image);
  129. }
  130. if (is_string($image)) {
  131. return static::getImagine()->open(Yii::getAlias($image));
  132. }
  133. throw new InvalidParamException('File should be either ImageInterface, resource or a string containing file path.');
  134. }
  135. /**
  136. * Crops an image.
  137. *
  138. * For example:
  139. *
  140. * ```php
  141. * $obj->crop('path\to\image.jpg', 200, 200, [5, 5]);
  142. *
  143. * $point = new \Imagine\Image\Point(5, 5);
  144. * $obj->crop('path\to\image.jpg', 200, 200, $point);
  145. * ```
  146. *
  147. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  148. * @param int $width the crop width
  149. * @param int $height the crop height
  150. * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates.
  151. * @return ImageInterface
  152. * @throws InvalidParamException if the `$start` parameter is invalid
  153. */
  154. public static function crop($image, $width, $height, array $start = [0, 0])
  155. {
  156. if (!isset($start[0], $start[1])) {
  157. throw new InvalidParamException('$start must be an array of two elements.');
  158. }
  159. return static::ensureImageInterfaceInstance($image)
  160. ->copy()
  161. ->crop(new Point($start[0], $start[1]), new Box($width, $height));
  162. }
  163. /**
  164. * Rotates an image automatically based on EXIF information.
  165. *
  166. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  167. * @param string $color
  168. * @return \Imagine\Image\ImageInterface
  169. * @since 2.1.0
  170. */
  171. public static function autorotate($image, $color = '000000')
  172. {
  173. return (new Autorotate($color))->apply(static::ensureImageInterfaceInstance($image));
  174. }
  175. /**
  176. * Creates a thumbnail image.
  177. *
  178. * If one of thumbnail dimensions is set to `null`, another one is calculated automatically based on aspect ratio of
  179. * original image. Note that calculated thumbnail dimension may vary depending on the source image in this case.
  180. *
  181. * If both dimensions are specified, resulting thumbnail would be exactly the width and height specified. How it's
  182. * achieved depends on the mode defined via settings parameter.
  183. *
  184. * If `ImageInterface::THUMBNAIL_OUTBOUND` mode is used, which is default, then the thumbnail is scaled so that
  185. * its smallest side equals the length of the corresponding side in the original image. Any excess outside of
  186. * the scaled thumbnail’s area will be cropped, and the returned thumbnail will have the exact width and height
  187. * specified.
  188. *
  189. * If thumbnail mode is `ImageInterface::THUMBNAIL_INSET`, the original image is scaled down so it is fully
  190. * contained within the thumbnail dimensions. The rest is filled with background that could be configured via
  191. * [[Image::$thumbnailBackgroundColor]] and [[Image::$thumbnailBackgroundAlpha]].
  192. *
  193. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  194. * @param int $width the width in pixels to create the thumbnail
  195. * @param int $height the height in pixels to create the thumbnail
  196. * @param int $settings settings for resizing original image, one or more of the ManipulatorInterface::THUMBNAIL_ flags (joined with |)
  197. * @return ImageInterface
  198. */
  199. public static function thumbnail($image, $width, $height, $settings = ManipulatorInterface::THUMBNAIL_OUTBOUND)
  200. {
  201. $img = self::ensureImageInterfaceInstance($image);
  202. /** @var BoxInterface $sourceBox */
  203. $sourceBox = $img->getSize();
  204. $thumbnailBox = static::getThumbnailBox($sourceBox, $width, $height);
  205. $allowUpscale = (bool) ($settings & ImageInterface::THUMBNAIL_FLAG_UPSCALE);
  206. if (self::isUpscaling($sourceBox, $thumbnailBox) && !$allowUpscale) {
  207. return $img->copy();
  208. }
  209. $img = $img->thumbnail($thumbnailBox, $settings);
  210. if ($settings & ImageInterface::THUMBNAIL_OUTBOUND) {
  211. return $img;
  212. }
  213. $size = $img->getSize();
  214. if ($size->getWidth() == $width && $size->getHeight() == $height) {
  215. return $img;
  216. }
  217. $palette = new RGB();
  218. $color = $palette->color(static::$thumbnailBackgroundColor, static::$thumbnailBackgroundAlpha);
  219. // create empty image to preserve aspect ratio of thumbnail
  220. $thumb = static::getImagine()->create($thumbnailBox, $color);
  221. // calculate points
  222. $startX = 0;
  223. $startY = 0;
  224. if ($size->getWidth() < $width) {
  225. $startX = ceil(($width - $size->getWidth()) / 2);
  226. }
  227. if ($size->getHeight() < $height) {
  228. $startY = ceil(($height - $size->getHeight()) / 2);
  229. }
  230. $thumb->paste($img, new Point($startX, $startY));
  231. return $thumb;
  232. }
  233. /**
  234. * Resizes an image.
  235. *
  236. * If one of the dimensions is set to `null`, another one is calculated automatically based on aspect ratio of
  237. * original image.
  238. *
  239. * If both of the dimensions are set then new dimensions are calculated so that image keeps aspect ratio.
  240. *
  241. * You can set $keepAspectRatio to false if you want to force fixed width and height.
  242. *
  243. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  244. * @param int $width the width in pixels
  245. * @param int $height the height in pixels
  246. * @param bool $keepAspectRatio should the image keep aspect ratio
  247. * @param bool $allowUpscaling should the image be upscaled if needed
  248. * @return ImageInterface
  249. *
  250. * @since 2.1.1
  251. */
  252. public static function resize($image, $width, $height, $keepAspectRatio = true, $allowUpscaling = false)
  253. {
  254. $img = self::ensureImageInterfaceInstance($image)->copy();
  255. /** @var BoxInterface $sourceBox */
  256. $sourceBox = $img->getSize();
  257. $destinationBox = static::getBox($sourceBox, $width, $height, $keepAspectRatio);
  258. if ($allowUpscaling === false && self::isUpscaling($sourceBox, $destinationBox)) {
  259. return $img;
  260. }
  261. return $img->resize($destinationBox);
  262. }
  263. /**
  264. * Adds a watermark to an existing image.
  265. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  266. * @param string|resource|ImageInterface $watermarkImage either ImageInterface, resource or a string containing watermark file path
  267. * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates.
  268. * @return ImageInterface
  269. * @throws InvalidParamException if `$start` is invalid
  270. */
  271. public static function watermark($image, $watermarkImage, array $start = [0, 0])
  272. {
  273. if (!isset($start[0], $start[1])) {
  274. throw new InvalidParamException('$start must be an array of two elements.');
  275. }
  276. $img = self::ensureImageInterfaceInstance($image);
  277. $watermark = self::ensureImageInterfaceInstance($watermarkImage);
  278. $img->paste($watermark, new Point($start[0], $start[1]));
  279. return $img;
  280. }
  281. /**
  282. * Draws a text string on an existing image.
  283. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  284. * @param string $text the text to write to the image
  285. * @param string $fontFile the file path or path alias
  286. * @param array $start the starting position of the text. This must be an array with two elements representing `x` and `y` coordinates.
  287. * @param array $fontOptions the font options. The following options may be specified:
  288. *
  289. * - color: The font color. Defaults to "fff".
  290. * - size: The font size. Defaults to 12.
  291. * - angle: The angle to use to write the text. Defaults to 0.
  292. *
  293. * @return ImageInterface
  294. * @throws InvalidParamException if `$fontOptions` is invalid
  295. */
  296. public static function text($image, $text, $fontFile, array $start = [0, 0], array $fontOptions = [])
  297. {
  298. if (!isset($start[0], $start[1])) {
  299. throw new InvalidParamException('$start must be an array of two elements.');
  300. }
  301. $fontSize = ArrayHelper::getValue($fontOptions, 'size', 12);
  302. $fontColor = ArrayHelper::getValue($fontOptions, 'color', 'fff');
  303. $fontAngle = ArrayHelper::getValue($fontOptions, 'angle', 0);
  304. $palette = new RGB();
  305. $color = $palette->color($fontColor);
  306. $img = self::ensureImageInterfaceInstance($image);
  307. $font = static::getImagine()->font(Yii::getAlias($fontFile), $fontSize, $color);
  308. $img->draw()->text($text, $font, new Point($start[0], $start[1]), $fontAngle);
  309. return $img;
  310. }
  311. /**
  312. * Adds a frame around of the image. Please note that the image size will increase by `$margin` x 2.
  313. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  314. * @param int $margin the frame size to add around the image
  315. * @param string $color the frame color
  316. * @param int $alpha the alpha value of the frame.
  317. * @return ImageInterface
  318. */
  319. public static function frame($image, $margin = 20, $color = '666', $alpha = 100)
  320. {
  321. $img = self::ensureImageInterfaceInstance($image);
  322. $size = $img->getSize();
  323. $pasteTo = new Point($margin, $margin);
  324. $palette = new RGB();
  325. $color = $palette->color($color, $alpha);
  326. $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2));
  327. $finalImage = static::getImagine()->create($box, $color);
  328. $finalImage->paste($img, $pasteTo);
  329. return $finalImage;
  330. }
  331. /**
  332. * Returns box for a thumbnail to be created. If one of the dimensions is set to `null`, another one is calculated
  333. * automatically based on width to height ratio of original image box.
  334. *
  335. * @param BoxInterface $sourceBox original image box
  336. * @param int $width thumbnail width
  337. * @param int $height thumbnail height
  338. * @return BoxInterface thumbnail box
  339. *
  340. * @since 2.0.4
  341. */
  342. public static function getThumbnailBox(BoxInterface $sourceBox, $width, $height)
  343. {
  344. if ($width !== null && $height !== null) {
  345. return new Box($width, $height);
  346. }
  347. return self::getBox($sourceBox, $width, $height, false);
  348. }
  349. /**
  350. * Returns box for an image to be created.
  351. *
  352. * If one of the dimensions is set to `null`, another one is calculated automatically based on width to height ratio
  353. * of original image box.
  354. *
  355. * If both of the dimensions are set then new dimensions are calculated so that image keeps aspect ratio.
  356. *
  357. * You can set $keepAspectRatio to false if you want to force fixed width and height.
  358. *
  359. * @param BoxInterface $sourceBox original image box
  360. * @param int $width new image width
  361. * @param int $height new image height
  362. * @param bool $keepAspectRatio should we keep aspect ratio even if both with and height are set
  363. * @return BoxInterface new image box
  364. *
  365. * @since 2.1.1
  366. */
  367. public static function getBox(BoxInterface $sourceBox, $width, $height, $keepAspectRatio = true)
  368. {
  369. if ($width === null && $height === null) {
  370. throw new InvalidParamException('Width and height cannot be null at same time.');
  371. }
  372. $ratio = $sourceBox->getWidth() / $sourceBox->getHeight();
  373. if ($keepAspectRatio === false) {
  374. if ($height === null) {
  375. $height = ceil($width / $ratio);
  376. } elseif ($width === null) {
  377. $width = ceil($height * $ratio);
  378. }
  379. } else {
  380. if ($height === null) {
  381. $height = ceil($width / $ratio);
  382. } elseif ($width === null) {
  383. $width = ceil($height * $ratio);
  384. } elseif ($width / $height > $ratio) {
  385. $width = $height * $ratio;
  386. } else {
  387. $height = $width / $ratio;
  388. }
  389. }
  390. return new Box($width, $height);
  391. }
  392. /**
  393. * Checks if upscaling is going to happen
  394. *
  395. * @param BoxInterface $sourceBox
  396. * @param BoxInterface $destinationBox
  397. * @return bool
  398. */
  399. public static function isUpscaling(BoxInterface $sourceBox, BoxInterface $destinationBox)
  400. {
  401. return ($sourceBox->getWidth() <= $destinationBox->getWidth() && $sourceBox->getHeight() <= $destinationBox->getHeight()) || (!$destinationBox->getWidth() && !$destinationBox->getHeight());
  402. }
  403. }