PageRenderTime 52ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/controllers/components/image_version.php

https://github.com/bugcloud/cycle
PHP | 487 lines | 282 code | 39 blank | 166 comment | 98 complexity | 9319b42580500bffff82df8806888bdd MD5 | raw file
  1. <?php
  2. /** Image Version Component
  3. *
  4. * A custom component for automagically creating thumbnail versions of any image within your app.
  5. * Example controller use:
  6. * $images = $this->{$this->modelClass}->find('first');
  7. * $this->set('clear', $this->ImageVersion->flushVersion($images['Piece']['file'], array(150, 75), true));
  8. * $this->set('thumbnail', $this->ImageVersion->version(array('source' => $images['Piece']['file'], 'thumbSize' => array(150, 75))));
  9. * (that would clean out the entire folder 150x75 and then make a thumbnail again and return a path to $thumbnail for the view)
  10. *
  11. * @link http://www.shift8creative.com
  12. * @author Tom Maiaroto
  13. * @modifiedby Tom
  14. * @lastmodified 2010-05-05 01:29:32
  15. * @created 2008-09-25 01:00:00
  16. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  17. */
  18. class ImageVersionComponent extends Object {
  19. /**
  20. * Components
  21. *
  22. * @return void
  23. */
  24. //var $components = array('Session');
  25. var $controller;
  26. /**
  27. * Startup
  28. *
  29. * @param object $controller
  30. * @return void
  31. */
  32. function initialize(&$controller) {
  33. $this->controller = $controller;
  34. }
  35. /**
  36. * Returns a path to the generated thumbnail.
  37. * It will only generate a thumbnail for an image if the source is newer than the thumbnail,
  38. * or if the thumbnail doesn't exist yet.
  39. *
  40. * Note: Changing the quality later on after a thumbnail is already generated would have
  41. * no effect. Original source images would have to be updated (re-uploaded or modified via
  42. * "touch" command or some other means). Or the existing thumbnail would have to be destroyed
  43. * manually or with the flushVersions() method below.
  44. *
  45. * @modified 2009-11-10 by Kevin DeCapite (www.decapite.net)
  46. * - Changed 2 return lines to use ImageVersionComponent::formatPath() method
  47. * - See that method's comment block for details
  48. *
  49. * @modified 2010-05-03 by Tom Maiaroto
  50. * - Added "letterbox" support so resized images don't need to stretch (when not cropping), changed up some resizing math
  51. * - Changed version() method so it takes an array which makes it easier to add more options in the future, consolidated code a lot
  52. * - Added sharpening support
  53. *
  54. * @param $options Array[required] Options that change the size and cropping method of the image
  55. * - image String[required] Location of the source image.
  56. * - size Array[optional] Size of the thumbnail. Default: 75x75
  57. * - quality Int[optional] Quality of the thumbnail. Default: 85%
  58. * - crop Boolean[optional] Whether to crop the image (when one dimension is larger than specified $size)
  59. * - letterbox Mixed[optional] If defined, it needs to be an array that defines the RGB background color to use. So when crop is set to false, this will fill in the rest of the image with a background color. Note: Transparent images will have a transparent letterbox unless forced.
  60. * - force_letterbox_color Boolean[optional] Whether or not to force the letterbox color on images with transparency (gif and png images). Default: false (false meaning their letterboxes will be transparent, true meaning they get a colored letterbox which also floods behind any transparent/translucent areas of the image)
  61. * - sharpen Boolean[optional] Whether to sharpen the image version or not. Default: true (note: png and gif images are not sharpened because of possible problems with transparency)
  62. *
  63. * @return String path to thumbnail image.
  64. */
  65. function version($options = array('image'=>null, 'size'=>array(75, 75), 'quality'=>85, 'crop'=>false, 'letterbox'=>null, 'force_letterbox_color'=>false, 'sharpen'=>true)) {
  66. if(isset($options['image'])) { $source = $options['image']; } else { $source = null; }
  67. if(isset($options['size'])) { $thumbSize = $options['size']; } else { $thumbSize == array(75,75); }
  68. if(isset($options['quality'])) { $thumbQuality = $options['quality']; } else { $thumbQuality = 85; }
  69. if(isset($options['crop'])) { $crop = $options['crop']; } else { $crop = false; }
  70. if(isset($options['letterbox'])) { $letterbox = $options['letterbox']; } else { $letterbox = null; }
  71. if(is_string($letterbox)) { $letterbox = $this->_html2rgb($options['letterbox']); }
  72. if(isset($options['sharpen'])) { $sharpen = $options['sharpen']; } else { $sharpen = true; }
  73. if(isset($options['force_letterbox_color'])) { $force_letterbox_color = $options['force_letterbox_color']; } else { $force_letterbox_color = false; }
  74. // if no source provided, don't do anything
  75. if(empty($source)): return false; endif;
  76. // set defaults if null passed for any values
  77. if($thumbSize == null) { $thumbSize = array(75,75); }
  78. if($thumbQuality == null) { $thumbQuality = 85; }
  79. if($crop == null) { $crop = false; }
  80. $webroot = new Folder(WWW_ROOT);
  81. $this->webRoot = $webroot->path;
  82. // set the size
  83. $thumb_size_x = $original_thumb_size_x = $thumbSize[0];
  84. $thumb_size_y = $original_thumb_size_y = $thumbSize[1];
  85. // round the thumbnail quality in case someone provided a decimal
  86. $thumbQuality = ceil($thumbQuality);
  87. // or if a value was entered beyond the extremes
  88. if($thumbQuality > 100): $thumbQuality = 100; endif;
  89. if($thumbQuality < 0): $thumbQuality = 0; endif;
  90. // get full path of source file (note: a beginning slash doesn't matter, the File class handles that I believe)
  91. $originalFile = new File($this->webRoot . $source);
  92. $source = $originalFile->Folder->path.DS.$originalFile->name().'.'.$originalFile->ext();
  93. // if the source file doesn't exist, don't do anything
  94. if(!file_exists($source)): return false; endif;
  95. // get the destination where the new file will be saved (including file name)
  96. $pathToSave = $this->createPath($originalFile->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1]);
  97. $dest = $originalFile->Folder->path.DS.$thumb_size_x.'x'.$thumb_size_y.DS.$originalFile->name().'.'.$originalFile->ext();
  98. // First make sure it's an image that we can use (bmp support isn't added, but could be)
  99. switch(strtolower($originalFile->ext())):
  100. case 'jpg':
  101. case 'jpeg':
  102. case 'gif':
  103. case 'png':
  104. break;
  105. default:
  106. return false;
  107. break;
  108. endswitch;
  109. // Then see if the size version already exists and if so, is it older than our source image?
  110. if(file_exists($originalFile->Folder->path.DS.$thumb_size_x.'x'.$thumb_size_y.DS.$originalFile->name().'.'.$originalFile->ext())):
  111. $existingFile = new File($dest);
  112. if( date('YmdHis', $existingFile->lastChange()) > date('YmdHis', $originalFile->lastChange()) ):
  113. // if it's newer than the source, return the path. the source hasn't updated, so we don't need a new thumbnail.
  114. return $this->formatPath(substr(strstr($existingFile->Folder->path.DS.$existingFile->name().'.'.$existingFile->ext(), 'webroot'), 7));
  115. endif;
  116. endif;
  117. // Get source image dimensions
  118. $size = getimagesize($source);
  119. $width = $size[0];
  120. $height = $size[1];
  121. // $x and $y here are the image source offsets
  122. $x = NULL;
  123. $y = NULL;
  124. $dx = $dy = 0;
  125. if(($thumb_size_x > $width) && ($thumb_size_y > $height)) {
  126. $crop = false; // don't need to crop now do we?
  127. }
  128. // don't allow new width or height to be greater than the original
  129. if( $thumb_size_x > $width ) { $thumb_size_x = $width; }
  130. if( $thumb_size_y > $height ) { $thumb_size_y = $height; }
  131. // generate new w/h if not provided (cool, idiot proofing)
  132. if( $thumb_size_x && !$thumb_size_y ) {
  133. $thumb_size_y = $height * ( $thumb_size_x / $width );
  134. }
  135. elseif($thumb_size_y && !$thumb_size_x) {
  136. $thumb_size_x = $width * ( $thumb_size_y / $height );
  137. }
  138. elseif(!$thumb_size_x && !$thumb_size_y) {
  139. $thumb_size_x = $width;
  140. $thumb_size_y = $height;
  141. }
  142. // set some default values for other variables we set differently based on options like letterboxing, etc.
  143. // TODO: clean this up and consolidate variables so the image creation process is shorter and nicer
  144. $new_width = $thumb_size_x;
  145. $new_height = $thumb_size_y;
  146. $x_mid = ceil($new_width/2); //horizontal middle // TODO: possibly add options to change where the crop is from
  147. $y_mid = ceil($new_height/2); //vertical middle
  148. // If the thumbnail is square
  149. if($thumbSize[0] == $thumbSize[1]) {
  150. if($width > $height) {
  151. $x = ceil(($width - $height) / 2 );
  152. $width = $height;
  153. } elseif($height > $width) {
  154. $y = ceil(($height - $width) / 2);
  155. $height = $width;
  156. }
  157. // else if the thumbnail is rectangular, don't stretch it
  158. } else {
  159. // if we aren't cropping then keep aspect ratio and contain image within the specified size
  160. if($crop === false) {
  161. $ratio_orig = $width/$height;
  162. if ($thumb_size_x/$thumb_size_y > $ratio_orig) {
  163. $thumb_size_x = ceil($thumb_size_y*$ratio_orig);
  164. } else {
  165. $thumb_size_y = ceil($thumb_size_x/$ratio_orig);
  166. }
  167. }
  168. // if we are cropping...
  169. if($crop === true) {
  170. $ratio_orig = $width/$height;
  171. if ($thumb_size_x/$thumb_size_y > $ratio_orig) {
  172. $new_height = ceil($thumb_size_x/$ratio_orig);
  173. $new_width = $thumb_size_x;
  174. } else {
  175. $new_width = ceil($thumb_size_y*$ratio_orig);
  176. $new_height = $thumb_size_y;
  177. }
  178. $x_mid = ceil($new_width/2); //horizontal middle // TODO: possibly add options to change where the crop is from
  179. $y_mid = ceil($new_height/2); //vertical middle
  180. }
  181. }
  182. switch(strtolower($originalFile->ext())):
  183. case 'png':
  184. if($thumbQuality != 0) {
  185. $thumbQuality = ($thumbQuality - 100) / 11.111111;
  186. $thumbQuality = round(abs($thumbQuality));
  187. }
  188. $new_im = $this->_generateImage('png',$source, $dx, $dy, $x, $y, $x_mid, $y_mid, $new_width, $new_height, $original_thumb_size_x, $original_thumb_size_y, $thumb_size_x, $thumb_size_y, $height, $width, $letterbox, $crop, $sharpen, $force_letterbox_color);
  189. imagepng($new_im,$dest,$thumbQuality);
  190. imagedestroy($new_im);
  191. break;
  192. case 'gif':
  193. $new_im = $this->_generateImage('gif',$source, $dx, $dy, $x, $y, $x_mid, $y_mid, $new_width, $new_height, $original_thumb_size_x, $original_thumb_size_y, $thumb_size_x, $thumb_size_y, $height, $width, $letterbox, $crop, $sharpen, $force_letterbox_color);
  194. imagegif($new_im,$dest); // no quality setting
  195. imagedestroy($new_im);
  196. break;
  197. case 'jpg':
  198. case 'jpeg':
  199. $new_im = $this->_generateImage('jpg',$source, $dx, $dy, $x, $y, $x_mid, $y_mid, $new_width, $new_height, $original_thumb_size_x, $original_thumb_size_y, $thumb_size_x, $thumb_size_y, $height, $width, $letterbox, $crop, $sharpen, $force_letterbox_color);
  200. imagejpeg($new_im,$dest,$thumbQuality);
  201. imagedestroy($new_im);
  202. break;
  203. default:
  204. return false;
  205. break;
  206. endswitch;
  207. $outputPath = new File($dest);
  208. $finalPath = substr(strstr($outputPath->Folder->path.DS.$outputPath->name().'.'.$outputPath->ext(), 'webroot'), 7);
  209. // PHP 5.3.0 would allow for a true flag as the third argument in strstr()... which would take out "webroot" so substr() wasn't required, but for older PHP...
  210. return $this->formatPath($finalPath);
  211. }
  212. // Do all the processing...
  213. function _generateImage($type=null,$source=null, $dx=null, $dy=null, $x=null, $y=null, $x_mid=null, $y_mid=null, $new_width=null, $new_height=null, $original_thumb_size_x=null, $original_thumb_size_y=null, $thumb_size_x=null, $thumb_size_y=null, $height=null, $width=null, $letterbox=null, $crop=null, $sharpen=null, $force_letterbox_color=null) {
  214. switch($type) {
  215. case 'jpg':
  216. case 'jpeg':
  217. $im = imagecreatefromjpeg($source);
  218. break;
  219. case 'png':
  220. $im = imagecreatefrompng($source);
  221. break;
  222. case 'gif':
  223. $im = imagecreatefromgif($source);
  224. break;
  225. default:
  226. case null:
  227. return false;
  228. break;
  229. }
  230. // CREATE THE NEW IMAGE
  231. if(!empty($letterbox)) {
  232. // if letterbox, use the originally passed dimensions (keeping the final image size to whatever was requested, fitting the other image inside this box)
  233. $new_im = ImageCreatetruecolor($original_thumb_size_x,$original_thumb_size_y);
  234. // We want to now set the destination coordinates so we center the image (take overal "box" size and divide in half and subtract by final resized image size divided in half)
  235. $dx = ceil(($original_thumb_size_x / 2) - ($thumb_size_x / 2));
  236. $dy = ceil(($original_thumb_size_y / 2) - ($thumb_size_y / 2));
  237. } else {
  238. // otherwise, use adjusted resize dimensions
  239. $new_im = ImageCreatetruecolor($thumb_size_x,$thumb_size_y);
  240. }
  241. // If we're cropping, we need to use a different calculated width and height
  242. if($crop === true) {
  243. $cropped_im = imagecreatetruecolor(round($new_width), round($new_height));
  244. }
  245. if(($type == 'png') || ($type == 'gif')) {
  246. $trnprt_indx = imagecolortransparent($im);
  247. // If we have a specific transparent color that was saved with the image
  248. if ($trnprt_indx >= 0) {
  249. // Get the original image's transparent color's RGB values
  250. $trnprt_color = imagecolorsforindex($im, $trnprt_indx);
  251. // Allocate the same color in the new image resource
  252. $trnprt_indx = imagecolorallocate($new_im, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);
  253. // Completely fill the background of the new image with allocated color.
  254. imagefill($new_im, 0, 0, $trnprt_indx);
  255. // Set the background color for new image to transparent
  256. imagecolortransparent($new_im, $trnprt_indx);
  257. if(isset($cropped_im)) { imagefill($cropped_im, 0, 0, $trnprt_indx); imagecolortransparent($cropped_im, $trnprt_indx); } // do the same for the image if cropped
  258. } elseif($type == 'png') {
  259. // ...a png may, instead, have an alpha channel that determines its translucency...
  260. // Fill the (currently empty) new cropped image with a transparent background
  261. if(isset($cropped_im)) {
  262. $transparent_index = imagecolortransparent($cropped_im); // allocate
  263. //imagepalettecopy($im, $cropped_im); // Don't need to copy the pallette...
  264. imagefill($cropped_im, 0, 0, $transparent_index);
  265. //imagecolortransparent($cropped_im, $transparent_index); // we need this and the next line even?? for all the trouble i went through, i'm leaving it in case it needs to be turned back on.
  266. //imagetruecolortopalette($cropped_im, true, 256);
  267. }
  268. // Fill the new image with a transparent background
  269. imagealphablending($new_im, false);
  270. // Create/allocate a new transparent color for image
  271. $trnprt_indx = imagecolorallocatealpha($new_im, 0, 0, 0, 127); // $trnprt_indx = imagecolortransparent($new_im, imagecolorallocatealpha($new_im, 0, 0, 0, 127)); // seems to be no difference, but why call an extra function?
  272. imagefill($new_im, 0, 0, $trnprt_indx); // Completely fill the background of the new image with allocated color.
  273. imagesavealpha($new_im, true); // Restore transparency blending
  274. }
  275. }
  276. // PNG AND GIF can have transparent letterbox and that area needs to be filled too (it already is though if it's transparent)
  277. if(!empty($letterbox)) {
  278. $background_color = imagecolorallocate($new_im, 255, 255, 255); // default white
  279. if((is_array($letterbox)) && (count($letterbox) == 3)) {
  280. $background_color = imagecolorallocate($new_im, $letterbox[0], $letterbox[1], $letterbox[2]);
  281. }
  282. // Transparent images like png and gif will show the letterbox color in their transparent areas so it will look weird
  283. if(($type == 'gif') || ($type == 'png')) {
  284. // But we will give the user a choice, forcing letterbox will effectively "flood" the background with that color.
  285. if($force_letterbox_color === true) {
  286. imagealphablending($new_im, true);
  287. if(isset($cropped_im)) { imagefill($cropped_im, 0, 0, $background_color); }
  288. } else {
  289. // If the user doesn't force letterboxing color on gif and png, make it transaprent ($trnprt_indx from above)
  290. $background_color = $trnprt_indx;
  291. }
  292. }
  293. imagefill($new_im, 0, 0, $background_color);
  294. }
  295. // If cropping, we have to set some coordinates
  296. if($crop === true) {
  297. imagecopyresampled($cropped_im, $im, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
  298. // if letterbox we may have to set some coordinates as well depending on the image dimensions ($dx, $dy) unless its letterbox style
  299. if(empty($letterbox)) {
  300. imagecopyresampled($new_im, $cropped_im, 0, 0, ($x_mid-($thumb_size_x/2)), ($y_mid-($thumb_size_y/2)), $thumb_size_x, $thumb_size_y, $thumb_size_x, $thumb_size_y);
  301. } else {
  302. imagecopyresampled($new_im, $cropped_im,$dx,$dy, ($x_mid-($thumb_size_x/2)), ($y_mid-($thumb_size_y/2)), $thumb_size_x, $thumb_size_y, $thumb_size_x, $thumb_size_y);
  303. }
  304. } else {
  305. imagecopyresampled($new_im,$im,$dx,$dy,$x,$y,$thumb_size_x,$thumb_size_y,$width,$height);
  306. }
  307. // SHARPEN (optional) -- can't sharpen transparent/translucent PNG
  308. if(($sharpen === true) && ($type != 'png') && ($type != 'gif')) {
  309. $sharpness = $this->_findSharp($width, $thumb_size_x);
  310. $sharpenMatrix = array(
  311. array(-1, -2, -1),
  312. array(-2, $sharpness + 12, -2),
  313. array(-1, -2, -1)
  314. );
  315. $divisor = $sharpness;
  316. $offset = 0;
  317. imageconvolution($new_im, $sharpenMatrix, $divisor, $offset);
  318. }
  319. return $new_im;
  320. }
  321. /**
  322. * Computes for sharpening the image.
  323. *
  324. * function from Ryan Rud (http://adryrun.com)
  325. */
  326. function _findSharp($orig, $final) {
  327. $final = $final * (750.0 / $orig);
  328. $a = 52;
  329. $b = -0.27810650887573124;
  330. $c = .00047337278106508946;
  331. $result = $a + $b * $final + $c * $final * $final;
  332. return max(round($result), 0);
  333. }
  334. /**
  335. * Deletes a single thumbnail or a directory of thumbnail versions created by the component.
  336. * Useful during development, or when changing the crop flag or dimensions often to keep tidy.
  337. * Maybe say a hypothetical CMS has an admin option for a user to change the thumbnail size of
  338. * a profile photo...well, we might want to run this to clean out the old versions right?
  339. * Or when a record was deleted containing an image that has a version...afterDelete()...
  340. *
  341. * @param $source String[required] Location of a source image.
  342. * @param $thumbSize Array[optional] Size of the thumbnail. Default: 75x75
  343. * @param $clearAll Boolean[optional] Clear all the thumbnails in the same directory. Default: false
  344. *
  345. * @return
  346. */
  347. function flushVersion($source=null, $thumbSize=array(75, 75), $clearAll=false) {
  348. if((is_null($source)) || (!is_string($source))): return false; endif;
  349. $webroot = new Folder(WWW_ROOT);
  350. // take off any beginning slashes (webroot has a trailing one)
  351. if(substr($source, 0, 1) == '/'):
  352. $source = substr($source, 1);
  353. endif;
  354. $pathToFile = $webroot->path . $source;
  355. $file = new File($pathToFile);
  356. //debug($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].DS.$file->name);
  357. // REMOVE THE FILE (doesn't matter if we remove the directory too later on)
  358. if(file_exists($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1])):
  359. if(unlink($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].DS.$file->name)):
  360. //debug('The file was deleted.');
  361. else:
  362. //debug('The file could not be deleted.');
  363. endif;
  364. endif;
  365. // IF SPECIFIED, REMOVE THE DIRECTORY AND ALL FILES IN IT
  366. if($clearAll === true):
  367. if($webroot->delete($file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1])):
  368. //debug('All files in the folder: '.$file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].' have been deleted including the folder.');
  369. else:
  370. //debug('The folder: '.$file->Folder->path.DS.$thumbSize[0].'x'.$thumbSize[1].' and its files could not be deleted.');
  371. endif;
  372. endif;
  373. return;
  374. }
  375. /**
  376. * Pass a full path like /var/www/htdocs/app/webroot/files
  377. * Don't include trailing slash.
  378. *
  379. * @modified 2009-11-10 by Kevin DeCapite (www.decapite.net)
  380. * - Now allows for full path like c:\path\to\htdocs\app\webroot\files
  381. * - Changed explode() function to use DS constant instead of "/"
  382. * - Modified definition of $root var to be compatible with Windows' environments
  383. * - Added inline comments where changes were included
  384. * - Refactored tabbing and spacing for readability & consistency
  385. *
  386. * @param $path String[optional]
  387. * @return String Path.
  388. */
  389. function createPath($path = null) {
  390. //$path = $this->webRoot . 'files' . DS . $path;
  391. $directories = explode(DS, $path);
  392. // If on a Windows platform, define root accordingly (assumes <drive letter>: syntax)
  393. if (substr($directories[0], -1) == ':') {
  394. $root = $directories[0];
  395. array_shift($directories);
  396. } else {
  397. // Initialize root to empty string on *nix platforms
  398. $root = '';
  399. // looks to see if a slash was included in the path to begin with and if so it removes it
  400. if ($directories[0] == '') {
  401. array_shift($directories);
  402. }
  403. }
  404. foreach ($directories as $directory) {
  405. if (!file_exists($root.DS.$directory)) {
  406. mkdir($root.DS.$directory);
  407. }
  408. $root = $root.DS.$directory;
  409. }
  410. // put a trailing slash on
  411. $root = $root.DS;
  412. return $root;
  413. }
  414. /**
  415. * Formats a path into a URL-friendly path
  416. * Converts '\' to '/' if DS = '\'
  417. * Otherwise will do nothing
  418. *
  419. * @author Kevin DeCapite (www.decapite.net)
  420. * @created 2009-11-10
  421. * @param $path
  422. * @return unknown_type
  423. */
  424. function formatPath($path) {
  425. return str_replace(DS, '/', $path);
  426. }
  427. /**
  428. * Converts web hex value into rgb array.
  429. *
  430. * @param $color[String] The web hex string (ex. #0000 or 0000)
  431. * @return array The rgb array
  432. */
  433. function _html2rgb($color) {
  434. if ($color[0] == '#')
  435. $color = substr($color, 1);
  436. if (strlen($color) == 6)
  437. list($r, $g, $b) = array($color[0].$color[1],
  438. $color[2].$color[3],
  439. $color[4].$color[5]);
  440. elseif (strlen($color) == 3)
  441. list($r, $g, $b) = array($color[0].$color[0], $color[1].$color[1], $color[2].$color[2]);
  442. else
  443. return false;
  444. $r = hexdec($r); $g = hexdec($g); $b = hexdec($b);
  445. return array($r, $g, $b);
  446. }
  447. }
  448. ?>