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.

rcube_image.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2005-2012, The Roundcube Dev Team |
  6. | Copyright (C) 2011-2012, Kolab Systems AG |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. | PURPOSE: |
  13. | Image resizer and converter |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Author: Aleksander Machniak <alec@alec.pl> |
  17. +-----------------------------------------------------------------------+
  18. */
  19. /**
  20. * Image resizer and converter
  21. *
  22. * @package Framework
  23. * @subpackage Utils
  24. */
  25. class rcube_image
  26. {
  27. private $image_file;
  28. const TYPE_GIF = 1;
  29. const TYPE_JPG = 2;
  30. const TYPE_PNG = 3;
  31. const TYPE_TIF = 4;
  32. public static $extensions = array(
  33. self::TYPE_GIF => 'gif',
  34. self::TYPE_JPG => 'jpg',
  35. self::TYPE_PNG => 'png',
  36. self::TYPE_TIF => 'tif',
  37. );
  38. /**
  39. * Class constructor
  40. *
  41. * @param string $filename Image file name/path
  42. */
  43. function __construct($filename)
  44. {
  45. $this->image_file = $filename;
  46. }
  47. /**
  48. * Get image properties.
  49. *
  50. * @return mixed Hash array with image props like type, width, height
  51. */
  52. public function props()
  53. {
  54. // use GD extension
  55. if (function_exists('getimagesize') && ($imsize = @getimagesize($this->image_file))) {
  56. $width = $imsize[0];
  57. $height = $imsize[1];
  58. $gd_type = $imsize['2'];
  59. $type = image_type_to_extension($imsize['2'], false);
  60. $channels = $imsize['channels'];
  61. }
  62. // use ImageMagick
  63. if (!$type && ($data = $this->identify())) {
  64. list($type, $width, $height) = $data;
  65. $channels = null;
  66. }
  67. if ($type) {
  68. return array(
  69. 'type' => $type,
  70. 'gd_type' => $gd_type,
  71. 'width' => $width,
  72. 'height' => $height,
  73. 'channels' => $channels,
  74. );
  75. }
  76. return null;
  77. }
  78. /**
  79. * Resize image to a given size. Use only to shrink an image.
  80. * If an image is smaller than specified size it will be not resized.
  81. *
  82. * @param int $size Max width/height size
  83. * @param string $filename Output filename
  84. * @param boolean $browser_compat Convert to image type displayable by any browser
  85. *
  86. * @return mixed Output type on success, False on failure
  87. */
  88. public function resize($size, $filename = null, $browser_compat = false)
  89. {
  90. $result = false;
  91. $rcube = rcube::get_instance();
  92. $convert = $rcube->config->get('im_convert_path', false);
  93. $props = $this->props();
  94. if (empty($props)) {
  95. return false;
  96. }
  97. if (!$filename) {
  98. $filename = $this->image_file;
  99. }
  100. // use Imagemagick
  101. if ($convert || class_exists('Imagick', false)) {
  102. $p['out'] = $filename;
  103. $p['in'] = $this->image_file;
  104. $type = $props['type'];
  105. if (!$type && ($data = $this->identify())) {
  106. $type = $data[0];
  107. }
  108. $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
  109. $p['intype'] = $type;
  110. // convert to an image format every browser can display
  111. if ($browser_compat && !in_array($type, array('jpg','gif','png'))) {
  112. $type = 'jpg';
  113. }
  114. // If only one dimension is greater than the limit convert doesn't
  115. // work as expected, we need to calculate new dimensions
  116. $scale = $size / max($props['width'], $props['height']);
  117. // if file is smaller than the limit, we do nothing
  118. // but copy original file to destination file
  119. if ($scale >= 1 && $p['intype'] == $type) {
  120. $result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false;
  121. }
  122. else {
  123. $valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
  124. if (in_array($type, explode(',', $valid_types))) { // Valid type?
  125. if ($scale >= 1) {
  126. $width = $props['width'];
  127. $height = $props['height'];
  128. }
  129. else {
  130. $width = intval($props['width'] * $scale);
  131. $height = intval($props['height'] * $scale);
  132. }
  133. // use ImageMagick in command line
  134. if ($convert) {
  135. $p += array(
  136. 'type' => $type,
  137. 'quality' => 75,
  138. 'size' => $width . 'x' . $height,
  139. );
  140. $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
  141. . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
  142. }
  143. // use PHP's Imagick class
  144. else {
  145. try {
  146. $image = new Imagick($this->image_file);
  147. try {
  148. // it throws exception on formats not supporting these features
  149. $image->setImageBackgroundColor('white');
  150. $image->setImageAlphaChannel(11);
  151. $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
  152. }
  153. catch (Exception $e) {
  154. // ignore errors
  155. }
  156. $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
  157. $image->setImageCompressionQuality(75);
  158. $image->setImageFormat($type);
  159. $image->stripImage();
  160. $image->scaleImage($width, $height);
  161. if ($image->writeImage($filename)) {
  162. $result = '';
  163. }
  164. }
  165. catch (Exception $e) {
  166. rcube::raise_error($e, true, false);
  167. }
  168. }
  169. }
  170. }
  171. if ($result === '') {
  172. @chmod($filename, 0600);
  173. return $type;
  174. }
  175. }
  176. // do we have enough memory? (#1489937)
  177. if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
  178. return false;
  179. }
  180. // use GD extension
  181. if ($props['gd_type']) {
  182. if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
  183. $image = imagecreatefromjpeg($this->image_file);
  184. $type = 'jpg';
  185. }
  186. else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
  187. $image = imagecreatefromgif($this->image_file);
  188. $type = 'gif';
  189. }
  190. else if($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
  191. $image = imagecreatefrompng($this->image_file);
  192. $type = 'png';
  193. }
  194. else {
  195. // @TODO: print error to the log?
  196. return false;
  197. }
  198. if ($image === false) {
  199. return false;
  200. }
  201. $scale = $size / max($props['width'], $props['height']);
  202. // Imagemagick resize is implemented in shrinking mode (see -resize argument above)
  203. // we do the same here, if an image is smaller than specified size
  204. // we do nothing but copy original file to destination file
  205. if ($scale >= 1) {
  206. $result = $this->image_file == $filename || copy($this->image_file, $filename);
  207. }
  208. else {
  209. $width = intval($props['width'] * $scale);
  210. $height = intval($props['height'] * $scale);
  211. $new_image = imagecreatetruecolor($width, $height);
  212. if ($new_image === false) {
  213. return false;
  214. }
  215. // Fix transparency of gif/png image
  216. if ($props['gd_type'] != IMAGETYPE_JPEG) {
  217. imagealphablending($new_image, false);
  218. imagesavealpha($new_image, true);
  219. $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
  220. imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
  221. }
  222. imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
  223. $image = $new_image;
  224. // fix rotation of image if EXIF data exists and specifies rotation (GD strips the EXIF data)
  225. if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
  226. $exif = exif_read_data($this->image_file);
  227. if ($exif && $exif['Orientation']) {
  228. switch ($exif['Orientation']) {
  229. case 3:
  230. $image = imagerotate($image, 180, 0);
  231. break;
  232. case 6:
  233. $image = imagerotate($image, -90, 0);
  234. break;
  235. case 8:
  236. $image = imagerotate($image, 90, 0);
  237. break;
  238. }
  239. }
  240. }
  241. if ($props['gd_type'] == IMAGETYPE_JPEG) {
  242. $result = imagejpeg($image, $filename, 75);
  243. }
  244. elseif($props['gd_type'] == IMAGETYPE_GIF) {
  245. $result = imagegif($image, $filename);
  246. }
  247. elseif($props['gd_type'] == IMAGETYPE_PNG) {
  248. $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
  249. }
  250. }
  251. if ($result) {
  252. @chmod($filename, 0600);
  253. return $type;
  254. }
  255. }
  256. // @TODO: print error to the log?
  257. return false;
  258. }
  259. /**
  260. * Convert image to a given type
  261. *
  262. * @param int $type Destination file type (see class constants)
  263. * @param string $filename Output filename (if empty, original file will be used
  264. * and filename extension will be modified)
  265. *
  266. * @return bool True on success, False on failure
  267. */
  268. public function convert($type, $filename = null)
  269. {
  270. $rcube = rcube::get_instance();
  271. $convert = $rcube->config->get('im_convert_path', false);
  272. if (!$filename) {
  273. $filename = $this->image_file;
  274. // modify extension
  275. if ($extension = self::$extensions[$type]) {
  276. $filename = preg_replace('/\.[^.]+$/', '', $filename) . '.' . $extension;
  277. }
  278. }
  279. // use ImageMagick in command line
  280. if ($convert) {
  281. $p['in'] = $this->image_file;
  282. $p['out'] = $filename;
  283. $p['type'] = self::$extensions[$type];
  284. $result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -strip -quality 75 {in} {type}:{out}', $p);
  285. if ($result === '') {
  286. chmod($filename, 0600);
  287. return true;
  288. }
  289. }
  290. // use PHP's Imagick class
  291. if (class_exists('Imagick', false)) {
  292. try {
  293. $image = new Imagick($this->image_file);
  294. $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
  295. $image->setImageCompressionQuality(75);
  296. $image->setImageFormat(self::$extensions[$type]);
  297. $image->stripImage();
  298. if ($image->writeImage($filename)) {
  299. @chmod($filename, 0600);
  300. return true;
  301. }
  302. }
  303. catch (Exception $e) {
  304. rcube::raise_error($e, true, false);
  305. }
  306. }
  307. // use GD extension (TIFF isn't supported)
  308. $props = $this->props();
  309. // do we have enough memory? (#1489937)
  310. if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
  311. return false;
  312. }
  313. if ($props['gd_type']) {
  314. if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
  315. $image = imagecreatefromjpeg($this->image_file);
  316. }
  317. else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
  318. $image = imagecreatefromgif($this->image_file);
  319. }
  320. else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
  321. $image = imagecreatefrompng($this->image_file);
  322. }
  323. else {
  324. // @TODO: print error to the log?
  325. return false;
  326. }
  327. if ($type == self::TYPE_JPG) {
  328. $result = imagejpeg($image, $filename, 75);
  329. }
  330. else if ($type == self::TYPE_GIF) {
  331. $result = imagegif($image, $filename);
  332. }
  333. else if ($type == self::TYPE_PNG) {
  334. $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
  335. }
  336. if ($result) {
  337. @chmod($filename, 0600);
  338. return true;
  339. }
  340. }
  341. // @TODO: print error to the log?
  342. return false;
  343. }
  344. /**
  345. * Checks if image format conversion is supported
  346. *
  347. * @return boolean True if specified format can be converted to another format
  348. */
  349. public static function is_convertable($mimetype = null)
  350. {
  351. $rcube = rcube::get_instance();
  352. // @TODO: check if specified mimetype is really supported
  353. return class_exists('Imagick', false) || $rcube->config->get('im_convert_path');
  354. }
  355. /**
  356. * ImageMagick based image properties read.
  357. */
  358. private function identify()
  359. {
  360. $rcube = rcube::get_instance();
  361. // use ImageMagick in command line
  362. if ($cmd = $rcube->config->get('im_identify_path')) {
  363. $args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]");
  364. $id = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args);
  365. if ($id) {
  366. return explode(' ', strtolower($id));
  367. }
  368. }
  369. // use PHP's Imagick class
  370. if (class_exists('Imagick', false)) {
  371. try {
  372. $image = new Imagick($this->image_file);
  373. return array(
  374. strtolower($image->getImageFormat()),
  375. $image->getImageWidth(),
  376. $image->getImageHeight(),
  377. );
  378. }
  379. catch (Exception $e) {}
  380. }
  381. }
  382. /**
  383. * Check if we have enough memory to load specified image
  384. */
  385. private function mem_check($props)
  386. {
  387. // image size is unknown, we can't calculate required memory
  388. if (!$props['width']) {
  389. return true;
  390. }
  391. // channels: CMYK - 4, RGB - 3
  392. $multip = ($props['channels'] ?: 3) + 1;
  393. // calculate image size in memory (in bytes)
  394. $size = $props['width'] * $props['height'] * $multip;
  395. return rcube_utils::mem_check($size);
  396. }
  397. }