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

Revision 16211, 21.2 KB checked in by plg, 7 years ago (diff)

merge r16210 from branch 2.4 to trunk

bug 2672: deactivate sampling-factor (for now)

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