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

Last change on this file since 28199 was 28199, checked in by rvelices, 10 years ago

external imagick fix watermark issue if the watermark is larger than target derivative (the width/height of image object did not reflect applied resize/crop)

File size: 21.7 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2014 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->width = $width;
551    $this->height = $height;
552
553    $this->add_command('crop', $width.'x'.$height.'+'.$x.'+'.$y);
554    return true;
555  }
556
557  function strip()
558  {
559    $this->add_command('strip');
560    return true;
561  }
562
563  function rotate($rotation)
564  {
565    if (empty($rotation))
566    {
567      return true;
568    }
569
570    if ($rotation==90 || $rotation==270)
571    {
572      $tmp = $this->width;
573      $this->width = $this->height;
574      $this->height = $tmp;
575    }
576    $this->add_command('rotate', -$rotation);
577    $this->add_command('orient', 'top-left');
578    return true;
579  }
580
581  function set_compression_quality($quality)
582  {
583    $this->add_command('quality', $quality);
584    return true;
585  }
586
587  function resize($width, $height)
588  {
589    $this->width = $width;
590    $this->height = $height;
591
592    $this->add_command('filter', 'Lanczos');
593    $this->add_command('resize', $width.'x'.$height.'!');
594    return true;
595  }
596
597  function sharpen($amount)
598  {
599    $m = pwg_image::get_sharpen_matrix($amount);
600
601    $param ='convolve "'.count($m).':';
602    foreach ($m as $line)
603    {
604      $param .= ' ';
605      $param .= implode(',', $line);
606    }
607    $param .= '"';
608    $this->add_command('morphology', $param);
609    return true;
610  }
611
612  function compose($overlay, $x, $y, $opacity)
613  {
614    $param = 'compose dissolve -define compose:args='.$opacity;
615    $param .= ' '.escapeshellarg(realpath($overlay->image->source_filepath));
616    $param .= ' -gravity NorthWest -geometry +'.$x.'+'.$y;
617    $param .= ' -composite';
618    $this->add_command($param);
619    return true;
620  }
621
622  function write($destination_filepath)
623  {
624    $this->add_command('interlace', 'line'); // progressive rendering
625    // use 4:2:2 chroma subsampling (reduce file size by 20-30% with "almost" no human perception)
626    //
627    // option deactivated for Piwigo 2.4.1, it doesn't work fo old versions
628    // of ImageMagick, see bug:2672. To reactivate once we have a better way
629    // to detect IM version and when we know which version supports this
630    // option
631    //
632    if (version_compare(pwg_image::$ext_imagick_version, '6.6') > 0)
633    {
634      $this->add_command('sampling-factor', '4:2:2' );
635    }
636
637    $exec = $this->imagickdir.'convert';
638    $exec .= ' "'.realpath($this->source_filepath).'"';
639
640    foreach ($this->commands as $command => $params)
641    {
642      $exec .= ' -'.$command;
643      if (!empty($params))
644      {
645        $exec .= ' '.$params;
646      }
647    }
648
649    $dest = pathinfo($destination_filepath);
650    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'" 2>&1';
651    @exec($exec, $returnarray);
652
653    if (function_exists('ilog')) ilog($exec);
654    if (is_array($returnarray) && (count($returnarray)>0) )
655    {
656      if (function_exists('ilog')) ilog('ERROR', $returnarray);
657      foreach($returnarray as $line)
658        trigger_error($line, E_USER_WARNING);
659    }
660    return is_array($returnarray);
661  }
662}
663
664// +-----------------------------------------------------------------------+
665// |                       Class for GD library                            |
666// +-----------------------------------------------------------------------+
667
668class image_gd implements imageInterface
669{
670  var $image;
671  var $quality = 95;
672
673  function __construct($source_filepath)
674  {
675    $gd_info = gd_info();
676    $extension = strtolower(get_extension($source_filepath));
677
678    if (in_array($extension, array('jpg', 'jpeg')))
679    {
680      $this->image = imagecreatefromjpeg($source_filepath);
681    }
682    else if ($extension == 'png')
683    {
684      $this->image = imagecreatefrompng($source_filepath);
685    }
686    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
687    {
688      $this->image = imagecreatefromgif($source_filepath);
689    }
690    else
691    {
692      die('[Image GD] unsupported file extension');
693    }
694  }
695
696  function get_width()
697  {
698    return imagesx($this->image);
699  }
700
701  function get_height()
702  {
703    return imagesy($this->image);
704  }
705
706  function crop($width, $height, $x, $y)
707  {
708    $dest = imagecreatetruecolor($width, $height);
709
710    imagealphablending($dest, false);
711    imagesavealpha($dest, true);
712    if (function_exists('imageantialias'))
713    {
714      imageantialias($dest, true);
715    }
716
717    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
718
719    if ($result !== false)
720    {
721      imagedestroy($this->image);
722      $this->image = $dest;
723    }
724    else
725    {
726      imagedestroy($dest);
727    }
728    return $result;
729  }
730
731  function strip()
732  {
733    return true;
734  }
735
736  function rotate($rotation)
737  {
738    $dest = imagerotate($this->image, $rotation, 0);
739    imagedestroy($this->image);
740    $this->image = $dest;
741    return true;
742  }
743
744  function set_compression_quality($quality)
745  {
746    $this->quality = $quality;
747    return true;
748  }
749
750  function resize($width, $height)
751  {
752    $dest = imagecreatetruecolor($width, $height);
753
754    imagealphablending($dest, false);
755    imagesavealpha($dest, true);
756    if (function_exists('imageantialias'))
757    {
758      imageantialias($dest, true);
759    }
760
761    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
762
763    if ($result !== false)
764    {
765      imagedestroy($this->image);
766      $this->image = $dest;
767    }
768    else
769    {
770      imagedestroy($dest);
771    }
772    return $result;
773  }
774
775  function sharpen($amount)
776  {
777    $m = pwg_image::get_sharpen_matrix($amount);
778    return imageconvolution($this->image, $m, 1, 0);
779  }
780
781  function compose($overlay, $x, $y, $opacity)
782  {
783    $ioverlay = $overlay->image->image;
784    /* A replacement for php's imagecopymerge() function that supports the alpha channel
785    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
786
787    $ow = imagesx($ioverlay);
788    $oh = imagesy($ioverlay);
789
790                // Create a new blank image the site of our source image
791                $cut = imagecreatetruecolor($ow, $oh);
792
793                // Copy the blank image into the destination image where the source goes
794                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
795
796                // Place the source image in the destination image
797                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
798                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
799    imagedestroy($cut);
800    return true;
801  }
802
803  function write($destination_filepath)
804  {
805    $extension = strtolower(get_extension($destination_filepath));
806
807    if ($extension == 'png')
808    {
809      imagepng($this->image, $destination_filepath);
810    }
811    elseif ($extension == 'gif')
812    {
813      imagegif($this->image, $destination_filepath);
814    }
815    else
816    {
817      imagejpeg($this->image, $destination_filepath, $this->quality);
818    }
819  }
820
821  function destroy()
822  {
823    imagedestroy($this->image);
824  }
825}
826
827?>
Note: See TracBrowser for help on using the repository browser.