/Ip/Internal/ImageHelper.php

https://gitlab.com/x33n/ImpressPages · PHP · 538 lines · 407 code · 44 blank · 87 comment · 60 complexity · 097a14fb4b9aeee3f447f6449bd3904b MD5 · raw file

  1. <?php
  2. /**
  3. * @package ImpressPages
  4. *
  5. */
  6. namespace Ip\Internal;
  7. class ImageHelper
  8. {
  9. const ERROR_MEMORY = 1; // Can't get required memory.
  10. const ERROR_INCOMPATIBLE = 2; // Incompatible file MIME type.
  11. const ERROR_WRITE = 3; // Can't write destination file.
  12. const ERROR_UNKNOWN_MIME = 4; // Can't write destination file.
  13. const ERROR_UNKNOWN_CROP_TYPE = 5; // Unknown crop type.
  14. const CROP_TYPE_FIT = 1; // Resize to fit
  15. const CROP_TYPE_CROP = 2; // Crop image if it don't fit
  16. const CROP_TYPE_WIDTH = 3; // Resize to width
  17. const CROP_TYPE_HEIGHT = 4; // Resize to height
  18. /**
  19. * @param string $imageFile
  20. * @param int $widthDest required width
  21. * @param int $heightDest required height
  22. * @param string $destDir typicaly BASE_DIR.IMAGE_URL or BASE_DIR.TMP_IMAGE_URL
  23. * @param string $type
  24. * Available types:
  25. * fit - resize to fit
  26. * crop - crop image if it don't fit
  27. * width - resize to width
  28. * height - resize to height
  29. * @param bool $forced if true, resizes image even if she fits to specified size (is smaller than required)
  30. * @param int $quality from 0 (biggest compression) to 100 (best quality)
  31. * @return string file name of resized image in destDir folder
  32. * @throws \Exception
  33. * @throws \Ip\Exception
  34. */
  35. public static function resize($imageFile, $widthDest, $heightDest, $destDir, $type, $forced, $quality)
  36. {
  37. $imageInfo = getimagesize($imageFile);
  38. if (!self::resizeRequired($imageInfo[0], $imageInfo[1], $widthDest, $heightDest, $type, $forced)) {
  39. $newName = \Ip\Internal\File\Functions::genUnoccupiedName($imageFile, $destDir);
  40. copy($imageFile, $destDir . $newName);
  41. return $newName;
  42. }
  43. if (!self::getMemoryNeeded($imageFile)) {
  44. throw new \Exception("Can't get memory needed", self::ERROR_MEMORY);
  45. }
  46. try {
  47. $image = self::createImageImage($imageFile);
  48. } catch (\Exception $e) {
  49. throw new \Ip\Exception($e->getMessage(), $e->getCode(), $e);
  50. }
  51. $imageNew = self::resizeImage($image, $widthDest, $heightDest, $imageInfo[0], $imageInfo[1], $type);
  52. $newName = \Ip\Internal\File\Functions::genUnoccupiedName($imageFile, $destDir);
  53. $newFile = $destDir . $newName;
  54. $mime = self::getMimeType($imageFile);
  55. try {
  56. self::saveImage($imageNew, $newFile, $quality, $mime);
  57. } catch (\Exception $e) {
  58. throw new \Ip\Exception($e->getMessage(), $e->getCode(), $e);
  59. }
  60. return $newName;
  61. }
  62. /**
  63. * @param string $imageFile
  64. * @param string $destDir
  65. * @param int $x1
  66. * @param int $y1
  67. * @param int $x2
  68. * @param int $y2
  69. * @param int $quality
  70. * @param int $widthDest
  71. * @param int $heightDest
  72. * @return string
  73. * @throws \Ip\Exception
  74. * @throws \Exception
  75. */
  76. public static function crop($imageFile, $destDir, $x1, $y1, $x2, $y2, $quality, $widthDest, $heightDest)
  77. {
  78. if ($widthDest === null) {
  79. $widthDest = $x2 - $x1;
  80. }
  81. if ($heightDest === null) {
  82. $heightDest = $y2 - $y1;
  83. }
  84. $imageInfo = getimagesize($imageFile);
  85. if ($imageInfo[0] == $widthDest && $imageInfo[1] == $heightDest && $x1 == 0 && $y1 == 0) { // Don't need to crop or resize.
  86. $newName = \Ip\Internal\File\Functions::genUnoccupiedName($imageFile, $destDir);
  87. copy($imageFile, $destDir . $newName);
  88. return $newName;
  89. }
  90. if (!self::getMemoryNeeded($imageFile)) {
  91. throw new \Ip\Exception("Can't get memory needed", self::ERROR_MEMORY);
  92. }
  93. try {
  94. $image = self::createImageImage($imageFile);
  95. } catch (\Exception $e) {
  96. throw new \Ip\Exception($e->getMessage(), $e->getCode(), $e);
  97. }
  98. if ($x2 - $x1 > imagesx($image) || $y2 - $y1 > imagesy(
  99. $image
  100. ) || $x1 < 0 || $y1 < 0
  101. ) { // Cropping area goes out of image edge. Fill transparent.
  102. /**
  103. * Negative coordinates x1, y1 are possible.
  104. * This part of code just adds tarnsparent edges in this way making $image required proportions.
  105. * We don't care about the size in this step.
  106. */
  107. $tmpImage = imagecreatetruecolor($x2 - $x1, $y2 - $y1);
  108. imagealphablending($tmpImage, false);
  109. imagesavealpha($tmpImage, true);
  110. $color = imagecolorallocatealpha($tmpImage, 255, 255, 255, 127);
  111. imagefilledrectangle($tmpImage, 0, 0, $x2 - $x1, $y2 - $y1, $color);
  112. if ($x1 >= 0) {
  113. $sx1 = $x1;
  114. $dx1 = 0;
  115. } else {
  116. $sx1 = 0;
  117. $dx1 = -$x1;
  118. }
  119. if ($y1 >= 0) {
  120. $sy1 = $y1;
  121. $dy1 = 0;
  122. } else {
  123. $sy1 = 0;
  124. $dy1 = -$y1;
  125. }
  126. if ($x2 - $x1 > imagesx($image)) {
  127. $sx2 = imagesx($image);
  128. // $dx2 = $x2 - $x1;
  129. $dx2 = $dx1 + imagesx($image);
  130. } else {
  131. $sx2 = $x2;
  132. $dx2 = imagesx($tmpImage);
  133. }
  134. if ($y2 - $y1 > imagesy($image)) {
  135. $sy2 = imagesy($image);
  136. $dy2 = $dy1 + imagesy($image);
  137. } else {
  138. $sy2 = $y2;
  139. $dy2 = imagesy($tmpImage);
  140. }
  141. imagecopyresampled(
  142. $tmpImage,
  143. $image,
  144. $dx1,
  145. $dy1,
  146. $sx1,
  147. $sy1,
  148. $dx2 - $dx1,
  149. $dy2 - $dy1,
  150. $sx2 - $sx1,
  151. $sy2 - $sy1
  152. );
  153. $image = $tmpImage;
  154. $sx1 = 0;
  155. $sy1 = 0;
  156. $sx2 = imagesx($image);
  157. $sy2 = imagesy($image);
  158. /* Transparency required. Transform to png. */
  159. $mime = IMAGETYPE_PNG;
  160. $path_parts = pathinfo($imageFile);
  161. if ($path_parts['extension'] != 'png') {
  162. $tmpImageName = $path_parts['filename'] . '.png';
  163. } else {
  164. $tmpImageName = $imageFile;
  165. }
  166. $newName = \Ip\Internal\File\Functions::genUnoccupiedName($tmpImageName, $destDir);
  167. } else {
  168. $sx1 = $x1;
  169. $sx2 = $x2;
  170. $sy1 = $y1;
  171. $sy2 = $y2;
  172. $mime = self::getMimeType($imageFile);
  173. $newName = \Ip\Internal\File\Functions::genUnoccupiedName($imageFile, $destDir);
  174. }
  175. /**
  176. * Our $image is required proportions. The only thing we need to do is to scale the image and save.
  177. */
  178. $imageNew = imagecreatetruecolor($widthDest, $heightDest);
  179. imagealphablending($imageNew, false);
  180. imagesavealpha($imageNew, true);
  181. $color = imagecolorallocatealpha($imageNew, 255, 255, 255, 127);
  182. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightDest, $color);
  183. imagecopyresampled($imageNew, $image, 0, 0, $sx1, $sy1, $widthDest, $heightDest, $sx2 - $sx1, $sy2 - $sy1);
  184. $newFile = $destDir . $newName;
  185. try {
  186. self::saveImage($imageNew, $newFile, $quality, $mime);
  187. } catch (\Exception $e) {
  188. throw new \Exception($e->getMessage(), $e->getCode(), $e);
  189. }
  190. return $newName;
  191. }
  192. /**
  193. * @param resource $image
  194. * @param string $fileName
  195. * @param int $quality
  196. * @return bool
  197. * @throws \Exception
  198. */
  199. public static function saveJpeg($image, $fileName, $quality)
  200. {
  201. if (!imagejpeg($image, $fileName, $quality)) {
  202. throw new \Exception("Can't write to file: " . $fileName, self::ERROR_WRITE);
  203. }
  204. return true;
  205. }
  206. /**
  207. * @param resource $image
  208. * @param string $fileName
  209. * @param int $quality
  210. * @return bool
  211. * @throws \Exception
  212. */
  213. public static function savePng($image, $fileName, $quality)
  214. {
  215. // Png quality is from 0 (no compression) to 9.
  216. $tmpQuality = $quality / 10;
  217. $tmpQuality = 9 - $tmpQuality;
  218. if ($tmpQuality < 0) {
  219. $tmpQuality = 0;
  220. }
  221. if (!imagepng($image, $fileName, $tmpQuality)) {
  222. throw new \Exception("Can't write to file: " . $fileName, self::ERROR_WRITE);
  223. }
  224. return true;
  225. }
  226. /**
  227. * @param $imageFile
  228. * @return bool|null
  229. */
  230. public static function getMemoryNeeded($imageFile)
  231. {
  232. $imageInfo = getimagesize($imageFile);
  233. if (!isset($imageInfo['channels']) || !$imageInfo['channels']) {
  234. $imageInfo['channels'] = 4;
  235. }
  236. if (!isset($imageInfo['bits']) || !$imageInfo['bits']) {
  237. $imageInfo['bits'] = 8;
  238. }
  239. if (!isset($imageInfo[0])) {
  240. $imageInfo[0] = 1;
  241. }
  242. if (!isset($imageInfo[1])) {
  243. $imageInfo[1] = 1;
  244. }
  245. $a64kb = 65536;
  246. $bytesNeeded = round(
  247. ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + $a64kb) * 1.65
  248. );
  249. return \Ip\Internal\System\Helper\SystemInfo::allocateMemory($bytesNeeded);
  250. }
  251. /**
  252. * @param string $image
  253. * @return resource
  254. * @throws \Exception
  255. */
  256. public static function createImageImage($image)
  257. {
  258. $mime = self::getMimeType($image);
  259. switch ($mime) {
  260. case IMAGETYPE_JPEG:
  261. case IMAGETYPE_JPEG2000:
  262. $originalSetting = ini_set('gd.jpeg_ignore_warning', 1);
  263. $image = imagecreatefromjpeg($image);
  264. if ($originalSetting !== false) {
  265. ini_set('gd.jpeg_ignore_warning', $originalSetting);
  266. }
  267. break;
  268. case IMAGETYPE_GIF:
  269. $image = imagecreatefromgif($image);
  270. imageAlphaBlending($image, false);
  271. imageSaveAlpha($image, true);
  272. break;
  273. case IMAGETYPE_PNG:
  274. $image = imagecreatefrompng($image);
  275. imageAlphaBlending($image, false);
  276. imageSaveAlpha($image, true);
  277. break;
  278. default:
  279. throw new \Exception("Incompatible type. Type detected: " . $mime, self::ERROR_INCOMPATIBLE);
  280. }
  281. return $image;
  282. }
  283. /**
  284. * @param string $imageFile
  285. * @return string
  286. * @throws \Exception
  287. */
  288. public static function getMimeType($imageFile)
  289. {
  290. $imageInfo = getimagesize($imageFile);
  291. if (isset($imageInfo[2])) {
  292. return $imageInfo[2];
  293. } else {
  294. throw new \Exception("Unknown file type.", self::ERROR_UNKNOWN_MIME);
  295. }
  296. }
  297. /**
  298. * @param $image
  299. * @param $widthDest
  300. * @param $heightDest
  301. * @param $widthSource
  302. * @param $heightSource
  303. * @param $type
  304. * @return resource
  305. * @throws \Exception
  306. */
  307. private static function resizeImage($image, $widthDest, $heightDest, $widthSource, $heightSource, $type)
  308. {
  309. $dest_proportion = $widthDest / $heightDest;
  310. $sourceProportion = (double)$widthSource / (double)$heightSource;
  311. switch ($type) {
  312. case self::CROP_TYPE_FIT:
  313. if ($sourceProportion > $dest_proportion) {
  314. $width_skirtumas = 0;
  315. $height_skirtumas = ($heightDest - $widthDest / ($sourceProportion)) / 2;
  316. } else {
  317. $width_skirtumas = ($widthDest - $heightDest * ($sourceProportion)) / 2;
  318. $height_skirtumas = 0;
  319. }
  320. if ($height_skirtumas == 0 && $width_skirtumas != 0) {
  321. $widthDest = $heightDest * $sourceProportion;
  322. }
  323. elseif ($height_skirtumas != 0 && $width_skirtumas == 0) {
  324. $heightDest = $widthDest / $sourceProportion;
  325. }
  326. $imageNew = imagecreatetruecolor($widthDest, $heightDest);
  327. imagealphablending($imageNew, false);
  328. imagesavealpha($imageNew, true);
  329. $color = imagecolorallocatealpha($imageNew, 255, 255, 255, 127);
  330. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightDest, $color);
  331. imagecopyresampled($imageNew, $image, 0, 0, 0, 0, $widthDest, $heightDest, $widthSource, $heightSource);
  332. break;
  333. case self::CROP_TYPE_CROP:
  334. if ($sourceProportion > $dest_proportion) {
  335. $width_skirtumas = ($widthSource - $heightSource * ($dest_proportion)) / 2;
  336. $height_skirtumas = 0;
  337. } else {
  338. $width_skirtumas = 0;
  339. $height_skirtumas = ($heightSource - $widthSource / $dest_proportion) / 2;
  340. }
  341. $imageNew = imagecreatetruecolor($widthDest, $heightDest);
  342. imagealphablending($imageNew, false);
  343. imagesavealpha($imageNew, true);
  344. $color = imagecolorallocatealpha($imageNew, 255, 255, 255, 127);
  345. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightDest, $color);
  346. imagecopyresampled(
  347. $imageNew,
  348. $image,
  349. 0,
  350. 0,
  351. $width_skirtumas,
  352. $height_skirtumas,
  353. $widthDest,
  354. $heightDest,
  355. $widthSource - $width_skirtumas * 2,
  356. $heightSource - $height_skirtumas * 2
  357. );
  358. break;
  359. case self::CROP_TYPE_WIDTH:
  360. $heightTmp = $widthDest / $sourceProportion;
  361. $imageNew = imagecreatetruecolor($widthDest, $heightTmp);
  362. imagealphablending($imageNew, false);
  363. imagesavealpha($imageNew, true);
  364. $color = imagecolorallocatealpha($imageNew, 255, 255, 255, 127);
  365. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightTmp, $color);
  366. imagecopyresampled($imageNew, $image, 0, 0, 0, 0, $widthDest, $heightTmp, $widthSource, $heightSource);
  367. if ($heightTmp > $heightDest) {
  368. $image = $imageNew;
  369. $imageNew = imagecreatetruecolor($widthDest, $heightDest);
  370. $color = imagecolorallocate($imageNew, 255, 255, 255);
  371. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightDest, $color);
  372. imagecopyresampled($imageNew, $image, 0, 0, 0, 0, $widthDest, $heightDest, $widthDest, $heightDest);
  373. }
  374. break;
  375. case self::CROP_TYPE_HEIGHT:
  376. $widthTmp = $heightDest * $sourceProportion;
  377. $imageNew = imagecreatetruecolor($widthTmp, $heightDest);
  378. imagealphablending($imageNew, false);
  379. imagesavealpha($imageNew, true);
  380. $color = imagecolorallocatealpha($imageNew, 255, 255, 255, 127);
  381. imagefilledrectangle($imageNew, 0, 0, $widthTmp, $heightDest, $color);
  382. imagecopyresampled($imageNew, $image, 0, 0, 0, 0, $widthTmp, $heightDest, $widthSource, $heightSource);
  383. if ($widthTmp > $widthDest) {
  384. $image = $imageNew;
  385. $imageNew = imagecreatetruecolor($widthDest, $heightDest);
  386. $color = imagecolorallocate($imageNew, 255, 255, 255);
  387. imagefilledrectangle($imageNew, 0, 0, $widthDest, $heightDest, $color);
  388. imagecopyresampled($imageNew, $image, 0, 0, 0, 0, $widthDest, $heightDest, $widthDest, $heightDest);
  389. }
  390. break;
  391. default:
  392. throw new \Exception("Unknown crop type: " . $type, self::ERROR_UNKNOWN_CROP_TYPE);
  393. }
  394. return $imageNew;
  395. }
  396. /**
  397. * @param $widthS
  398. * @param $heightS
  399. * @param $widthT
  400. * @param $heightT
  401. * @param $type
  402. * @param $forced
  403. * @return bool
  404. * @throws \Exception
  405. */
  406. private static function resizeRequired($widthS, $heightS, $widthT, $heightT, $type, $forced)
  407. {
  408. switch ($type) {
  409. case self::CROP_TYPE_FIT:
  410. if ($forced) {
  411. return $widthS != $widthT || $heightS != $heightT;
  412. } else {
  413. return $widthS > $widthT || $heightS > $heightT;
  414. }
  415. break;
  416. case self::CROP_TYPE_CROP:
  417. if ($forced) {
  418. return $widthS != $widthT || $heightS != $heightT;
  419. } else {
  420. return $widthS > $widthT || $heightS > $heightT;
  421. }
  422. break;
  423. case self::CROP_TYPE_WIDTH:
  424. if ($forced) {
  425. return $widthS != $widthT;
  426. } else {
  427. return $widthS > $widthT;
  428. }
  429. break;
  430. case self::CROP_TYPE_HEIGHT:
  431. if ($forced) {
  432. return $heightS != $heightT;
  433. } else {
  434. return $heightS > $heightT;
  435. }
  436. break;
  437. default:
  438. throw new \Exception("Unknown crop type: " . $type, self::ERROR_UNKNOWN_CROP_TYPE);
  439. }
  440. }
  441. /**
  442. * @param string $imageNew
  443. * @param string $newFile
  444. * @param int $quality
  445. * @param string $mime
  446. * @throws \Exception
  447. */
  448. private static function saveImage($imageNew, $newFile, $quality, $mime)
  449. {
  450. switch ($mime) {
  451. case IMAGETYPE_GIF:
  452. case IMAGETYPE_PNG:
  453. try {
  454. // Fill transparent places with white.
  455. /*
  456. $width = imagesx($imageNew);
  457. $height = imagesy($imageNew);
  458. $imageBg = imagecreatetruecolor($width, $height);
  459. imagealphablending($imageBg, false);
  460. imagesavealpha($imageBg,true);
  461. imagealphablending($imageNew, true);
  462. imagesavealpha($imageNew, true);
  463. $color = imagecolorallocatealpha($imageBg, 255, 255, 0, 0);
  464. imagefilledrectangle($imageBg, 0, 0, $width, $height, $color);
  465. imagecopymerge($imageBg, $imageNew, 0, 0, 0, 0, $width, $height, 50);
  466. */
  467. self::savePng($imageNew, $newFile, 9); // 9 - Maximum compression. PNG is always lossless.
  468. } catch (\Exception $e) {
  469. throw new \Exception ($e->getMessage(), $e->getCode(), $e);
  470. }
  471. break;
  472. case IMAGETYPE_JPEG2000:
  473. case IMAGETYPE_JPEG:
  474. default:
  475. try {
  476. self::saveJpeg($imageNew, $newFile, $quality);
  477. } catch (\Exception $e) {
  478. throw new \Exception ($e->getMessage(), $e->getCode(), $e);
  479. }
  480. break;
  481. }
  482. }
  483. }