PageRenderTime 255ms CodeModel.GetById 29ms RepoModel.GetById 4ms app.codeStats 0ms

/core/classes/framework/image/image.php

https://github.com/GinoPane/fantasia
PHP | 774 lines | 408 code | 111 blank | 255 comment | 44 complexity | 8de680d0aee18b2f89ef2a899592f99c MD5 | raw file
  1. <?php
  2. class Image {
  3. const MIME_TYPE_JPEG = "image/jpeg";
  4. const MIME_TYPE_PNG = "image/png";
  5. const MIME_TYPE_GIF = "image/gif";
  6. const MIME_TYPE_WBMP = "image/wbmp";
  7. const FLIP_HORIZONTAL = "y";
  8. const FLIP_VERTICAL = "x";
  9. const ORIENTATION_PORTRAIT = 1;
  10. const ORIENTATION_LANDSCAPE = 2;
  11. const ORIENTATION_SQUARE = 3;
  12. const OUTPUT_JPEG = "jpeg";
  13. const OUTPUT_JPG = "jpg";
  14. const OUTPUT_PNG = "png";
  15. const OUTPUT_GIF = "gif";
  16. const DEFAULT_IMAGE_QUALITY = 90;
  17. /**
  18. *
  19. * @var string
  20. */
  21. private $imagePath = "";
  22. /**
  23. *
  24. * @var resource
  25. */
  26. private $image;
  27. /**
  28. *
  29. * @var array
  30. */
  31. private $originalInfo = array();
  32. /**
  33. *
  34. * @param string $imageFile path to the image file
  35. *
  36. * @return \Image
  37. * @throws Exception
  38. */
  39. public function __construct($imageFile = null) {
  40. if (!$imageFile){
  41. throw new Exception('Path to the image file required!');
  42. }
  43. if(!extension_loaded('gd')){
  44. throw new Exception('Required extension GD is not loaded!');
  45. }
  46. if (!is_readable($imageFile)){
  47. throw new Exception('File is not readable!');
  48. }
  49. if (!$this->_load($imageFile)){
  50. throw new Exception('Can not load image file!');
  51. } else {
  52. return $this;
  53. }
  54. }
  55. public function __destruct() {
  56. if($this->image){
  57. imagedestroy($this->image);
  58. }
  59. }
  60. public function _load($filename) {
  61. $this->imagePath = $filename;
  62. $info = getimagesize($this->imagePath);
  63. if ($info) {
  64. switch($info['mime']) {
  65. case self::MIME_TYPE_GIF:
  66. $this->image = imagecreatefromgif($this->imagePath);
  67. break;
  68. case self::MIME_TYPE_JPEG:
  69. $this->image = imagecreatefromjpeg($this->imagePath);
  70. break;
  71. case self::MIME_TYPE_PNG:
  72. $this->image = imagecreatefrompng($this->imagePath);
  73. break;
  74. case self::MIME_TYPE_WBMP:
  75. $this->image = imagecreatefromwbmp($this->imagePath);
  76. break;
  77. default:
  78. throw new Exception('Invalid image: ' . $this->imagePath);
  79. break;
  80. }
  81. imagesavealpha($this->image, true);
  82. imagealphablending($this->image, true);
  83. $this->originalInfo = array(
  84. 'image' => $this->image,
  85. 'width' => $info[0],
  86. 'height' => $info[1],
  87. 'orientation' => $this->getOrientation(),
  88. 'exif' => function_exists('exif_read_data') ? @exif_read_data($this->imagePath) : array(),
  89. 'format' => $this->fileExt($this->imagePath),
  90. 'mime' => $info['mime']
  91. );
  92. return true;
  93. } else {
  94. return false;
  95. }
  96. }
  97. /**
  98. *
  99. * Restores original image
  100. *
  101. * @return \Image
  102. */
  103. public function restore()
  104. {
  105. $this->image = $this->originalInfo['image'];
  106. return $this;
  107. }
  108. /**
  109. *
  110. * Saves image or outputs it to the browser.
  111. * Available oprions are:
  112. * - filename - name of destination file. Outputs to the browser if no file
  113. * - format - image save format. Will be set from 'filename' option if there is one
  114. * or from original image info
  115. * - quality - quality of the image from 0 to 100. Effects on 'jpg' and 'png' output
  116. *
  117. * @param array $options
  118. * @return \Image
  119. * @throws Exception
  120. */
  121. public function save(array $options = array()) {
  122. $filename = "";
  123. $format = "";
  124. $quality = self::DEFAULT_IMAGE_QUALITY;
  125. extract($options, EXTR_IF_EXISTS | EXTR_OVERWRITE);
  126. if (!$format){
  127. if ($filename){
  128. $format = $this->fileExt($filename);
  129. } else {
  130. $format = $this->originalInfo['format'];
  131. }
  132. }
  133. switch(strtolower($format)) {
  134. case self::OUTPUT_GIF: {
  135. $result = imagegif($this->image, $filename);
  136. break;
  137. }
  138. case self::OUTPUT_JPEG:
  139. case self::OUTPUT_JPG: {
  140. $quality = $this->keepWithin($quality, 0, 100);
  141. $result = imagejpeg($this->image, $filename, $quality);
  142. break;
  143. }
  144. case self::OUTPUT_PNG: {
  145. if($quality >= 10) {
  146. $quality = ($quality - $quality % 10) / 10;;
  147. }
  148. $quality = $this->keepWithin($quality, 0, 9);
  149. $filters = null;
  150. if (isset($options['pngfilters'])){
  151. $filters = $options['pngfilters'];
  152. }
  153. $result = imagepng($this->image, $filename, $quality, $filters);
  154. break;
  155. }
  156. default:
  157. throw new Exception("Unsupported format image format: '{$format}'");
  158. }
  159. if(!$result)
  160. throw new Exception('Unable to save image: ' . $filename);
  161. return $this;
  162. }
  163. /**
  164. *
  165. * Returns information about original image
  166. *
  167. * @return array
  168. */
  169. public function getOriginalInfo(){
  170. return $this->originalInfo;
  171. }
  172. /**
  173. *
  174. * Returns mime type of the image
  175. *
  176. * @return string
  177. */
  178. public function getMimeType(){
  179. return $this->originalInfo['mime'];
  180. }
  181. /**
  182. *
  183. * Get current image width
  184. *
  185. * @return int
  186. */
  187. public function getWidth() {
  188. return imagesx($this->image);
  189. }
  190. /**
  191. *
  192. * Get current image height
  193. *
  194. * @return int
  195. */
  196. public function getHeight() {
  197. return imagesy($this->image);
  198. }
  199. /**
  200. *
  201. * Get current image orientation
  202. *
  203. * @return string
  204. */
  205. public function getOrientation() {
  206. if($this->getWidth() > $this->getHeight()){
  207. return self::ORIENTATION_LANDSCAPE;
  208. }
  209. if($this->getWidth() < $this->getHeight()){
  210. return self::ORIENTATION_PORTRAIT;
  211. }
  212. return self::ORIENTATION_SQUARE;
  213. }
  214. /**
  215. *
  216. * @param string $direction
  217. * @return \Image
  218. */
  219. private function flip($direction) {
  220. $height = $this->getHeight();
  221. $width = $this->getWidth();
  222. $new = imagecreatetruecolor($width, $height);
  223. imagealphablending($new, false);
  224. imagesavealpha($new, true);
  225. switch( strtolower($direction) ) {
  226. case self::FLIP_VERTICAL: {
  227. for($y = 0; $y < $height; $y++){
  228. imagecopy($new, $this->image, 0, $y, 0, $height - $y - 1, $width, 1);
  229. }
  230. break;
  231. }
  232. case self::FLIP_HORIZONTAL: {
  233. for($x = 0; $x < $width; $x++){
  234. imagecopy($new, $this->image, $x, 0, $width - $x - 1, 0, 1, $height);
  235. }
  236. break;
  237. }
  238. }
  239. $this->image = $new;
  240. return $this;
  241. }
  242. public function flipHorizontal(){
  243. return $this->flip(self::FLIP_HORIZONTAL);
  244. }
  245. public function flipVertical(){
  246. return $this->flip(self::FLIP_VERTICAL);
  247. }
  248. /**
  249. *
  250. * Rotates image by $angle degrees
  251. *
  252. * @param int $angle
  253. * @param string $bgColor hex color value
  254. * @return \Image
  255. * @throws Exception
  256. */
  257. public function rotate($angle, $bgColor = '#000000') {
  258. if (function_exists('imagerotate')){
  259. $rgb = $this->hex2rgb($bgColor);
  260. $bgColor = imagecolorallocate($this->image, $rgb['r'], $rgb['g'], $rgb['b']);
  261. $new = imagerotate($this->image, ($this->keepWithin($angle, -360, 360)), $bgColor);
  262. imagesavealpha($new, true);
  263. imagealphablending($new, true);
  264. $this->image = $new;
  265. return $this;
  266. } else {
  267. throw new Exception("Image rotate function is not supported");
  268. }
  269. }
  270. /**
  271. *
  272. * Uses Exif orientation tag to orient image respectively
  273. *
  274. * @return \Image
  275. */
  276. public function autoOrient() {
  277. if (isset($this->original_info['exif']['Orientation'])){
  278. switch( $this->original_info['exif']['Orientation'] ) {
  279. case 1:{
  280. // Do nothing
  281. break;
  282. }
  283. case 2: {
  284. // Flip horizontal
  285. $this->flipHorizontal();
  286. break;
  287. }
  288. case 3: {
  289. // Rotate 180 counterclockwise
  290. $this->rotate(180);
  291. break;
  292. }
  293. case 4: {
  294. // vertical flip
  295. $this->flipVertical();
  296. break;
  297. }
  298. case 5: {
  299. // flip vertically and rotate 90 clockwise
  300. $this->flipVertical()->rotate(-90);
  301. break;
  302. }
  303. case 6: {
  304. // Rotate 90 clockwise
  305. $this->rotate(-90);
  306. break;
  307. }
  308. case 7: {
  309. // flip horizontally and rotate 90 clockwise
  310. $this->flipHorizontal()->rotate(-90);
  311. break;
  312. }
  313. case 8: {
  314. // Rotate 90 counterclockwise
  315. $this->rotate(90);
  316. break;
  317. }
  318. }
  319. }
  320. return $this;
  321. }
  322. /**
  323. *
  324. * Resizes image to the specified dimensions
  325. *
  326. * @param type $width
  327. * @param type $height
  328. * @return \Image
  329. */
  330. public function resize($width, $height, $keepAspectRatio = false) {
  331. $new = imagecreatetruecolor($width, $height);
  332. imagealphablending($new, false);
  333. imagesavealpha($new, true);
  334. $dstX = 0;
  335. $dstY = 0;
  336. $dstWidth = $width;
  337. $dstHeight = $height;
  338. if($keepAspectRatio){
  339. $srcAspectRatio = $this->getWidth() / $this->getHeight();
  340. $dstAspectRatio = $width / $height;
  341. if ($srcAspectRatio > $dstAspectRatio) {
  342. $dstHeight = $width / $srcAspectRatio;
  343. $dstY = round(($height - $dstHeight) / 2);
  344. } elseif($srcAspectRatio < $dstAspectRatio) {
  345. $dstWidth = $height * $srcAspectRatio;
  346. $dstX = round(($width - $dstWidth) / 2);
  347. }
  348. }
  349. imagecopyresampled($new, $this->image, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $this->getWidth(), $this->getHeight());
  350. $this->image = $new;
  351. return $this;
  352. }
  353. /**
  354. *
  355. * @param type $width
  356. * @return type
  357. */
  358. public function fitToWidth($width) {
  359. $aspectRatio = $this->getHeight() / $this->getWidth();
  360. $height = $width * $aspectRatio;
  361. return $this->resize($width, $height);
  362. }
  363. /**
  364. *
  365. * @param type $height
  366. * @return type
  367. */
  368. public function fitToHeight($height) {
  369. $aspectRatio = $this->getHeight() / $this->getWidth();
  370. $width = $height / $aspectRatio;
  371. return $this->resize($width, $height);
  372. }
  373. /**
  374. *
  375. * Best fit to specified dimensions
  376. *
  377. * @param type $maxWidth
  378. * @param type $maxHeight
  379. * @return \Image
  380. */
  381. public function bestFit($maxWidth, $maxHeight) {
  382. if($this->getWidth() == $maxWidth && $this->getHeight() == $maxHeight){
  383. return $this;
  384. }
  385. $aspectRatio = $this->getHeight() / $this->getWidth();
  386. // Make width fit into new dimensions
  387. if( $this->getWidth() > $maxWidth ) {
  388. $width = $maxWidth;
  389. $height = $width * $aspectRatio;
  390. } else {
  391. $width = $this->getWidth();
  392. $height = $this->getHeight();
  393. }
  394. // Make height fit into new dimensions
  395. if( $height > $maxHeight ) {
  396. $height = $maxHeight;
  397. $width = $height / $aspectRatio;
  398. }
  399. return $this->resize($width, $height);
  400. }
  401. /**
  402. *
  403. * Crops the image
  404. *
  405. * @param int $x1 left
  406. * @param int $y1 top
  407. * @param int $x2 right
  408. * @param int $y2 bottom
  409. * @return \Image
  410. */
  411. public function crop($x1, $y1, $x2, $y2) {
  412. if( $x2 < $x1 ) list($x1, $x2) = array($x2, $x1);
  413. if( $y2 < $y1 ) list($y1, $y2) = array($y2, $y1);
  414. $cropWidth = $this->keepWithin($x2 - $x1, 0, $this->getWidth() - $x1);
  415. $cropHeight = $this->keepWithin($y2 - $y1, 0, $this->getHeight() - $y1);
  416. $new = imagecreatetruecolor($cropWidth, $cropHeight);
  417. imagealphablending($new, false);
  418. imagesavealpha($new, true);
  419. imagecopyresampled($new, $this->image, 0, 0, $x1, $y1, $cropWidth, $cropHeight, $cropWidth, $cropHeight);
  420. $this->image = $new;
  421. return $this;
  422. }
  423. /**
  424. *
  425. * Crops the biggest part of the image best for $xWidth, $yWidth.
  426. *
  427. * @param int $xWidth
  428. * @param int $yWidth
  429. * @return \Image
  430. */
  431. public function centerCrop($xWidth, $yWidth) {
  432. $height = $this->getHeight();
  433. $width = $this->getWidth();
  434. $srcAspectRatio = $this->getWidth() / $this->getHeight();
  435. $dstAspectRatio = $xWidth / $yWidth;
  436. if($srcAspectRatio > $dstAspectRatio) {
  437. $xOffsetWidth = $height * $dstAspectRatio;
  438. $yOffsetHeight = $height;
  439. $xOffset = ($width - $xOffsetWidth) / 2;
  440. $yOffset = 0;
  441. } elseif ($srcAspectRatio <= $dstAspectRatio) {
  442. $xOffsetWidth = $width;
  443. $yOffsetHeight = $width / $dstAspectRatio;
  444. $xOffset = 0;
  445. $yOffset = ($height - $yOffsetHeight) / 2;
  446. }
  447. $this->crop($xOffset, $yOffset, $xOffset + $xOffsetWidth, $yOffset + $yOffsetHeight);
  448. $this->resize($xWidth, $yWidth);
  449. return $this;
  450. }
  451. /**
  452. *
  453. * Grayscale the image
  454. *
  455. * @return \Image
  456. */
  457. public function grayscale() {
  458. imagefilter($this->image, IMG_FILTER_GRAYSCALE);
  459. return $this;
  460. }
  461. /**
  462. *
  463. * Negate the image
  464. *
  465. * @return \Image
  466. */
  467. public function negate() {
  468. imagefilter($this->image, IMG_FILTER_NEGATE);
  469. return $this;
  470. }
  471. /**
  472. *
  473. * Changes brightness level
  474. *
  475. * @param int $level
  476. * @return \Image
  477. */
  478. public function brightness($level) {
  479. imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $this->keepWithin($level, -255, 255));
  480. return $this;
  481. }
  482. /**
  483. *
  484. * Changes contrast level
  485. *
  486. * @param int $level
  487. * @return \Image
  488. */
  489. public function contrast($level) {
  490. imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keepWithin($level, -100, 100));
  491. return $this;
  492. }
  493. /**
  494. *
  495. * Colorizes
  496. *
  497. * @param string $color hex color
  498. * @param float $opacity 0 to 1 value
  499. * @return \Image
  500. */
  501. public function colorize($color, $opacity = 0) {
  502. $rgb = $this->hex2rgb($color);
  503. $alpha = $this->keepWithin(127 - (127 * $opacity), 0, 127);
  504. imagefilter($this->image, IMG_FILTER_COLORIZE, $this->keepWithin($rgb['r'], 0, 255), $this->keepWithin($rgb['g'], 0, 255), $this->keepWithin($rgb['b'], 0, 255), $alpha);
  505. return $this;
  506. }
  507. /**
  508. *
  509. * Uses edge detection to highlight the edges in the image
  510. *
  511. * @return \Image
  512. */
  513. public function edges() {
  514. imagefilter($this->image, IMG_FILTER_EDGEDETECT);
  515. return $this;
  516. }
  517. /**
  518. *
  519. * Embosses the image
  520. *
  521. * @return \Image
  522. */
  523. public function emboss() {
  524. imagefilter($this->image, IMG_FILTER_EMBOSS);
  525. return $this;
  526. }
  527. /**
  528. *
  529. * Uses mean removal to achieve a "sketchy" effect
  530. *
  531. * @return \Image
  532. */
  533. public function meanRemove() {
  534. imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
  535. return $this;
  536. }
  537. /**
  538. *
  539. * Alias of meanRemove()
  540. *
  541. * @return type
  542. */
  543. public function sketch(){
  544. return $this->meanRemove();
  545. }
  546. /**
  547. * Blurs the image using the Gaussian method
  548. *
  549. * @param int $passes
  550. * @return \Image
  551. */
  552. public function gaussianBlur($passes = 1){
  553. for($i = 0; $i < $passes; $i++){
  554. imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR);
  555. }
  556. return $this;
  557. }
  558. /**
  559. * Blurs the image
  560. *
  561. * @param int $passes
  562. * @return \Image
  563. */
  564. public function selectiveBlur($passes = 1){
  565. for($i = 0; $i < $passes; $i++){
  566. imagefilter($this->image, IMG_FILTER_SELECTIVE_BLUR);
  567. }
  568. return $this;
  569. }
  570. /**
  571. *
  572. * Makes the image smoother
  573. *
  574. * @param type $level
  575. * @return \Image
  576. */
  577. public function smooth($level) {
  578. imagefilter($this->image, IMG_FILTER_SMOOTH, $this->keepWithin($level, -10, 10));
  579. return $this;
  580. }
  581. /**
  582. *
  583. * Applies pixelation effect to the image
  584. *
  585. * @param type $blockSize
  586. * @return \Image
  587. */
  588. public function pixelate($blockSize = 10) {
  589. imagefilter($this->image, IMG_FILTER_PIXELATE, $blockSize, true);
  590. return $this;
  591. }
  592. /**
  593. *
  594. * Makes sepia effect
  595. *
  596. * @return \Image
  597. */
  598. public function sepia() {
  599. imagefilter($this->image, IMG_FILTER_GRAYSCALE);
  600. $this->colorize('#704214');
  601. return $this;
  602. }
  603. /**
  604. *
  605. * Ensure that the value is always between $min and $max
  606. *
  607. * @param mixed $value
  608. * @param mixed $min
  609. * @param mixed $max
  610. * @return mixed
  611. */
  612. private function keepWithin($value, $min, $max) {
  613. if( $value < $min ) return $min;
  614. if( $value > $max ) return $max;
  615. return $value;
  616. }
  617. /**
  618. *
  619. * Returns file extension
  620. *
  621. * @param string $filename
  622. * @return string
  623. */
  624. private function fileExt($filename) {
  625. if(!preg_match('/\./', $filename)) {
  626. return '';
  627. }
  628. return preg_replace('/^.*\./', '', $filename);
  629. }
  630. /**
  631. *
  632. * Converts hex color to its RGB representation
  633. *
  634. * @param type $hex_color
  635. * @return boolean
  636. */
  637. private function hex2rgb($hex_color) {
  638. if($hex_color[0] == '#'){
  639. $hex_color = substr($hex_color, 1);
  640. }
  641. if(strlen($hex_color) == 6) {
  642. list($r, $g, $b) = array(
  643. $hex_color[0] . $hex_color[1],
  644. $hex_color[2] . $hex_color[3],
  645. $hex_color[4] . $hex_color[5]
  646. );
  647. } elseif(strlen($hex_color) == 3) {
  648. list($r, $g, $b) = array(
  649. $hex_color[0] . $hex_color[0],
  650. $hex_color[1] . $hex_color[1],
  651. $hex_color[2] . $hex_color[2]
  652. );
  653. } else {
  654. trigger_error("Hex color format is incorrect", E_USER_WARNING);
  655. return false;
  656. }
  657. return array(
  658. 'r' => hexdec($r),
  659. 'g' => hexdec($g),
  660. 'b' => hexdec($b)
  661. );
  662. }
  663. }