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