source: trunk/admin/include/image.class.php @ 16936

Last change on this file since 16936 was 16936, checked in by plg, 8 years ago

merge r16934 from branch 2.4 to trunk

bug 2705 fixed: if the rotation is "0", no rotation is needed, we skip the
rotate function which behaves badly (in ExtIM) with a zero degree rotation.

File size: 21.5 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2012 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24// +-----------------------------------------------------------------------+
25// |                           Image Interface                             |
26// +-----------------------------------------------------------------------+
27
28// Define all needed methods for image class
29interface imageInterface
30{
31  function get_width();
32
33  function get_height();
34
35  function set_compression_quality($quality);
36
37  function crop($width, $height, $x, $y);
38
39  function strip();
40
41  function rotate($rotation);
42
43  function resize($width, $height);
44
45  function sharpen($amount);
46
47  function compose($overlay, $x, $y, $opacity);
48
49  function write($destination_filepath);
50}
51
52// +-----------------------------------------------------------------------+
53// |                          Main Image Class                             |
54// +-----------------------------------------------------------------------+
55
56class pwg_image
57{
58  var $image;
59  var $library = '';
60  var $source_filepath = '';
61  static $ext_imagick_version = '';
62
63  function __construct($source_filepath, $library=null)
64  {
65    $this->source_filepath = $source_filepath;
66
67    trigger_action('load_image_library', array(&$this) );
68
69    if (is_object($this->image))
70    {
71      return; // A plugin may have load its own library
72    }
73
74    $extension = strtolower(get_extension($source_filepath));
75
76    if (!in_array($extension, array('jpg', 'jpeg', 'png', 'gif')))
77    {
78      die('[Image] unsupported file extension');
79    }
80
81    if (!($this->library = self::get_library($library, $extension)))
82    {
83      die('No image library available on your server.');
84    }
85
86    $class = 'image_'.$this->library;
87    $this->image = new $class($source_filepath);
88  }
89
90  // Unknow methods will be redirected to image object
91  function __call($method, $arguments)
92  {
93    return call_user_func_array(array($this->image, $method), $arguments);
94  }
95
96  // Piwigo resize function
97  function pwg_resize($destination_filepath, $max_width, $max_height, $quality, $automatic_rotation=true, $strip_metadata=false, $crop=false, $follow_orientation=true)
98  {
99    $starttime = get_moment();
100
101    // width/height
102    $source_width  = $this->image->get_width();
103    $source_height = $this->image->get_height();
104
105    $rotation = null;
106    if ($automatic_rotation)
107    {
108      $rotation = self::get_rotation_angle($this->source_filepath);
109    }
110    $resize_dimensions = self::get_resize_dimensions($source_width, $source_height, $max_width, $max_height, $rotation, $crop, $follow_orientation);
111
112    // testing on height is useless in theory: if width is unchanged, there
113    // should be no resize, because width/height ratio is not modified.
114    if ($resize_dimensions['width'] == $source_width and $resize_dimensions['height'] == $source_height)
115    {
116      // the image doesn't need any resize! We just copy it to the destination
117      copy($this->source_filepath, $destination_filepath);
118      return $this->get_resize_result($destination_filepath, $resize_dimensions['width'], $resize_dimensions['height'], $starttime);
119    }
120
121    $this->image->set_compression_quality($quality);
122
123    if ($strip_metadata)
124    {
125      // we save a few kilobytes. For example a thumbnail with metadata weights 25KB, without metadata 7KB.
126      $this->image->strip();
127    }
128
129    if (isset($resize_dimensions['crop']))
130    {
131      $this->image->crop($resize_dimensions['crop']['width'], $resize_dimensions['crop']['height'], $resize_dimensions['crop']['x'], $resize_dimensions['crop']['y']);
132    }
133
134    $this->image->resize($resize_dimensions['width'], $resize_dimensions['height']);
135
136    if (!empty($rotation))
137    {
138      $this->image->rotate($rotation);
139    }
140
141    $this->image->write($destination_filepath);
142
143    // everything should be OK if we are here!
144    return $this->get_resize_result($destination_filepath, $resize_dimensions['width'], $resize_dimensions['height'], $starttime);
145  }
146
147  static function get_resize_dimensions($width, $height, $max_width, $max_height, $rotation=null, $crop=false, $follow_orientation=true)
148  {
149    $rotate_for_dimensions = false;
150    if (isset($rotation) and in_array(abs($rotation), array(90, 270)))
151    {
152      $rotate_for_dimensions = true;
153    }
154
155    if ($rotate_for_dimensions)
156    {
157      list($width, $height) = array($height, $width);
158    }
159
160    if ($crop)
161    {
162      $x = 0;
163      $y = 0;
164
165      if ($width < $height and $follow_orientation)
166      {
167        list($max_width, $max_height) = array($max_height, $max_width);
168      }
169
170      $img_ratio = $width / $height;
171      $dest_ratio = $max_width / $max_height;
172
173      if($dest_ratio > $img_ratio)
174      {
175        $destHeight = round($width * $max_height / $max_width);
176        $y = round(($height - $destHeight) / 2 );
177        $height = $destHeight;
178      }
179      elseif ($dest_ratio < $img_ratio)
180      {
181        $destWidth = round($height * $max_width / $max_height);
182        $x = round(($width - $destWidth) / 2 );
183        $width = $destWidth;
184      }
185    }
186
187    $ratio_width  = $width / $max_width;
188    $ratio_height = $height / $max_height;
189    $destination_width = $width;
190    $destination_height = $height;
191
192    // maximal size exceeded ?
193    if ($ratio_width > 1 or $ratio_height > 1)
194    {
195      if ($ratio_width < $ratio_height)
196      {
197        $destination_width = round($width / $ratio_height);
198        $destination_height = $max_height;
199      }
200      else
201      {
202        $destination_width = $max_width;
203        $destination_height = round($height / $ratio_width);
204      }
205    }
206
207    if ($rotate_for_dimensions)
208    {
209      list($destination_width, $destination_height) = array($destination_height, $destination_width);
210    }
211
212    $result = array(
213      'width' => $destination_width,
214      'height'=> $destination_height,
215      );
216
217    if ($crop and ($x or $y))
218    {
219      $result['crop'] = array(
220        'width' => $width,
221        'height' => $height,
222        'x' => $x,
223        'y' => $y,
224        );
225    }
226    return $result;
227  }
228
229  static function get_rotation_angle($source_filepath)
230  {
231    list($width, $height, $type) = getimagesize($source_filepath);
232    if (IMAGETYPE_JPEG != $type)
233    {
234      return null;
235    }
236
237    if (!function_exists('exif_read_data'))
238    {
239      return null;
240    }
241
242    $rotation = 0;
243
244    $exif = exif_read_data($source_filepath);
245
246    if (isset($exif['Orientation']) and preg_match('/^\s*(\d)/', $exif['Orientation'], $matches))
247    {
248      $orientation = $matches[1];
249      if (in_array($orientation, array(3, 4)))
250      {
251        $rotation = 180;
252      }
253      elseif (in_array($orientation, array(5, 6)))
254      {
255        $rotation = 270;
256      }
257      elseif (in_array($orientation, array(7, 8)))
258      {
259        $rotation = 90;
260      }
261    }
262
263    return $rotation;
264  }
265
266  static function get_rotation_code_from_angle($rotation_angle)
267  {
268    switch($rotation_angle)
269    {
270      case 0:   return 0;
271      case 90:  return 1;
272      case 180: return 2;
273      case 270: return 3;
274    }
275  }
276
277  static function get_rotation_angle_from_code($rotation_code)
278  {
279    switch($rotation_code%4)
280    {
281      case 0: return 0;
282      case 1: return 90;
283      case 2: return 180;
284      case 3: return 270;
285    }
286  }
287
288  /** Returns a normalized convolution kernel for sharpening*/
289  static function get_sharpen_matrix($amount)
290  {
291    // Amount should be in the range of 48-10
292    $amount = round(abs(-48 + ($amount * 0.38)), 2);
293
294    $matrix = array(
295      array(-1,   -1,    -1),
296      array(-1, $amount, -1),
297      array(-1,   -1,    -1),
298      );
299
300    $norm = array_sum(array_map('array_sum', $matrix));
301
302    for ($i=0; $i<3; $i++)
303    {
304      $line = & $matrix[$i];
305      for ($j=0; $j<3; $j++)
306      {
307        $line[$j] /= $norm;
308      }
309    }
310
311    return $matrix;
312  }
313
314  private function get_resize_result($destination_filepath, $width, $height, $time=null)
315  {
316    return array(
317      'source'      => $this->source_filepath,
318      'destination' => $destination_filepath,
319      'width'       => $width,
320      'height'      => $height,
321      'size'        => floor(filesize($destination_filepath) / 1024).' KB',
322      'time'        => $time ? number_format((get_moment() - $time) * 1000, 2, '.', ' ').' ms' : null,
323      'library'     => $this->library,
324    );
325  }
326
327  static function is_imagick()
328  {
329    return (extension_loaded('imagick') and class_exists('Imagick'));
330  }
331
332  static function is_ext_imagick()
333  {
334    global $conf;
335
336    if (!function_exists('exec'))
337    {
338      return false;
339    }
340    @exec($conf['ext_imagick_dir'].'convert -version', $returnarray);
341    if (is_array($returnarray) and !empty($returnarray[0]) and preg_match('/ImageMagick/i', $returnarray[0]))
342    {
343      if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match))
344      {
345        self::$ext_imagick_version = $match[1];
346      }
347      return true;
348    }
349    return false;
350  }
351
352  static function is_gd()
353  {
354    return function_exists('gd_info');
355  }
356
357  static function get_library($library=null, $extension=null)
358  {
359    global $conf;
360
361    if (is_null($library))
362    {
363      $library = $conf['graphics_library'];
364    }
365
366    // Choose image library
367    switch (strtolower($library))
368    {
369      case 'auto':
370      case 'imagick':
371        if ($extension != 'gif' and self::is_imagick())
372        {
373          return 'imagick';
374        }
375      case 'ext_imagick':
376        if ($extension != 'gif' and self::is_ext_imagick())
377        {
378          return 'ext_imagick';
379        }
380      case 'gd':
381        if (self::is_gd())
382        {
383          return 'gd';
384        }
385      default:
386        if ($library != 'auto')
387        {
388          // Requested library not available. Try another library
389          return self::get_library('auto', $extension);
390        }
391    }
392    return false;
393  }
394
395  function destroy()
396  {
397    if (method_exists($this->image, 'destroy'))
398    {
399      return $this->image->destroy();
400    }
401    return true;
402  }
403}
404
405// +-----------------------------------------------------------------------+
406// |                   Class for Imagick extension                         |
407// +-----------------------------------------------------------------------+
408
409class image_imagick implements imageInterface
410{
411  var $image;
412
413  function __construct($source_filepath)
414  {
415    // A bug cause that Imagick class can not be extended
416    $this->image = new Imagick($source_filepath);
417  }
418
419  function get_width()
420  {
421    return $this->image->getImageWidth();
422  }
423
424  function get_height()
425  {
426    return $this->image->getImageHeight();
427  }
428
429  function set_compression_quality($quality)
430  {
431    return $this->image->setImageCompressionQuality($quality);
432  }
433
434  function crop($width, $height, $x, $y)
435  {
436    return $this->image->cropImage($width, $height, $x, $y);
437  }
438
439  function strip()
440  {
441    return $this->image->stripImage();
442  }
443
444  function rotate($rotation)
445  {
446    $this->image->rotateImage(new ImagickPixel(), -$rotation);
447    $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
448    return true;
449  }
450
451  function resize($width, $height)
452  {
453    $this->image->setInterlaceScheme(Imagick::INTERLACE_LINE);
454
455    // TODO need to explain this condition
456    if ($this->get_width()%2 == 0
457        && $this->get_height()%2 == 0
458        && $this->get_width() > 3*$width)
459    {
460      $this->image->scaleImage($this->get_width()/2, $this->get_height()/2);
461    }
462
463    return $this->image->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 0.9);
464  }
465
466  function sharpen($amount)
467  {
468    $m = pwg_image::get_sharpen_matrix($amount);
469    return  $this->image->convolveImage($m);
470  }
471
472  function compose($overlay, $x, $y, $opacity)
473  {
474    $ioverlay = $overlay->image->image;
475    /*if ($ioverlay->getImageAlphaChannel() !== Imagick::ALPHACHANNEL_OPAQUE)
476    {
477      // Force the image to have an alpha channel
478      $ioverlay->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE);
479    }*/
480
481    global $dirty_trick_xrepeat;
482    if ( !isset($dirty_trick_xrepeat) && $opacity < 100)
483    {// NOTE: Using setImageOpacity will destroy current alpha channels!
484      $ioverlay->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA);
485      $dirty_trick_xrepeat = true;
486    }
487
488    return $this->image->compositeImage($ioverlay, Imagick::COMPOSITE_DISSOLVE, $x, $y);
489  }
490
491  function write($destination_filepath)
492  {
493    // use 4:2:2 chroma subsampling (reduce file size by 20-30% with "almost" no human perception)
494    $this->image->setSamplingFactors( array(2,1) );
495    return $this->image->writeImage($destination_filepath);
496  }
497}
498
499// +-----------------------------------------------------------------------+
500// |            Class for ImageMagick external installation                |
501// +-----------------------------------------------------------------------+
502
503class image_ext_imagick implements imageInterface
504{
505  var $imagickdir = '';
506  var $source_filepath = '';
507  var $width = '';
508  var $height = '';
509  var $commands = array();
510
511  function __construct($source_filepath)
512  {
513    global $conf;
514    $this->source_filepath = $source_filepath;
515    $this->imagickdir = $conf['ext_imagick_dir'];
516
517    if (strpos(@$_SERVER['SCRIPT_FILENAME'], '/kunden/') === 0)  // 1and1
518    {
519      @putenv('MAGICK_THREAD_LIMIT=1');
520    }
521
522    $command = $this->imagickdir.'identify -format "%wx%h" "'.realpath($source_filepath).'"';
523    @exec($command, $returnarray);
524    if(!is_array($returnarray) or empty($returnarray[0]) or !preg_match('/^(\d+)x(\d+)$/', $returnarray[0], $match))
525    {
526      die("[External ImageMagick] Corrupt image\n" . var_export($returnarray, true));
527    }
528
529    $this->width = $match[1];
530    $this->height = $match[2];
531  }
532
533  function add_command($command, $params=null)
534  {
535    $this->commands[$command] = $params;
536  }
537
538  function get_width()
539  {
540    return $this->width;
541  }
542
543  function get_height()
544  {
545    return $this->height;
546  }
547
548  function crop($width, $height, $x, $y)
549  {
550    $this->add_command('crop', $width.'x'.$height.'+'.$x.'+'.$y);
551    return true;
552  }
553
554  function strip()
555  {
556    $this->add_command('strip');
557    return true;
558  }
559
560  function rotate($rotation)
561  {
562    if ($rotation==90 || $rotation==270)
563    {
564      $tmp = $this->width;
565      $this->width = $this->height;
566      $this->height = $tmp;
567    }
568    $this->add_command('rotate', -$rotation);
569    $this->add_command('orient', 'top-left');
570    return true;
571  }
572
573  function set_compression_quality($quality)
574  {
575    $this->add_command('quality', $quality);
576    return true;
577  }
578
579  function resize($width, $height)
580  {
581    $this->add_command('filter', 'Lanczos');
582    $this->add_command('resize', $width.'x'.$height.'!');
583    return true;
584  }
585
586  function sharpen($amount)
587  {
588    $m = pwg_image::get_sharpen_matrix($amount);
589
590    $param ='convolve "'.count($m).':';
591    foreach ($m as $line)
592    {
593      $param .= ' ';
594      $param .= implode(',', $line);
595    }
596    $param .= '"';
597    $this->add_command('morphology', $param);
598    return true;
599  }
600
601  function compose($overlay, $x, $y, $opacity)
602  {
603    $param = 'compose dissolve -define compose:args='.$opacity;
604    $param .= ' '.escapeshellarg(realpath($overlay->image->source_filepath));
605    $param .= ' -gravity NorthWest -geometry +'.$x.'+'.$y;
606    $param .= ' -composite';
607    $this->add_command($param);
608    return true;
609  }
610
611  function write($destination_filepath)
612  {
613    $this->add_command('interlace', 'line'); // progressive rendering
614    // use 4:2:2 chroma subsampling (reduce file size by 20-30% with "almost" no human perception)
615    //
616    // option deactivated for Piwigo 2.4.1, it doesn't work fo old versions
617    // of ImageMagick, see bug:2672. To reactivate once we have a better way
618    // to detect IM version and when we know which version supports this
619    // option
620    //
621    if (version_compare(pwg_image::$ext_imagick_version, '6.6') > 0)
622    {
623      $this->add_command('sampling-factor', '4:2:2' );
624    }
625
626    $exec = $this->imagickdir.'convert';
627    $exec .= ' "'.realpath($this->source_filepath).'"';
628
629    foreach ($this->commands as $command => $params)
630    {
631      $exec .= ' -'.$command;
632      if (!empty($params))
633      {
634        $exec .= ' '.$params;
635      }
636    }
637
638    $dest = pathinfo($destination_filepath);
639    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'" 2>&1';
640    @exec($exec, $returnarray);
641
642    if (function_exists('ilog')) ilog($exec);
643    if (is_array($returnarray) && (count($returnarray)>0) )
644    {
645      if (function_exists('ilog')) ilog('ERROR', $returnarray);
646      foreach($returnarray as $line)
647        trigger_error($line, E_USER_WARNING);
648    }
649    return is_array($returnarray);
650  }
651}
652
653// +-----------------------------------------------------------------------+
654// |                       Class for GD library                            |
655// +-----------------------------------------------------------------------+
656
657class image_gd implements imageInterface
658{
659  var $image;
660  var $quality = 95;
661
662  function __construct($source_filepath)
663  {
664    $gd_info = gd_info();
665    $extension = strtolower(get_extension($source_filepath));
666
667    if (in_array($extension, array('jpg', 'jpeg')))
668    {
669      $this->image = imagecreatefromjpeg($source_filepath);
670    }
671    else if ($extension == 'png')
672    {
673      $this->image = imagecreatefrompng($source_filepath);
674    }
675    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
676    {
677      $this->image = imagecreatefromgif($source_filepath);
678    }
679    else
680    {
681      die('[Image GD] unsupported file extension');
682    }
683  }
684
685  function get_width()
686  {
687    return imagesx($this->image);
688  }
689
690  function get_height()
691  {
692    return imagesy($this->image);
693  }
694
695  function crop($width, $height, $x, $y)
696  {
697    $dest = imagecreatetruecolor($width, $height);
698
699    imagealphablending($dest, false);
700    imagesavealpha($dest, true);
701    if (function_exists('imageantialias'))
702    {
703      imageantialias($dest, true);
704    }
705
706    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
707
708    if ($result !== false)
709    {
710      imagedestroy($this->image);
711      $this->image = $dest;
712    }
713    else
714    {
715      imagedestroy($dest);
716    }
717    return $result;
718  }
719
720  function strip()
721  {
722    return true;
723  }
724
725  function rotate($rotation)
726  {
727    $dest = imagerotate($this->image, $rotation, 0);
728    imagedestroy($this->image);
729    $this->image = $dest;
730    return true;
731  }
732
733  function set_compression_quality($quality)
734  {
735    $this->quality = $quality;
736    return true;
737  }
738
739  function resize($width, $height)
740  {
741    $dest = imagecreatetruecolor($width, $height);
742
743    imagealphablending($dest, false);
744    imagesavealpha($dest, true);
745    if (function_exists('imageantialias'))
746    {
747      imageantialias($dest, true);
748    }
749
750    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
751
752    if ($result !== false)
753    {
754      imagedestroy($this->image);
755      $this->image = $dest;
756    }
757    else
758    {
759      imagedestroy($dest);
760    }
761    return $result;
762  }
763
764  function sharpen($amount)
765  {
766    $m = pwg_image::get_sharpen_matrix($amount);
767    return imageconvolution($this->image, $m, 1, 0);
768  }
769
770  function compose($overlay, $x, $y, $opacity)
771  {
772    $ioverlay = $overlay->image->image;
773    /* A replacement for php's imagecopymerge() function that supports the alpha channel
774    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
775
776    $ow = imagesx($ioverlay);
777    $oh = imagesy($ioverlay);
778
779                // Create a new blank image the site of our source image
780                $cut = imagecreatetruecolor($ow, $oh);
781
782                // Copy the blank image into the destination image where the source goes
783                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
784
785                // Place the source image in the destination image
786                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
787                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
788    imagedestroy($cut);
789    return true;
790  }
791
792  function write($destination_filepath)
793  {
794    $extension = strtolower(get_extension($destination_filepath));
795
796    if ($extension == 'png')
797    {
798      imagepng($this->image, $destination_filepath);
799    }
800    elseif ($extension == 'gif')
801    {
802      imagegif($this->image, $destination_filepath);
803    }
804    else
805    {
806      imagejpeg($this->image, $destination_filepath, $this->quality);
807    }
808  }
809
810  function destroy()
811  {
812    imagedestroy($this->image);
813  }
814}
815
816?>
Note: See TracBrowser for help on using the repository browser.