PageRenderTime 59ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/library/spoon/thumbnail/thumbnail.php

http://github.com/forkcms/forkcms
PHP | 775 lines | 370 code | 141 blank | 264 comment | 91 complexity | b3cbf56a84176ea0caf2de4e1d99719b MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Spoon Library
  4. *
  5. * This source file is part of the Spoon Library. More information,
  6. * documentation and tutorials can be found @ http://www.spoon-library.com
  7. *
  8. * @package spoon
  9. * @subpackage thumbnail
  10. *
  11. *
  12. * @author Davy Hellemans <davy@spoon-library.com>
  13. * @since 1.0.0
  14. */
  15. /**
  16. * This class is used to create thumbnails
  17. *
  18. * @package spoon
  19. * @subpackage thumbnail
  20. *
  21. *
  22. * @author Tijs Verkoyen <tijs@spoon-library.com>
  23. * @author Davy Hellemans <davy@spoon-library.com>
  24. * @since 1.0.0
  25. */
  26. class SpoonThumbnail
  27. {
  28. /**
  29. * Is enlargement allowed
  30. *
  31. * @var bool
  32. */
  33. private $allowEnlargement = false;
  34. /**
  35. * The horizontal crop position
  36. *
  37. * @var string
  38. */
  39. private $cropPositionHorizontal = 'center';
  40. /**
  41. * The vertical crop position
  42. *
  43. * @var string
  44. */
  45. private $cropPositionVertical = 'middle';
  46. /**
  47. * The path for the original image
  48. *
  49. * @var string
  50. */
  51. private $filename;
  52. /**
  53. * Should we respect the original aspect ratio?
  54. *
  55. * @var bool
  56. */
  57. private $forceOriginalAspectRatio = true;
  58. /**
  59. * The height for the thumbnail
  60. *
  61. * @var int
  62. */
  63. private $height;
  64. /**
  65. * The image resource
  66. *
  67. * @var resource
  68. */
  69. private $image;
  70. /**
  71. * The width for the thumbnail
  72. *
  73. * @var int
  74. */
  75. private $width;
  76. /**
  77. * The strict setting
  78. *
  79. * @var bool
  80. */
  81. private $strict = true;
  82. /**
  83. * Default constructor.
  84. *
  85. * @param string $filename The path to the source-image.
  86. * @param int[optional] $width The required width, if not provided it will be calculated based on the height.
  87. * @param int[optional] $height The required height, if not provided it will be calculated based on the width.
  88. * @param bool[optional] $strict Should strict-mode be activated?
  89. */
  90. public function __construct($filename, $width = null, $height = null, $strict = true)
  91. {
  92. // check if gd is available
  93. if(!extension_loaded('gd')) throw new SpoonThumbnailException('GD2 isn\'t loaded. Contact your server-admin to enable it.');
  94. // redefine vars
  95. $filename = (string) $filename;
  96. if($width != null) $width = (int) $width;
  97. if($height != null) $height = (int) $height;
  98. // set strict
  99. $this->strict = (bool) $strict;
  100. // validate
  101. if(!SpoonFile::exists($filename)) throw new SpoonThumbnailException('The sourcefile "' . $filename . '" couldn\'t be found.');
  102. // set properties
  103. $this->filename = $filename;
  104. $this->width = $width;
  105. $this->height = $height;
  106. }
  107. /**
  108. * Check if file is supported.
  109. *
  110. * @return bool True if the file is supported, false if not.
  111. * @param string $filename The path to the file tp check.
  112. */
  113. public static function isSupportedFileType($filename)
  114. {
  115. // get watermarkfile properties
  116. list($width, $height, $type) = @getimagesize($filename);
  117. // create image from sourcefile
  118. switch($type)
  119. {
  120. // gif
  121. case IMG_GIF:
  122. // jpg
  123. case IMG_JPG:
  124. // png
  125. case 3:
  126. case IMG_PNG:
  127. return true;
  128. break;
  129. default:
  130. return false;
  131. }
  132. }
  133. /**
  134. * Outputs the image as png to the browser.
  135. *
  136. * @param bool[optional] $headers Should the headers be send? This is a usefull when you're debugging.
  137. */
  138. public function parse($headers = true)
  139. {
  140. // set headers
  141. if($headers) SpoonHTTP::setHeaders('Content-type: image/png');
  142. // get current dimensions
  143. $imageProperties = @getimagesize($this->filename);
  144. // validate imageProperties
  145. if($imageProperties === false) throw new SpoonThumbnailException('The sourcefile "' . $this->filename . '" could not be found.');
  146. // set current dimensions
  147. $currentWidth = (int) $imageProperties[0];
  148. $currentHeight = (int) $imageProperties[1];
  149. $currentType = (int) $imageProperties[2];
  150. $currentMime = (string) $imageProperties['mime'];
  151. // resize image
  152. $this->resizeImage($currentWidth, $currentHeight, $currentType, $currentMime);
  153. // output image
  154. $success = @imagepng($this->image);
  155. // validate
  156. if(!$success) throw new SpoonThumbnailException('Something went wrong while outputting the image.');
  157. // cleanup the memory
  158. @imagedestroy($this->image);
  159. }
  160. /**
  161. * Saves the image to a file (quality is only used for jpg images).
  162. *
  163. * @return bool True if the image was saved, false if not.
  164. * @param string $filename The path where the image should be saved.
  165. * @param int[optional] $quality The quality to use (only applies on jpg-images).
  166. * @param int[optional] $chmod Mode that should be applied on the file.
  167. */
  168. public function parseToFile($filename, $quality = 100, $chmod = 0666)
  169. {
  170. // redefine vars
  171. $filename = (string) $filename;
  172. $quality = (int) $quality;
  173. //
  174. if(@is_writable(dirname($filename)) !== true)
  175. {
  176. // does the folder exist? if not, try to create
  177. if(!SpoonDirectory::create(dirname($filename)))
  178. {
  179. if($this->strict) throw new SpoonThumbnailException('The destination-path should be writable.');
  180. return false;
  181. }
  182. }
  183. // get extension
  184. $extension = SpoonFile::getExtension($filename);
  185. // invalid quality
  186. if(!SpoonFilter::isBetween(1, 100, $quality))
  187. {
  188. // strict?
  189. if($this->strict) throw new SpoonThumbnailException('The quality should be between 1 - 100');
  190. return false;
  191. }
  192. // invalid extension
  193. if(SpoonFilter::getValue($extension, array('gif', 'jpeg', 'jpg', 'png'), '') == '')
  194. {
  195. if($this->strict) throw new SpoonThumbnailException('Only gif, jpeg, jpg or png are allowed types.');
  196. return false;
  197. }
  198. // get current dimensions
  199. $imageProperties = @getimagesize($this->filename);
  200. // validate imageProperties
  201. if($imageProperties === false)
  202. {
  203. // strict?
  204. if($this->strict) throw new SpoonThumbnailException('The sourcefile "' . $this->filename . '" could not be found.');
  205. return false;
  206. }
  207. // set current dimensions
  208. $currentWidth = (int) $imageProperties[0];
  209. $currentHeight = (int) $imageProperties[1];
  210. $currentType = (int) $imageProperties[2];
  211. $currentMime = (string) $imageProperties['mime'];
  212. // file is the same?
  213. if(($currentType == IMAGETYPE_GIF && $extension == 'gif') || ($currentType == IMAGETYPE_JPEG && in_array($extension, array('jpg', 'jpeg'))) || ($currentType == IMAGETYPE_PNG && $extension == 'png'))
  214. {
  215. if($currentWidth == $this->width && $currentHeight == $this->height)
  216. {
  217. return SpoonDirectory::copy($this->filename, $filename, true, true, $chmod);
  218. }
  219. }
  220. // resize image
  221. $this->resizeImage($currentWidth, $currentHeight, $currentType, $currentMime);
  222. // output to file
  223. switch(strtolower($extension))
  224. {
  225. case 'gif':
  226. $return = @imagegif($this->image, $filename);
  227. break;
  228. case 'jpeg':
  229. case 'jpg':
  230. $return = @imagejpeg($this->image, $filename, $quality);
  231. break;
  232. case 'png':
  233. $return = @imagepng($this->image, $filename);
  234. break;
  235. }
  236. // chmod
  237. @chmod($filename, $chmod);
  238. // cleanup memory
  239. @imagedestroy($this->image);
  240. // return success
  241. return (bool) $return;
  242. }
  243. /**
  244. * This internal function will resize/crop the image.
  245. *
  246. * @param int $currentWidth Original width.
  247. * @param int $currentHeight Original height.
  248. * @param int $currentType Current type of image.
  249. * @param string $currentMime Current mime-type.
  250. */
  251. private function resizeImage($currentWidth, $currentHeight, $currentType, $currentMime)
  252. {
  253. // check if needed dimensions are present
  254. if(!$this->forceOriginalAspectRatio) $this->resizeImageWithoutForceAspectRatio($currentWidth, $currentHeight, $currentType, $currentMime);
  255. // FAR is on
  256. else $this->resizeImageWithForceAspectRatio($currentWidth, $currentHeight, $currentType, $currentMime);
  257. }
  258. /**
  259. * Resize the image with Force Aspect Ratio.
  260. *
  261. * @param int $currentWidth Original width.
  262. * @param int $currentHeight Original height.
  263. * @param int $currentType Current type of image.
  264. * @param string $currentMime Current mime-type.
  265. */
  266. private function resizeImageWithForceAspectRatio($currentWidth, $currentHeight, $currentType, $currentMime)
  267. {
  268. // current width is larger then current height
  269. if($currentWidth > $currentHeight)
  270. {
  271. // width is specified
  272. if($this->width !== null)
  273. {
  274. // width is specified
  275. $newWidth = $this->width;
  276. // calculate new height
  277. $newHeight = (int) floor($currentHeight * ($this->width / $currentWidth));
  278. }
  279. // height is specified
  280. elseif($this->height !== null)
  281. {
  282. // height is specified
  283. $newHeight = $this->height;
  284. // calculate new width
  285. $newWidth = (int) floor($currentWidth * ($this->height / $currentHeight));
  286. }
  287. // no dimensions
  288. else throw new SpoonThumbnailException('No width or height specified.');
  289. }
  290. // current width equals current height
  291. if($currentWidth == $currentHeight)
  292. {
  293. // width is specified
  294. if($this->width !== null)
  295. {
  296. $newWidth = $this->width;
  297. $newHeight = $this->width;
  298. }
  299. // height is specified
  300. elseif($this->height !== null)
  301. {
  302. $newWidth = $this->height;
  303. $newHeight = $this->height;
  304. }
  305. // no dimensions
  306. else throw new SpoonThumbnailException('No width or height specified.');
  307. }
  308. // current width is smaller then current height
  309. if($currentWidth < $currentHeight)
  310. {
  311. // height is specified
  312. if($this->height !== null)
  313. {
  314. // height is specified
  315. $newHeight = $this->height;
  316. // calculate new width
  317. $newWidth = (int) floor($currentWidth * ($this->height / $currentHeight));
  318. }
  319. // width is specified
  320. elseif($this->width !== null)
  321. {
  322. // width is specified
  323. $newWidth = $this->width;
  324. // calculate new height
  325. $newHeight = (int) floor($currentHeight * ($this->width / $currentWidth));
  326. }
  327. // no dimensions
  328. else throw new SpoonThumbnailException('No width or height specified.');
  329. }
  330. // check if we stay within the borders
  331. if($this->width !== null && $this->height !== null)
  332. {
  333. if($newWidth > $this->width)
  334. {
  335. // width is specified
  336. $newWidth = $this->width;
  337. // calculate new height
  338. $newHeight = (int) floor($currentHeight * ($this->width / $currentWidth));
  339. }
  340. if($newHeight > $this->height)
  341. {
  342. // height is specified
  343. $newHeight = $this->height;
  344. // calculate new width
  345. $newWidth = (int) floor($currentWidth * ($this->height / $currentHeight));
  346. }
  347. }
  348. // read current image
  349. switch($currentType)
  350. {
  351. case IMG_GIF:
  352. $currentImage = @imagecreatefromgif($this->filename);
  353. break;
  354. case IMG_JPG:
  355. $currentImage = @imagecreatefromjpeg($this->filename);
  356. break;
  357. case 3:
  358. case IMG_PNG:
  359. $currentImage = @imagecreatefrompng($this->filename);
  360. break;
  361. default:
  362. throw new SpoonThumbnailException('The file you specified "' . $currentMime . '" is not supported. Only gif, jpeg, jpg and png are supported.');
  363. }
  364. // validate image
  365. if($currentImage === false) throw new SpoonThumbnailException('The file you specified is corrupt.');
  366. // create image resource
  367. $this->image = @imagecreatetruecolor($newWidth, $newHeight);
  368. // validate
  369. if($this->image === false) throw new SpoonThumbnailException('Could not create new image.');
  370. // set transparent
  371. @imagealphablending($this->image, false);
  372. // transparency supported
  373. if(in_array($currentType, array(IMG_GIF, 3, IMG_PNG)))
  374. {
  375. // get transparent color
  376. $colorTransparent = @imagecolorallocatealpha($this->image, 0, 0, 0, 127);
  377. // any color found?
  378. if($colorTransparent !== false)
  379. {
  380. @imagefill($this->image, 0, 0, $colorTransparent);
  381. @imagesavealpha($this->image, true);
  382. }
  383. }
  384. // resize
  385. $success = @imagecopyresampled($this->image, $currentImage, 0, 0, 0, 0, $newWidth, $newHeight, $currentWidth, $currentHeight);
  386. // image creation fail
  387. if(!$success)
  388. {
  389. if($this->strict) throw new SpoonThumbnailException('Something went wrong while trying to resize the image.');
  390. return false;
  391. }
  392. // reset if needed
  393. if(!$this->allowEnlargement && $currentWidth <= $newWidth && $currentHeight <= $newHeight) $this->image = $currentImage;
  394. // set transparency for GIF, or try to
  395. if($currentType == IMG_GIF)
  396. {
  397. // get transparent index
  398. $transparentIndex = @imagecolortransparent($currentImage);
  399. // valid index
  400. if($transparentIndex > 0)
  401. {
  402. // magic
  403. $transparentColor = @imagecolorsforindex($currentImage, $transparentIndex);
  404. // validate transparent color
  405. if($transparentColor !== false)
  406. {
  407. // get color
  408. $transparentIndex = @imagecolorallocate($this->image, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
  409. // fill
  410. if($transparentIndex !== false)
  411. {
  412. @imagefill($this->image, 0, 0, $transparentIndex);
  413. @imagecolortransparent($this->image, $transparentIndex);
  414. }
  415. }
  416. }
  417. }
  418. }
  419. /**
  420. * Resize the image without Force Aspect Ratio.
  421. *
  422. * @param int $currentWidth Original width.
  423. * @param int $currentHeight Original height.
  424. * @param int $currentType Current type of image.
  425. * @param string $currentMime Current mime-type.
  426. */
  427. private function resizeImageWithoutForceAspectRatio($currentWidth, $currentHeight, $currentType, $currentMime)
  428. {
  429. // validate
  430. if($this->width === null || $this->height === null) throw new SpoonThumbnailException('If forceAspectRatio is false you have to specify width and height.');
  431. // set new size
  432. $newWidth = $this->width;
  433. $newHeight = $this->height;
  434. // read current image
  435. switch($currentType)
  436. {
  437. case IMG_GIF:
  438. $currentImage = @imagecreatefromgif($this->filename);
  439. break;
  440. case IMG_JPG:
  441. $currentImage = @imagecreatefromjpeg($this->filename);
  442. break;
  443. case 3:
  444. case IMG_PNG:
  445. $currentImage = @imagecreatefrompng($this->filename);
  446. break;
  447. default:
  448. throw new SpoonThumbnailException('The file you specified "' . $currentMime . '" is not supported. Only gif, jpeg, jpg and png are supported.');
  449. }
  450. // current width is larger then current height
  451. if($currentWidth > $currentHeight)
  452. {
  453. $tempHeight = $this->height;
  454. $tempWidth = (int) floor($currentWidth * ($this->height / $currentHeight));
  455. }
  456. // current width equals current height
  457. if($currentWidth == $currentHeight)
  458. {
  459. $tempWidth = $this->width;
  460. $tempHeight = $this->width;
  461. }
  462. // current width is smaller then current height
  463. if($currentWidth < $currentHeight)
  464. {
  465. $tempWidth = $this->width;
  466. $tempHeight = (int) floor($currentHeight * ($this->width / $currentWidth));
  467. }
  468. // recalculate
  469. if($tempWidth < $this->width || $tempHeight < $this->height)
  470. {
  471. // current width is smaller than the current height
  472. if($currentWidth < $currentHeight)
  473. {
  474. $tempHeight = $this->height;
  475. $tempWidth = (int) floor($currentWidth * ($this->height / $currentHeight));
  476. }
  477. // current width is greater than the current height
  478. if($currentWidth > $currentHeight)
  479. {
  480. $tempWidth = $this->width;
  481. $tempHeight = (int) floor($currentHeight * ($this->width / $currentWidth));
  482. }
  483. }
  484. // create image resource
  485. $tempImage = @imagecreatetruecolor($tempWidth, $tempHeight);
  486. // set transparent
  487. @imagealphablending($tempImage, false);
  488. @imagesavealpha($tempImage, true);
  489. // resize
  490. $success = @imagecopyresampled($tempImage, $currentImage, 0, 0, 0, 0, $tempWidth, $tempHeight, $currentWidth, $currentHeight);
  491. // destroy original image
  492. imagedestroy($currentImage);
  493. // image creation fail
  494. if(!$success)
  495. {
  496. if($this->strict) throw new SpoonThumbnailException('Something went wrong while resizing the image.');
  497. return false;
  498. }
  499. // calculate horizontal crop position
  500. switch($this->cropPositionHorizontal)
  501. {
  502. case 'left':
  503. $x = 0;
  504. break;
  505. case 'center':
  506. $x = (int) floor(($tempWidth - $this->width) / 2);
  507. break;
  508. case 'right':
  509. $x = (int) $tempWidth - $this->width;
  510. break;
  511. }
  512. // calculate vertical crop position
  513. switch($this->cropPositionVertical)
  514. {
  515. case 'top':
  516. $y = 0;
  517. break;
  518. case 'middle':
  519. $y = (int) floor(($tempHeight - $this->height) / 2);
  520. break;
  521. case 'bottom':
  522. $y = (int) $tempHeight - $this->height;
  523. break;
  524. }
  525. // init vars
  526. $newWidth = $this->width;
  527. $newHeight = $this->height;
  528. // validate
  529. if(!$this->allowEnlargement && ($newWidth > $currentWidth || $newHeight > $currentHeight))
  530. {
  531. if($this->strict) throw new SpoonThumbnailException('The specified width/height is larger then the original width/height. Please enable allowEnlargement.');
  532. return false;
  533. }
  534. // create image resource
  535. $this->image = @imagecreatetruecolor($this->width, $this->height);
  536. // set transparent
  537. @imagealphablending($this->image, false);
  538. $colorTransparent = @imagecolorallocatealpha($this->image, 0, 0, 0, 127);
  539. @imagefill($this->image, 0, 0, $colorTransparent);
  540. @imagesavealpha($this->image, true);
  541. // resize
  542. $success = @imagecopyresampled($this->image, $tempImage, 0, 0, $x, $y, $newWidth, $newHeight, $newWidth, $newHeight);
  543. // destroy temp
  544. @imagedestroy($tempImage);
  545. // image creation fail
  546. if(!$success)
  547. {
  548. if($this->strict) throw new SpoonThumbnailException('Something went wrong while resizing the image.');
  549. return false;
  550. }
  551. // set transparent for GIF
  552. if($currentType == IMG_GIF)
  553. {
  554. // get transparent index
  555. $transparentIndex = @imagecolortransparent($currentImage);
  556. // valid index
  557. if($transparentIndex > 0)
  558. {
  559. // magic
  560. $transparentColor = @imagecolorsforindex($currentImage, $transparentIndex);
  561. $transparentIndex = @imagecolorallocate($this->image, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
  562. // fill
  563. @imagefill($this->image, 0, 0, $transparentIndex);
  564. @imagecolortransparent($this->image, $transparentIndex);
  565. }
  566. }
  567. }
  568. /**
  569. * set the allowEnlargement, default is false.
  570. *
  571. * @param bool[optional] $on May the original image be enlarged.
  572. */
  573. public function setAllowEnlargement($on = false)
  574. {
  575. $this->allowEnlargement = (bool) $on;
  576. }
  577. /**
  578. * Sets the horizontal and vertical cropposition.
  579. *
  580. * @return mixed In strict-mode it wil return false on errors.
  581. * @param string[optional] $horizontal The horizontal crop position, possible values are: left, center, right.
  582. * @param string[optional] $vertical The vertical crop position, possible values are: top, middle, bottom.
  583. */
  584. public function setCropPosition($horizontal = 'center', $vertical = 'middle')
  585. {
  586. // redefine vars
  587. $horizontal = (string) $horizontal;
  588. $vertical = (string) $vertical;
  589. // validate horizontal
  590. if(SpoonFilter::getValue($horizontal, array('left', 'center', 'right'), '') == '')
  591. {
  592. if($this->strict) throw new SpoonThumbnailException('The horizontal crop-position "' . $horizontal . '" isn\'t valid.');
  593. return false;
  594. }
  595. // validte vertical
  596. if(SpoonFilter::getValue($vertical, array('top', 'middle', 'bottom'), '') == '')
  597. {
  598. if($this->strict) throw new SpoonThumbnailException('The vertical crop-position "' . $vertical . '" isn\'t valid.');
  599. return false;
  600. }
  601. // set properties
  602. $this->cropPositionHorizontal = $horizontal;
  603. $this->cropPositionVertical = $vertical;
  604. }
  605. /**
  606. * Enables the Force aspect ratio.
  607. *
  608. * @param bool[optional] $on Should the original aspect ratio be respected?
  609. */
  610. public function setForceOriginalAspectRatio($on = true)
  611. {
  612. $this->forceOriginalAspectRatio = (bool) $on;
  613. }
  614. /**
  615. * Set the strict option.
  616. *
  617. * @param bool[optional] $on Should strict-mode be enabled?
  618. */
  619. public function setStrict($on = true)
  620. {
  621. $this->strict = (bool) $on;
  622. }
  623. }
  624. /**
  625. * This exception is used to handle image related exceptions.
  626. *
  627. * @package spoon
  628. * @subpackage thumbnail
  629. *
  630. *
  631. * @author Tijs Verkoyen <tijs@spoon-library.com>
  632. * @since 1.0.0
  633. */
  634. class SpoonThumbnailException extends SpoonException {}