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

Last change on this file since 19703 was 19703, checked in by plg, 11 years ago

update Piwigo headers to 2013 (the end of the world didn't occur as expected on r12922)

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