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