BaseImage.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://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.
  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 string $mode mode of resizing original image to use in case both width and height specified
  197. * @return ImageInterface
  198. */
  199. public static function thumbnail($image, $width, $height, $mode = 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. if (self::isUpscaling($sourceBox, $thumbnailBox)) {
  206. return $img->copy();
  207. }
  208. $img = $img->thumbnail($thumbnailBox, $mode);
  209. if ($mode == ManipulatorInterface::THUMBNAIL_OUTBOUND) {
  210. return $img;
  211. }
  212. $size = $img->getSize();
  213. if ($size->getWidth() == $width && $size->getHeight() == $height) {
  214. return $img;
  215. }
  216. $palette = new RGB();
  217. $color = $palette->color(static::$thumbnailBackgroundColor, static::$thumbnailBackgroundAlpha);
  218. // create empty image to preserve aspect ratio of thumbnail
  219. $thumb = static::getImagine()->create($thumbnailBox, $color);
  220. // calculate points
  221. $startX = 0;
  222. $startY = 0;
  223. if ($size->getWidth() < $width) {
  224. $startX = ceil(($width - $size->getWidth()) / 2);
  225. }
  226. if ($size->getHeight() < $height) {
  227. $startY = ceil(($height - $size->getHeight()) / 2);
  228. }
  229. $thumb->paste($img, new Point($startX, $startY));
  230. return $thumb;
  231. }
  232. /**
  233. * Resizes an image.
  234. *
  235. * If one of the dimensions is set to `null`, another one is calculated automatically based on aspect ratio of
  236. * original image.
  237. *
  238. * If both of the dimensions are set then new dimensions are calculated so that image keeps aspect ratio.
  239. *
  240. * You can set $keepAspectRatio to false if you want to force fixed width and height.
  241. *
  242. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  243. * @param int $width the width in pixels
  244. * @param int $height the height in pixels
  245. * @param bool $keepAspectRatio should the image keep aspect ratio
  246. * @param bool $allowUpscaling should the image be upscaled if needed
  247. * @return ImageInterface
  248. *
  249. * @since 2.1.1
  250. */
  251. public static function resize($image, $width, $height, $keepAspectRatio = true, $allowUpscaling = false)
  252. {
  253. $img = self::ensureImageInterfaceInstance($image)->copy();
  254. /** @var BoxInterface $sourceBox */
  255. $sourceBox = $img->getSize();
  256. $destinationBox = static::getBox($sourceBox, $width, $height, $keepAspectRatio);
  257. if ($allowUpscaling === false && self::isUpscaling($sourceBox, $destinationBox)) {
  258. return $img;
  259. }
  260. return $img->resize($destinationBox);
  261. }
  262. /**
  263. * Adds a watermark to an existing image.
  264. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  265. * @param string|resource|ImageInterface $watermarkImage either ImageInterface, resource or a string containing watermark file path
  266. * @param array $start the starting point. This must be an array with two elements representing `x` and `y` coordinates.
  267. * @return ImageInterface
  268. * @throws InvalidParamException if `$start` is invalid
  269. */
  270. public static function watermark($image, $watermarkImage, array $start = [0, 0])
  271. {
  272. if (!isset($start[0], $start[1])) {
  273. throw new InvalidParamException('$start must be an array of two elements.');
  274. }
  275. $img = self::ensureImageInterfaceInstance($image);
  276. $watermark = self::ensureImageInterfaceInstance($watermarkImage);
  277. $img->paste($watermark, new Point($start[0], $start[1]));
  278. return $img;
  279. }
  280. /**
  281. * Draws a text string on an existing image.
  282. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  283. * @param string $text the text to write to the image
  284. * @param string $fontFile the file path or path alias
  285. * @param array $start the starting position of the text. This must be an array with two elements representing `x` and `y` coordinates.
  286. * @param array $fontOptions the font options. The following options may be specified:
  287. *
  288. * - color: The font color. Defaults to "fff".
  289. * - size: The font size. Defaults to 12.
  290. * - angle: The angle to use to write the text. Defaults to 0.
  291. *
  292. * @return ImageInterface
  293. * @throws InvalidParamException if `$fontOptions` is invalid
  294. */
  295. public static function text($image, $text, $fontFile, array $start = [0, 0], array $fontOptions = [])
  296. {
  297. if (!isset($start[0], $start[1])) {
  298. throw new InvalidParamException('$start must be an array of two elements.');
  299. }
  300. $fontSize = ArrayHelper::getValue($fontOptions, 'size', 12);
  301. $fontColor = ArrayHelper::getValue($fontOptions, 'color', 'fff');
  302. $fontAngle = ArrayHelper::getValue($fontOptions, 'angle', 0);
  303. $palette = new RGB();
  304. $color = $palette->color($fontColor);
  305. $img = self::ensureImageInterfaceInstance($image);
  306. $font = static::getImagine()->font(Yii::getAlias($fontFile), $fontSize, $color);
  307. $img->draw()->text($text, $font, new Point($start[0], $start[1]), $fontAngle);
  308. return $img;
  309. }
  310. /**
  311. * Adds a frame around of the image. Please note that the image size will increase by `$margin` x 2.
  312. * @param string|resource|ImageInterface $image either ImageInterface, resource or a string containing file path
  313. * @param int $margin the frame size to add around the image
  314. * @param string $color the frame color
  315. * @param int $alpha the alpha value of the frame.
  316. * @return ImageInterface
  317. */
  318. public static function frame($image, $margin = 20, $color = '666', $alpha = 100)
  319. {
  320. $img = self::ensureImageInterfaceInstance($image);
  321. $size = $img->getSize();
  322. $pasteTo = new Point($margin, $margin);
  323. $palette = new RGB();
  324. $color = $palette->color($color, $alpha);
  325. $box = new Box($size->getWidth() + ceil($margin * 2), $size->getHeight() + ceil($margin * 2));
  326. $finalImage = static::getImagine()->create($box, $color);
  327. $finalImage->paste($img, $pasteTo);
  328. return $finalImage;
  329. }
  330. /**
  331. * Returns box for a thumbnail to be created. If one of the dimensions is set to `null`, another one is calculated
  332. * automatically based on width to height ratio of original image box.
  333. *
  334. * @param BoxInterface $sourceBox original image box
  335. * @param int $width thumbnail width
  336. * @param int $height thumbnail height
  337. * @return BoxInterface thumbnail box
  338. *
  339. * @since 2.0.4
  340. */
  341. protected static function getThumbnailBox(BoxInterface $sourceBox, $width, $height)
  342. {
  343. if ($width !== null && $height !== null) {
  344. return new Box($width, $height);
  345. }
  346. return self::getBox($sourceBox, $width, $height, false);
  347. }
  348. /**
  349. * Returns box for an image to be created.
  350. *
  351. * If one of the dimensions is set to `null`, another one is calculated automatically based on width to height ratio
  352. * of original image box.
  353. *
  354. * If both of the dimensions are set then new dimensions are calculated so that image keeps aspect ratio.
  355. *
  356. * You can set $keepAspectRatio to false if you want to force fixed width and height.
  357. *
  358. * @param BoxInterface $sourceBox original image box
  359. * @param int $width new image width
  360. * @param int $height new image height
  361. * @param bool $keepAspectRatio should we keep aspect ratio even if both with and height are set
  362. * @return BoxInterface new image box
  363. *
  364. * @since 2.1.1
  365. */
  366. protected static function getBox(BoxInterface $sourceBox, $width, $height, $keepAspectRatio = true)
  367. {
  368. if ($width === null && $height === null) {
  369. throw new InvalidParamException('Width and height cannot be null at same time.');
  370. }
  371. $ratio = $sourceBox->getWidth() / $sourceBox->getHeight();
  372. if ($keepAspectRatio === false) {
  373. if ($height === null) {
  374. $height = ceil($width / $ratio);
  375. } elseif ($width === null) {
  376. $width = ceil($height * $ratio);
  377. }
  378. } else {
  379. if ($height === null) {
  380. $height = ceil($width / $ratio);
  381. } elseif ($width === null) {
  382. $width = ceil($height * $ratio);
  383. } elseif ($width / $height > $ratio) {
  384. $width = $height * $ratio;
  385. } else {
  386. $height = $width / $ratio;
  387. }
  388. }
  389. return new Box($width, $height);
  390. }
  391. /**
  392. * Checks if upscaling is going to happen
  393. *
  394. * @param BoxInterface $sourceBox
  395. * @param BoxInterface $destinationBox
  396. * @return bool
  397. */
  398. protected static function isUpscaling(BoxInterface $sourceBox, BoxInterface $destinationBox)
  399. {
  400. return ($sourceBox->getWidth() <= $destinationBox->getWidth() && $sourceBox->getHeight() <= $destinationBox->getHeight()) || (!$destinationBox->getWidth() && !$destinationBox->getHeight());
  401. }
  402. }