Image.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: yunwuxin <448901948@qq.com>
  10. // +----------------------------------------------------------------------
  11. namespace think;
  12. use think\image\Exception as ImageException;
  13. use think\image\gif\Gif;
  14. class Image
  15. {
  16. /* 缩略图相关常量定义 */
  17. const THUMB_SCALING = 1; //常量,标识缩略图等比例缩放类型
  18. const THUMB_FILLED = 2; //常量,标识缩略图缩放后填充类型
  19. const THUMB_CENTER = 3; //常量,标识缩略图居中裁剪类型
  20. const THUMB_NORTHWEST = 4; //常量,标识缩略图左上角裁剪类型
  21. const THUMB_SOUTHEAST = 5; //常量,标识缩略图右下角裁剪类型
  22. const THUMB_FIXED = 6; //常量,标识缩略图固定尺寸缩放类型
  23. /* 水印相关常量定义 */
  24. const WATER_NORTHWEST = 1; //常量,标识左上角水印
  25. const WATER_NORTH = 2; //常量,标识上居中水印
  26. const WATER_NORTHEAST = 3; //常量,标识右上角水印
  27. const WATER_WEST = 4; //常量,标识左居中水印
  28. const WATER_CENTER = 5; //常量,标识居中水印
  29. const WATER_EAST = 6; //常量,标识右居中水印
  30. const WATER_SOUTHWEST = 7; //常量,标识左下角水印
  31. const WATER_SOUTH = 8; //常量,标识下居中水印
  32. const WATER_SOUTHEAST = 9; //常量,标识右下角水印
  33. /* 翻转相关常量定义 */
  34. const FLIP_X = 1; //X轴翻转
  35. const FLIP_Y = 2; //Y轴翻转
  36. /**
  37. * 图像资源对象
  38. *
  39. * @var resource
  40. */
  41. protected $im;
  42. /** @var Gif */
  43. protected $gif;
  44. /**
  45. * 图像信息,包括 width, height, type, mime, size
  46. *
  47. * @var array
  48. */
  49. protected $info;
  50. protected function __construct(\SplFileInfo $file)
  51. {
  52. //获取图像信息
  53. $info = @getimagesize($file->getPathname());
  54. //检测图像合法性
  55. if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
  56. throw new ImageException('Illegal image file');
  57. }
  58. //设置图像信息
  59. $this->info = [
  60. 'width' => $info[0],
  61. 'height' => $info[1],
  62. 'type' => image_type_to_extension($info[2], false),
  63. 'mime' => $info['mime'],
  64. ];
  65. //打开图像
  66. if ('gif' == $this->info['type']) {
  67. $this->gif = new Gif($file->getPathname());
  68. $this->im = @imagecreatefromstring($this->gif->image());
  69. } else {
  70. $fun = "imagecreatefrom{$this->info['type']}";
  71. $this->im = @$fun($file->getPathname());
  72. }
  73. if (empty($this->im)) {
  74. throw new ImageException('Failed to create image resources!');
  75. }
  76. }
  77. /**
  78. * 打开一个图片文件
  79. * @param \SplFileInfo|string $file
  80. * @return Image
  81. */
  82. public static function open($file)
  83. {
  84. if (is_string($file)) {
  85. $file = new \SplFileInfo($file);
  86. }
  87. if (!$file->isFile()) {
  88. throw new ImageException('image file not exist');
  89. }
  90. return new self($file);
  91. }
  92. /**
  93. * 保存图像
  94. * @param string $pathname 图像保存路径名称
  95. * @param null|string $type 图像类型
  96. * @param int $quality 图像质量
  97. * @param bool $interlace 是否对JPEG类型图像设置隔行扫描
  98. * @return $this
  99. */
  100. public function save($pathname, $type = null, $quality = 80, $interlace = true)
  101. {
  102. //自动获取图像类型
  103. if (is_null($type)) {
  104. $type = $this->info['type'];
  105. } else {
  106. $type = strtolower($type);
  107. }
  108. //保存图像
  109. if ('jpeg' == $type || 'jpg' == $type) {
  110. //JPEG图像设置隔行扫描
  111. imageinterlace($this->im, $interlace);
  112. imagejpeg($this->im, $pathname, $quality);
  113. } elseif ('gif' == $type && !empty($this->gif)) {
  114. $this->gif->save($pathname);
  115. } elseif ('png' == $type) {
  116. //设定保存完整的 alpha 通道信息
  117. imagesavealpha($this->im, true);
  118. //ImagePNG生成图像的质量范围从0到9的
  119. imagepng($this->im, $pathname, min((int) ($quality / 10), 9));
  120. } else {
  121. $fun = 'image' . $type;
  122. $fun($this->im, $pathname);
  123. }
  124. return $this;
  125. }
  126. /**
  127. * 返回图像宽度
  128. * @return int 图像宽度
  129. */
  130. public function width()
  131. {
  132. return $this->info['width'];
  133. }
  134. /**
  135. * 返回图像高度
  136. * @return int 图像高度
  137. */
  138. public function height()
  139. {
  140. return $this->info['height'];
  141. }
  142. /**
  143. * 返回图像类型
  144. * @return string 图像类型
  145. */
  146. public function type()
  147. {
  148. return $this->info['type'];
  149. }
  150. /**
  151. * 返回图像MIME类型
  152. * @return string 图像MIME类型
  153. */
  154. public function mime()
  155. {
  156. return $this->info['mime'];
  157. }
  158. /**
  159. * 返回图像尺寸数组 0 - 图像宽度,1 - 图像高度
  160. * @return array 图像尺寸
  161. */
  162. public function size()
  163. {
  164. return [$this->info['width'], $this->info['height']];
  165. }
  166. /**
  167. * 旋转图像
  168. * @param int $degrees 顺时针旋转的度数
  169. * @return $this
  170. */
  171. public function rotate($degrees = 90)
  172. {
  173. do {
  174. $img = imagerotate($this->im, -$degrees, imagecolorallocatealpha($this->im, 0, 0, 0, 127));
  175. imagedestroy($this->im);
  176. $this->im = $img;
  177. } while (!empty($this->gif) && $this->gifNext());
  178. $this->info['width'] = imagesx($this->im);
  179. $this->info['height'] = imagesy($this->im);
  180. return $this;
  181. }
  182. /**
  183. * 翻转图像
  184. * @param integer $direction 翻转轴,X或者Y
  185. * @return $this
  186. */
  187. public function flip($direction = self::FLIP_X)
  188. {
  189. //原图宽度和高度
  190. $w = $this->info['width'];
  191. $h = $this->info['height'];
  192. do {
  193. $img = imagecreatetruecolor($w, $h);
  194. switch ($direction) {
  195. case self::FLIP_X:
  196. for ($y = 0; $y < $h; $y++) {
  197. imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1);
  198. }
  199. break;
  200. case self::FLIP_Y:
  201. for ($x = 0; $x < $w; $x++) {
  202. imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h);
  203. }
  204. break;
  205. default:
  206. throw new ImageException('不支持的翻转类型');
  207. }
  208. imagedestroy($this->im);
  209. $this->im = $img;
  210. } while (!empty($this->gif) && $this->gifNext());
  211. return $this;
  212. }
  213. /**
  214. * 裁剪图像
  215. *
  216. * @param integer $w 裁剪区域宽度
  217. * @param integer $h 裁剪区域高度
  218. * @param integer $x 裁剪区域x坐标
  219. * @param integer $y 裁剪区域y坐标
  220. * @param integer $width 图像保存宽度
  221. * @param integer $height 图像保存高度
  222. *
  223. * @return $this
  224. */
  225. public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null)
  226. {
  227. //设置保存尺寸
  228. empty($width) && $width = $w;
  229. empty($height) && $height = $h;
  230. do {
  231. //创建新图像
  232. $img = imagecreatetruecolor($width, $height);
  233. // 调整默认颜色
  234. $color = imagecolorallocate($img, 255, 255, 255);
  235. imagefill($img, 0, 0, $color);
  236. //裁剪
  237. imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h);
  238. imagedestroy($this->im); //销毁原图
  239. //设置新图像
  240. $this->im = $img;
  241. } while (!empty($this->gif) && $this->gifNext());
  242. $this->info['width'] = (int) $width;
  243. $this->info['height'] = (int) $height;
  244. return $this;
  245. }
  246. /**
  247. * 生成缩略图
  248. *
  249. * @param integer $width 缩略图最大宽度
  250. * @param integer $height 缩略图最大高度
  251. * @param int $type 缩略图裁剪类型
  252. *
  253. * @return $this
  254. */
  255. public function thumb($width, $height, $type = self::THUMB_SCALING)
  256. {
  257. //原图宽度和高度
  258. $w = $this->info['width'];
  259. $h = $this->info['height'];
  260. /* 计算缩略图生成的必要参数 */
  261. switch ($type) {
  262. /* 等比例缩放 */
  263. case self::THUMB_SCALING:
  264. //原图尺寸小于缩略图尺寸则不进行缩略
  265. if ($w < $width && $h < $height) {
  266. return $this;
  267. }
  268. //计算缩放比例
  269. $scale = min($width / $w, $height / $h);
  270. //设置缩略图的坐标及宽度和高度
  271. $x = $y = 0;
  272. $width = $w * $scale;
  273. $height = $h * $scale;
  274. break;
  275. /* 居中裁剪 */
  276. case self::THUMB_CENTER:
  277. //计算缩放比例
  278. $scale = max($width / $w, $height / $h);
  279. //设置缩略图的坐标及宽度和高度
  280. $w = $width / $scale;
  281. $h = $height / $scale;
  282. $x = ($this->info['width'] - $w) / 2;
  283. $y = ($this->info['height'] - $h) / 2;
  284. break;
  285. /* 左上角裁剪 */
  286. case self::THUMB_NORTHWEST:
  287. //计算缩放比例
  288. $scale = max($width / $w, $height / $h);
  289. //设置缩略图的坐标及宽度和高度
  290. $x = $y = 0;
  291. $w = $width / $scale;
  292. $h = $height / $scale;
  293. break;
  294. /* 右下角裁剪 */
  295. case self::THUMB_SOUTHEAST:
  296. //计算缩放比例
  297. $scale = max($width / $w, $height / $h);
  298. //设置缩略图的坐标及宽度和高度
  299. $w = $width / $scale;
  300. $h = $height / $scale;
  301. $x = $this->info['width'] - $w;
  302. $y = $this->info['height'] - $h;
  303. break;
  304. /* 填充 */
  305. case self::THUMB_FILLED:
  306. //计算缩放比例
  307. if ($w < $width && $h < $height) {
  308. $scale = 1;
  309. } else {
  310. $scale = min($width / $w, $height / $h);
  311. }
  312. //设置缩略图的坐标及宽度和高度
  313. $neww = $w * $scale;
  314. $newh = $h * $scale;
  315. $x = $this->info['width'] - $w;
  316. $y = $this->info['height'] - $h;
  317. $posx = ($width - $w * $scale) / 2;
  318. $posy = ($height - $h * $scale) / 2;
  319. do {
  320. //创建新图像
  321. $img = imagecreatetruecolor($width, $height);
  322. // 调整默认颜色
  323. $color = imagecolorallocate($img, 255, 255, 255);
  324. imagefill($img, 0, 0, $color);
  325. //裁剪
  326. imagecopyresampled($img, $this->im, $posx, $posy, $x, $y, $neww, $newh, $w, $h);
  327. imagedestroy($this->im); //销毁原图
  328. $this->im = $img;
  329. } while (!empty($this->gif) && $this->gifNext());
  330. $this->info['width'] = (int) $width;
  331. $this->info['height'] = (int) $height;
  332. return $this;
  333. /* 固定 */
  334. case self::THUMB_FIXED:
  335. $x = $y = 0;
  336. break;
  337. default:
  338. throw new ImageException('不支持的缩略图裁剪类型');
  339. }
  340. /* 裁剪图像 */
  341. return $this->crop($w, $h, $x, $y, $width, $height);
  342. }
  343. /**
  344. * 添加水印
  345. *
  346. * @param string $source 水印图片路径
  347. * @param int $locate 水印位置
  348. * @param int $alpha 透明度
  349. * @return $this
  350. */
  351. public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100)
  352. {
  353. if (!is_file($source)) {
  354. throw new ImageException('水印图像不存在');
  355. }
  356. //获取水印图像信息
  357. $info = getimagesize($source);
  358. if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
  359. throw new ImageException('非法水印文件');
  360. }
  361. //创建水印图像资源
  362. $fun = 'imagecreatefrom' . image_type_to_extension($info[2], false);
  363. $water = $fun($source);
  364. //设定水印图像的混色模式
  365. imagealphablending($water, true);
  366. /* 设定水印位置 */
  367. switch ($locate) {
  368. /* 右下角水印 */
  369. case self::WATER_SOUTHEAST:
  370. $x = $this->info['width'] - $info[0];
  371. $y = $this->info['height'] - $info[1];
  372. break;
  373. /* 左下角水印 */
  374. case self::WATER_SOUTHWEST:
  375. $x = 0;
  376. $y = $this->info['height'] - $info[1];
  377. break;
  378. /* 左上角水印 */
  379. case self::WATER_NORTHWEST:
  380. $x = $y = 0;
  381. break;
  382. /* 右上角水印 */
  383. case self::WATER_NORTHEAST:
  384. $x = $this->info['width'] - $info[0];
  385. $y = 0;
  386. break;
  387. /* 居中水印 */
  388. case self::WATER_CENTER:
  389. $x = ($this->info['width'] - $info[0]) / 2;
  390. $y = ($this->info['height'] - $info[1]) / 2;
  391. break;
  392. /* 下居中水印 */
  393. case self::WATER_SOUTH:
  394. $x = ($this->info['width'] - $info[0]) / 2;
  395. $y = $this->info['height'] - $info[1];
  396. break;
  397. /* 右居中水印 */
  398. case self::WATER_EAST:
  399. $x = $this->info['width'] - $info[0];
  400. $y = ($this->info['height'] - $info[1]) / 2;
  401. break;
  402. /* 上居中水印 */
  403. case self::WATER_NORTH:
  404. $x = ($this->info['width'] - $info[0]) / 2;
  405. $y = 0;
  406. break;
  407. /* 左居中水印 */
  408. case self::WATER_WEST:
  409. $x = 0;
  410. $y = ($this->info['height'] - $info[1]) / 2;
  411. break;
  412. default:
  413. /* 自定义水印坐标 */
  414. if (is_array($locate)) {
  415. list($x, $y) = $locate;
  416. } else {
  417. throw new ImageException('不支持的水印位置类型');
  418. }
  419. }
  420. do {
  421. //添加水印
  422. $src = imagecreatetruecolor($info[0], $info[1]);
  423. // 调整默认颜色
  424. $color = imagecolorallocate($src, 255, 255, 255);
  425. imagefill($src, 0, 0, $color);
  426. imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]);
  427. imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]);
  428. imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha);
  429. //销毁零时图片资源
  430. imagedestroy($src);
  431. } while (!empty($this->gif) && $this->gifNext());
  432. //销毁水印资源
  433. imagedestroy($water);
  434. return $this;
  435. }
  436. /**
  437. * 图像添加文字
  438. *
  439. * @param string $text 添加的文字
  440. * @param string $font 字体路径
  441. * @param integer $size 字号
  442. * @param string $color 文字颜色
  443. * @param int $locate 文字写入位置
  444. * @param integer $offset 文字相对当前位置的偏移量
  445. * @param integer $angle 文字倾斜角度
  446. *
  447. * @return $this
  448. * @throws ImageException
  449. */
  450. public function text($text, $font, $size, $color = '#00000000',
  451. $locate = self::WATER_SOUTHEAST, $offset = 0, $angle = 0) {
  452. if (!is_file($font)) {
  453. throw new ImageException("不存在的字体文件:{$font}");
  454. }
  455. //获取文字信息
  456. $info = imagettfbbox($size, $angle, $font, $text);
  457. $minx = min($info[0], $info[2], $info[4], $info[6]);
  458. $maxx = max($info[0], $info[2], $info[4], $info[6]);
  459. $miny = min($info[1], $info[3], $info[5], $info[7]);
  460. $maxy = max($info[1], $info[3], $info[5], $info[7]);
  461. /* 计算文字初始坐标和尺寸 */
  462. $x = $minx;
  463. $y = abs($miny);
  464. $w = $maxx - $minx;
  465. $h = $maxy - $miny;
  466. /* 设定文字位置 */
  467. switch ($locate) {
  468. /* 右下角文字 */
  469. case self::WATER_SOUTHEAST:
  470. $x += $this->info['width'] - $w;
  471. $y += $this->info['height'] - $h;
  472. break;
  473. /* 左下角文字 */
  474. case self::WATER_SOUTHWEST:
  475. $y += $this->info['height'] - $h;
  476. break;
  477. /* 左上角文字 */
  478. case self::WATER_NORTHWEST:
  479. // 起始坐标即为左上角坐标,无需调整
  480. break;
  481. /* 右上角文字 */
  482. case self::WATER_NORTHEAST:
  483. $x += $this->info['width'] - $w;
  484. break;
  485. /* 居中文字 */
  486. case self::WATER_CENTER:
  487. $x += ($this->info['width'] - $w) / 2;
  488. $y += ($this->info['height'] - $h) / 2;
  489. break;
  490. /* 下居中文字 */
  491. case self::WATER_SOUTH:
  492. $x += ($this->info['width'] - $w) / 2;
  493. $y += $this->info['height'] - $h;
  494. break;
  495. /* 右居中文字 */
  496. case self::WATER_EAST:
  497. $x += $this->info['width'] - $w;
  498. $y += ($this->info['height'] - $h) / 2;
  499. break;
  500. /* 上居中文字 */
  501. case self::WATER_NORTH:
  502. $x += ($this->info['width'] - $w) / 2;
  503. break;
  504. /* 左居中文字 */
  505. case self::WATER_WEST:
  506. $y += ($this->info['height'] - $h) / 2;
  507. break;
  508. default:
  509. /* 自定义文字坐标 */
  510. if (is_array($locate)) {
  511. list($posx, $posy) = $locate;
  512. $x += $posx;
  513. $y += $posy;
  514. } else {
  515. throw new ImageException('不支持的文字位置类型');
  516. }
  517. }
  518. /* 设置偏移量 */
  519. if (is_array($offset)) {
  520. $offset = array_map('intval', $offset);
  521. list($ox, $oy) = $offset;
  522. } else {
  523. $offset = intval($offset);
  524. $ox = $oy = $offset;
  525. }
  526. /* 设置颜色 */
  527. if (is_string($color) && 0 === strpos($color, '#')) {
  528. $color = str_split(substr($color, 1), 2);
  529. $color = array_map('hexdec', $color);
  530. if (empty($color[3]) || $color[3] > 127) {
  531. $color[3] = 0;
  532. }
  533. } elseif (!is_array($color)) {
  534. throw new ImageException('错误的颜色值');
  535. }
  536. do {
  537. /* 写入文字 */
  538. $col = imagecolorallocatealpha($this->im, $color[0], $color[1], $color[2], $color[3]);
  539. imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text);
  540. } while (!empty($this->gif) && $this->gifNext());
  541. return $this;
  542. }
  543. /**
  544. * 切换到GIF的下一帧并保存当前帧
  545. */
  546. protected function gifNext()
  547. {
  548. ob_start();
  549. ob_implicit_flush(0);
  550. imagegif($this->im);
  551. $img = ob_get_clean();
  552. $this->gif->image($img);
  553. $next = $this->gif->nextImage();
  554. if ($next) {
  555. imagedestroy($this->im);
  556. $this->im = imagecreatefromstring($next);
  557. return $next;
  558. } else {
  559. imagedestroy($this->im);
  560. $this->im = imagecreatefromstring($this->gif->image());
  561. return false;
  562. }
  563. }
  564. /**
  565. * 析构方法,用于销毁图像资源
  566. */
  567. public function __destruct()
  568. {
  569. empty($this->im) || imagedestroy($this->im);
  570. }
  571. }