国内流行的内容管理系统(CMS)多端全媒体解决方案 https://www.dedebiz.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

653 lines
23KB

  1. <?php
  2. if (!defined('DEDEINC')) exit ('dedebiz');
  3. /**
  4. * 图片缩放尺寸
  5. *
  6. * @version $id:imageresize.class.php 2022年1月16日 tianya $
  7. * @package DedeBIZ.Libraries
  8. * @copyright Copyright (c) 2022 DedeBIZ.COM
  9. * @license https://www.dedebiz.com/license
  10. * @link https://www.dedebiz.com
  11. * maintained by Gumlet.com
  12. */
  13. class ImageResizeException extends \Exception
  14. {
  15. }
  16. /**
  17. * PHP class to resize and scale images
  18. */
  19. class ImageResize
  20. {
  21. const CROPTOP = 1;
  22. const CROPCENTRE = 2;
  23. const CROPCENTER = 2;
  24. const CROPBOTTOM = 3;
  25. const CROPLEFT = 4;
  26. const CROPRIGHT = 5;
  27. const CROPTOPCENTER = 6;
  28. const IMG_FLIP_HORIZONTAL = 0;
  29. const IMG_FLIP_VERTICAL = 1;
  30. const IMG_FLIP_BOTH = 2;
  31. public $quality_jpg = 85;
  32. public $quality_webp = 85;
  33. public $quality_png = 6;
  34. public $quality_truecolor = true;
  35. public $gamma_correct = false;
  36. public $interlace = 1;
  37. public $source_type;
  38. protected $source_image;
  39. protected $original_w;
  40. protected $original_h;
  41. protected $dest_x = 0;
  42. protected $dest_y = 0;
  43. protected $source_x;
  44. protected $source_y;
  45. protected $dest_w;
  46. protected $dest_h;
  47. protected $source_w;
  48. protected $source_h;
  49. protected $source_info;
  50. protected $filters = [];
  51. /**
  52. * Create instance from a strng
  53. *
  54. * @param string $image_data
  55. * @return ImageResize
  56. * @throws ImageResizeException
  57. */
  58. public static function createFromString($image_data)
  59. {
  60. if (empty($image_data) || $image_data === null) {
  61. throw new ImageResizeException('image_data must not be empty');
  62. }
  63. $resize = new self('data://application/octet-stream;base64,'.base64_encode($image_data));
  64. return $resize;
  65. }
  66. /**
  67. * Add filter function for use right before save image to file.
  68. *
  69. * @param callable $filter
  70. * @return $this
  71. */
  72. public function addFilter(callable $filter)
  73. {
  74. $this->filters[] = $filter;
  75. return $this;
  76. }
  77. /**
  78. * Apply filters.
  79. *
  80. * @param $image resource an image resource identifier
  81. * @param $filterType filter type and default value is IMG_FILTER_NEGATE
  82. */
  83. protected function applyFilter($image, $filterType = IMG_FILTER_NEGATE)
  84. {
  85. foreach ($this->filters as $function) {
  86. $function($image, $filterType);
  87. }
  88. }
  89. /**
  90. * Loads image source and its properties to the instanciated object
  91. *
  92. * @param string $filename
  93. * @return ImageResize
  94. * @throws ImageResizeException
  95. */
  96. public function __construct($filename)
  97. {
  98. if (!defined('IMAGETYPE_WEBP')) {
  99. define('IMAGETYPE_WEBP', 18);
  100. }
  101. if (!defined('IMAGETYPE_BMP')) {
  102. define('IMAGETYPE_BMP', 6);
  103. }
  104. if ($filename === null || empty($filename) || (substr($filename, 0, 5) !== 'data:' && !is_file($filename))) {
  105. throw new ImageResizeException('File does not exist');
  106. }
  107. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  108. $checkWebp = false;
  109. if (strstr(finfo_file($finfo, $filename), 'image') === false) {
  110. if (version_compare(PHP_VERSION, '7.0.0', '<=') && strstr(file_get_contents($filename), 'WEBPVP8') !== false) {
  111. $checkWebp = true;
  112. $this->source_type = IMAGETYPE_WEBP;
  113. } else {
  114. throw new ImageResizeException('Unsupported file type');
  115. }
  116. } elseif(strstr(finfo_file($finfo, $filename), 'image/webp') !== false) {
  117. $checkWebp = true;
  118. $this->source_type = IMAGETYPE_WEBP;
  119. }
  120. if (!$image_info = getimagesize($filename, $this->source_info)) {
  121. $image_info = getimagesize($filename);
  122. }
  123. if (!$checkWebp) {
  124. if (!$image_info) {
  125. if (strstr(finfo_file($finfo, $filename), 'image') !== false) {
  126. throw new ImageResizeException('Unsupported image type');
  127. }
  128. throw new ImageResizeException('Could not read file');
  129. }
  130. $this->original_w = $image_info[0];
  131. $this->original_h = $image_info[1];
  132. $this->source_type = $image_info[2];
  133. }
  134. switch ($this->source_type) {
  135. case IMAGETYPE_GIF:
  136. $this->source_image = imagecreatefromgif($filename);
  137. break;
  138. case IMAGETYPE_JPEG:
  139. $this->source_image = $this->imageCreateJpegfromExif($filename);
  140. // set new width and height for image, maybe it has changed
  141. $this->original_w = imagesx($this->source_image);
  142. $this->original_h = imagesy($this->source_image);
  143. break;
  144. case IMAGETYPE_PNG:
  145. $this->source_image = imagecreatefrompng($filename);
  146. break;
  147. case IMAGETYPE_WEBP:
  148. $this->source_image = imagecreatefromwebp($filename);
  149. $this->original_w = imagesx($this->source_image);
  150. $this->original_h = imagesy($this->source_image);
  151. break;
  152. case IMAGETYPE_BMP:
  153. if (version_compare(PHP_VERSION, '7.2.0', '<')) {
  154. throw new ImageResizeException('For bmp support PHP >= 7.2.0 is required');
  155. }
  156. $this->source_image = imagecreatefrombmp($filename);
  157. break;
  158. default:
  159. throw new ImageResizeException('Unsupported image type');
  160. }
  161. if (!$this->source_image) {
  162. throw new ImageResizeException('Could not load image');
  163. }
  164. finfo_close($finfo);
  165. return $this->resize($this->getSourceWidth(), $this->getSourceHeight());
  166. }
  167. //http://stackoverflow.com/a/28819866
  168. public function imageCreateJpegfromExif($filename)
  169. {
  170. $img = imagecreatefromjpeg($filename);
  171. if (!function_exists('exif_read_data') || !isset($this->source_info['APP1']) || strpos($this->source_info['APP1'], 'Exif') !== 0) {
  172. return $img;
  173. } try {
  174. $exif = @exif_read_data($filename);
  175. } catch (Exception $e) {
  176. $exif = null;
  177. }
  178. if (!$exif || !isset($exif['Orientation'])) {
  179. return $img;
  180. }
  181. $orientation = $exif['Orientation'];
  182. if ($orientation === 6 || $orientation === 5) {
  183. $img = imagerotate($img, 270, 0);
  184. } elseif ($orientation === 3 || $orientation === 4) {
  185. $img = imagerotate($img, 180, 0);
  186. } elseif ($orientation === 8 || $orientation === 7) {
  187. $img = imagerotate($img, 90, 0);
  188. }
  189. if ($orientation === 5 || $orientation === 4 || $orientation === 7) {
  190. imageflip($img, IMG_FLIP_HORIZONTAL);
  191. }
  192. return $img;
  193. }
  194. /**
  195. * Saves new image
  196. *
  197. * @param string $filename
  198. * @param integer $image_type
  199. * @param integer $quality
  200. * @param integer $permissions
  201. * @param boolean $exact_size
  202. * @return static
  203. */
  204. public function save($filename, $image_type = null, $quality = null, $permissions = null, $exact_size = false)
  205. {
  206. $image_type = $image_type ?: $this->source_type;
  207. $quality = is_numeric($quality) ? (int) abs($quality) : null;
  208. switch ($image_type) {
  209. case IMAGETYPE_GIF:
  210. if (!empty($exact_size) && is_array($exact_size)) {
  211. $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
  212. } else {
  213. $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
  214. }
  215. $background = imagecolorallocatealpha($dest_image, 255, 255, 255, 1);
  216. imagecolortransparent($dest_image, $background);
  217. imagefill($dest_image, 0, 0, $background);
  218. imagesavealpha($dest_image, true);
  219. break;
  220. case IMAGETYPE_JPEG:
  221. if (!empty($exact_size) && is_array($exact_size)) {
  222. $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
  223. $background = imagecolorallocate($dest_image, 255, 255, 255);
  224. imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
  225. } else {
  226. $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
  227. $background = imagecolorallocate($dest_image, 255, 255, 255);
  228. imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
  229. }
  230. break;
  231. case IMAGETYPE_WEBP:
  232. if (version_compare(PHP_VERSION, '5.5.0', '<')) {
  233. throw new ImageResizeException('For WebP support PHP >= 5.5.0 is required');
  234. }
  235. if (!empty($exact_size) && is_array($exact_size)) {
  236. $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
  237. $background = imagecolorallocate($dest_image, 255, 255, 255);
  238. imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
  239. } else {
  240. $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
  241. $background = imagecolorallocate($dest_image, 255, 255, 255);
  242. imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
  243. }
  244. imagealphablending($dest_image, false);
  245. imagesavealpha($dest_image, true);
  246. break;
  247. case IMAGETYPE_PNG:
  248. if (!$this->quality_truecolor || !imageistruecolor($this->source_image)) {
  249. if (!empty($exact_size) && is_array($exact_size)) {
  250. $dest_image = imagecreate($exact_size[0], $exact_size[1]);
  251. } else {
  252. $dest_image = imagecreate($this->getDestWidth(), $this->getDestHeight());
  253. }
  254. } else {
  255. if(!empty($exact_size) && is_array($exact_size)) {
  256. $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
  257. } else {
  258. $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
  259. }
  260. }
  261. imagealphablending($dest_image, false);
  262. imagesavealpha($dest_image, true);
  263. $background = imagecolorallocatealpha($dest_image, 255, 255, 255, 127);
  264. imagecolortransparent($dest_image, $background);
  265. imagefill($dest_image, 0, 0, $background);
  266. break;
  267. case IMAGETYPE_BMP:
  268. if (version_compare(PHP_VERSION, '7.2.0', '<')) {
  269. throw new ImageResizeException('For WebP support PHP >= 7.2.0 is required');
  270. }
  271. if (!empty($exact_size) && is_array($exact_size)) {
  272. $dest_image = imagecreatetruecolor($exact_size[0], $exact_size[1]);
  273. $background = imagecolorallocate($dest_image, 255, 255, 255);
  274. imagefilledrectangle($dest_image, 0, 0, $exact_size[0], $exact_size[1], $background);
  275. } else {
  276. $dest_image = imagecreatetruecolor($this->getDestWidth(), $this->getDestHeight());
  277. $background = imagecolorallocate($dest_image, 255, 255, 255);
  278. imagefilledrectangle($dest_image, 0, 0, $this->getDestWidth(), $this->getDestHeight(), $background);
  279. }
  280. break;
  281. }
  282. imageinterlace($dest_image, $this->interlace);
  283. if ($this->gamma_correct) {
  284. imagegammacorrect($this->source_image, 2.2, 1.0);
  285. }
  286. if (!empty($exact_size) && is_array($exact_size)) {
  287. if ($this->getSourceHeight() < $this->getSourceWidth()) {
  288. $this->dest_x = 0;
  289. $this->dest_y = ($exact_size[1] - $this->getDestHeight()) / 2;
  290. }
  291. if ($this->getSourceHeight() > $this->getSourceWidth()) {
  292. $this->dest_x = ($exact_size[0] - $this->getDestWidth()) / 2;
  293. $this->dest_y = 0;
  294. }
  295. }
  296. imagecopyresampled(
  297. $dest_image,
  298. $this->source_image,
  299. $this->dest_x,
  300. $this->dest_y,
  301. $this->source_x,
  302. $this->source_y,
  303. $this->getDestWidth(),
  304. $this->getDestHeight(),
  305. $this->source_w,
  306. $this->source_h
  307. );
  308. if ($this->gamma_correct) {
  309. imagegammacorrect($dest_image, 1.0, 2.2);
  310. }
  311. $this->applyFilter($dest_image);
  312. switch ($image_type) {
  313. case IMAGETYPE_GIF:
  314. imagegif($dest_image, $filename);
  315. break;
  316. case IMAGETYPE_JPEG:
  317. if ($quality === null || $quality > 100) {
  318. $quality = $this->quality_jpg;
  319. }
  320. imagejpeg($dest_image, $filename, $quality);
  321. break;
  322. case IMAGETYPE_WEBP:
  323. if (version_compare(PHP_VERSION, '5.5.0', '<')) {
  324. throw new ImageResizeException('For WebP support PHP >= 5.5.0 is required');
  325. }
  326. if ($quality === null) {
  327. $quality = $this->quality_webp;
  328. }
  329. imagewebp($dest_image, $filename, $quality);
  330. break;
  331. case IMAGETYPE_PNG:
  332. if ($quality === null || $quality > 9) {
  333. $quality = $this->quality_png;
  334. }
  335. imagepng($dest_image, $filename, $quality);
  336. break;
  337. case IMAGETYPE_BMP:
  338. imagebmp($dest_image, $filename, $quality);
  339. break;
  340. }
  341. if ($permissions) {
  342. chmod($filename, $permissions);
  343. }
  344. imagedestroy($dest_image);
  345. return $this;
  346. }
  347. /**
  348. * Convert the image to string
  349. *
  350. * @param int $image_type
  351. * @param int $quality
  352. * @return string
  353. */
  354. public function getImageAsString($image_type = null, $quality = null)
  355. {
  356. $string_temp = tempnam(sys_get_temp_dir(), '');
  357. $this->save($string_temp, $image_type, $quality);
  358. $string = file_get_contents($string_temp);
  359. unlink($string_temp);
  360. return $string;
  361. }
  362. /**
  363. * Convert the image to string with the current settings
  364. *
  365. * @return string
  366. */
  367. public function __toString()
  368. {
  369. return $this->getImageAsString();
  370. }
  371. /**
  372. * Outputs image to browser
  373. * @param string $image_type
  374. * @param integer $quality
  375. */
  376. public function output($image_type = null, $quality = null)
  377. {
  378. $image_type = $image_type ?: $this->source_type;
  379. header('Content-Type: '.image_type_to_mime_type($image_type));
  380. $this->save(null, $image_type, $quality);
  381. }
  382. /**
  383. * Resizes image according to the given short side (short side proportional)
  384. *
  385. * @param integer $max_short
  386. * @param boolean $allow_enlarge
  387. * @return static
  388. */
  389. public function resizeToShortSide($max_short, $allow_enlarge = false)
  390. {
  391. if ($this->getSourceHeight() < $this->getSourceWidth()) {
  392. $ratio = $max_short / $this->getSourceHeight();
  393. $long = (int) ($this->getSourceWidth() * $ratio);
  394. $this->resize($long, $max_short, $allow_enlarge);
  395. } else {
  396. $ratio = $max_short / $this->getSourceWidth();
  397. $long = (int) ($this->getSourceHeight() * $ratio);
  398. $this->resize($max_short, $long, $allow_enlarge);
  399. }
  400. return $this;
  401. }
  402. /**
  403. * Resizes image according to the given long side (short side proportional)
  404. *
  405. * @param integer $max_long
  406. * @param boolean $allow_enlarge
  407. * @return static
  408. */
  409. public function resizeToLongSide($max_long, $allow_enlarge = false)
  410. {
  411. if ($this->getSourceHeight() > $this->getSourceWidth()) {
  412. $ratio = $max_long / $this->getSourceHeight();
  413. $short = (int) ($this->getSourceWidth() * $ratio);
  414. $this->resize($short, $max_long, $allow_enlarge);
  415. } else {
  416. $ratio = $max_long / $this->getSourceWidth();
  417. $short = (int) ($this->getSourceHeight() * $ratio);
  418. $this->resize($max_long, $short, $allow_enlarge);
  419. }
  420. return $this;
  421. }
  422. /**
  423. * Resizes image according to the given height (width proportional)
  424. *
  425. * @param integer $height
  426. * @param boolean $allow_enlarge
  427. * @return static
  428. */
  429. public function resizeToHeight($height, $allow_enlarge = false)
  430. {
  431. $ratio = $height / $this->getSourceHeight();
  432. $width = (int) ($this->getSourceWidth() * $ratio);
  433. $this->resize($width, $height, $allow_enlarge);
  434. return $this;
  435. }
  436. /**
  437. * Resizes image according to the given width (height proportional)
  438. *
  439. * @param integer $width
  440. * @param boolean $allow_enlarge
  441. * @return static
  442. */
  443. public function resizeToWidth($width, $allow_enlarge = false)
  444. {
  445. $ratio = $width / $this->getSourceWidth();
  446. $height = (int) ($this->getSourceHeight() * $ratio);
  447. $this->resize($width, $height, $allow_enlarge);
  448. return $this;
  449. }
  450. /**
  451. * Resizes image to best fit inside the given dimensions
  452. *
  453. * @param integer $max_width
  454. * @param integer $max_height
  455. * @param boolean $allow_enlarge
  456. * @return static
  457. */
  458. public function resizeToBestFit($max_width, $max_height, $allow_enlarge = false)
  459. {
  460. if ($this->getSourceWidth() <= $max_width && $this->getSourceHeight() <= $max_height && $allow_enlarge === false) {
  461. return $this;
  462. }
  463. $ratio = $this->getSourceHeight() / $this->getSourceWidth();
  464. $width = $max_width;
  465. $height = (int) ($width * $ratio);
  466. if ($height > $max_height) {
  467. $height = $max_height;
  468. $width = (int) ($height / $ratio);
  469. }
  470. return $this->resize($width, $height, $allow_enlarge);
  471. }
  472. /**
  473. * Resizes image according to given scale (proportionally)
  474. *
  475. * @param integer|float $scale
  476. * @return static
  477. */
  478. public function scale($scale)
  479. {
  480. $width = (int) ($this->getSourceWidth() * $scale / 100);
  481. $height = (int) ($this->getSourceHeight() * $scale / 100);
  482. $this->resize($width, $height, true);
  483. return $this;
  484. }
  485. /**
  486. * Resizes image according to the given width and height
  487. *
  488. * @param integer $width
  489. * @param integer $height
  490. * @param boolean $allow_enlarge
  491. * @return static
  492. */
  493. public function resize($width, $height, $allow_enlarge = false)
  494. {
  495. if (!$allow_enlarge) {
  496. // if the user hasn't explicitly allowed enlarging,
  497. // but either of the dimensions are larger then the original,
  498. // then just use original dimensions - this logic may need rethinking
  499. if ($width > $this->getSourceWidth() || $height > $this->getSourceHeight()) {
  500. $width = $this->getSourceWidth();
  501. $height = $this->getSourceHeight();
  502. }
  503. }
  504. $this->source_x = 0;
  505. $this->source_y = 0;
  506. $this->dest_w = $width;
  507. $this->dest_h = $height;
  508. $this->source_w = $this->getSourceWidth();
  509. $this->source_h = $this->getSourceHeight();
  510. return $this;
  511. }
  512. /**
  513. * Crops image according to the given width, height and crop position
  514. *
  515. * @param integer $width
  516. * @param integer $height
  517. * @param boolean $allow_enlarge
  518. * @param integer $position
  519. * @return static
  520. */
  521. public function crop($width, $height, $allow_enlarge = false, $position = self::CROPCENTER)
  522. {
  523. if (!$allow_enlarge) {
  524. // this logic is slightly different to resize(),
  525. // it will only reset dimensions to the original
  526. // if that particular dimenstion is larger
  527. if ($width > $this->getSourceWidth()) {
  528. $width = $this->getSourceWidth();
  529. }
  530. if ($height > $this->getSourceHeight()) {
  531. $height = $this->getSourceHeight();
  532. }
  533. }
  534. $ratio_source = $this->getSourceWidth() / $this->getSourceHeight();
  535. $ratio_dest = $width / $height;
  536. if ($ratio_dest < $ratio_source) {
  537. $this->resizeToHeight($height, $allow_enlarge);
  538. $excess_width = (int) (($this->getDestWidth() - $width) * $this->getSourceWidth() / $this->getDestWidth());
  539. $this->source_w = $this->getSourceWidth() - $excess_width;
  540. $this->source_x = $this->getCropPosition($excess_width, $position);
  541. $this->dest_w = $width;
  542. } else {
  543. $this->resizeToWidth($width, $allow_enlarge);
  544. $excess_height = (int) (($this->getDestHeight() - $height) * $this->getSourceHeight() / $this->getDestHeight());
  545. $this->source_h = $this->getSourceHeight() - $excess_height;
  546. $this->source_y = $this->getCropPosition($excess_height, $position);
  547. $this->dest_h = $height;
  548. }
  549. return $this;
  550. }
  551. /**
  552. * Crops image according to the given width, height, x and y
  553. *
  554. * @param integer $width
  555. * @param integer $height
  556. * @param integer $x
  557. * @param integer $y
  558. * @return static
  559. */
  560. public function freecrop($width, $height, $x = false, $y = false)
  561. {
  562. if ($x === false || $y === false) {
  563. return $this->crop($width, $height);
  564. }
  565. $this->source_x = $x;
  566. $this->source_y = $y;
  567. if ($width > $this->getSourceWidth() - $x) {
  568. $this->source_w = $this->getSourceWidth() - $x;
  569. } else {
  570. $this->source_w = $width;
  571. }
  572. if ($height > $this->getSourceHeight() - $y) {
  573. $this->source_h = $this->getSourceHeight() - $y;
  574. } else {
  575. $this->source_h = $height;
  576. }
  577. $this->dest_w = $width;
  578. $this->dest_h = $height;
  579. return $this;
  580. }
  581. /**
  582. * Gets source width
  583. *
  584. * @return integer
  585. */
  586. public function getSourceWidth()
  587. {
  588. return $this->original_w;
  589. }
  590. /**
  591. * Gets source height
  592. *
  593. * @return integer
  594. */
  595. public function getSourceHeight()
  596. {
  597. return $this->original_h;
  598. }
  599. /**
  600. * Gets width of the destination image
  601. *
  602. * @return integer
  603. */
  604. public function getDestWidth()
  605. {
  606. return $this->dest_w;
  607. }
  608. /**
  609. * Gets height of the destination image
  610. * @return integer
  611. */
  612. public function getDestHeight()
  613. {
  614. return $this->dest_h;
  615. }
  616. /**
  617. * Gets crop position (X or Y) according to the given position
  618. *
  619. * @param integer $expectedSize
  620. * @param integer $position
  621. * @return integer
  622. */
  623. protected function getCropPosition($expectedSize, $position = self::CROPCENTER)
  624. {
  625. $size = 0;
  626. switch ($position) {
  627. case self::CROPBOTTOM:
  628. case self::CROPRIGHT:
  629. $size = $expectedSize;
  630. break;
  631. case self::CROPCENTER:
  632. case self::CROPCENTRE:
  633. $size = $expectedSize / 2;
  634. break;
  635. case self::CROPTOPCENTER:
  636. $size = $expectedSize / 4;
  637. break;
  638. }
  639. return (int) $size;
  640. }
  641. /**
  642. * Enable or not the gamma color correction on the image, enabled by default
  643. *
  644. * @param bool $enable
  645. * @return static
  646. */
  647. public function gamma($enable = false)
  648. {
  649. $this->gamma_correct = $enable;
  650. return $this;
  651. }
  652. }