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

Last change on this file since 31103 was 31103, checked in by mistic100, 9 years ago

feature 3221 Lazy log file open, clean code

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_notify('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    global $logger;
625
626    $this->add_command('interlace', 'line'); // progressive rendering
627    // use 4:2:2 chroma subsampling (reduce file size by 20-30% with "almost" no human perception)
628    //
629    // option deactivated for Piwigo 2.4.1, it doesn't work fo old versions
630    // of ImageMagick, see bug:2672. To reactivate once we have a better way
631    // to detect IM version and when we know which version supports this
632    // option
633    //
634    if (version_compare(pwg_image::$ext_imagick_version, '6.6') > 0)
635    {
636      $this->add_command('sampling-factor', '4:2:2' );
637    }
638
639    $exec = $this->imagickdir.'convert';
640    $exec .= ' "'.realpath($this->source_filepath).'"';
641
642    foreach ($this->commands as $command => $params)
643    {
644      $exec .= ' -'.$command;
645      if (!empty($params))
646      {
647        $exec .= ' '.$params;
648      }
649    }
650
651    $dest = pathinfo($destination_filepath);
652    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'" 2>&1';
653    $logger->debug($exec, 'i.php');
654    @exec($exec, $returnarray);
655
656    if (is_array($returnarray) && (count($returnarray)>0) )
657    {
658      $logger->error('', 'i.php', $returnarray);
659      foreach ($returnarray as $line)
660        trigger_error($line, E_USER_WARNING);
661    }
662    return is_array($returnarray);
663  }
664}
665
666// +-----------------------------------------------------------------------+
667// |                       Class for GD library                            |
668// +-----------------------------------------------------------------------+
669
670class image_gd implements imageInterface
671{
672  var $image;
673  var $quality = 95;
674
675  function __construct($source_filepath)
676  {
677    $gd_info = gd_info();
678    $extension = strtolower(get_extension($source_filepath));
679
680    if (in_array($extension, array('jpg', 'jpeg')))
681    {
682      $this->image = imagecreatefromjpeg($source_filepath);
683    }
684    else if ($extension == 'png')
685    {
686      $this->image = imagecreatefrompng($source_filepath);
687    }
688    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
689    {
690      $this->image = imagecreatefromgif($source_filepath);
691    }
692    else
693    {
694      die('[Image GD] unsupported file extension');
695    }
696  }
697
698  function get_width()
699  {
700    return imagesx($this->image);
701  }
702
703  function get_height()
704  {
705    return imagesy($this->image);
706  }
707
708  function crop($width, $height, $x, $y)
709  {
710    $dest = imagecreatetruecolor($width, $height);
711
712    imagealphablending($dest, false);
713    imagesavealpha($dest, true);
714    if (function_exists('imageantialias'))
715    {
716      imageantialias($dest, true);
717    }
718
719    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
720
721    if ($result !== false)
722    {
723      imagedestroy($this->image);
724      $this->image = $dest;
725    }
726    else
727    {
728      imagedestroy($dest);
729    }
730    return $result;
731  }
732
733  function strip()
734  {
735    return true;
736  }
737
738  function rotate($rotation)
739  {
740    $dest = imagerotate($this->image, $rotation, 0);
741    imagedestroy($this->image);
742    $this->image = $dest;
743    return true;
744  }
745
746  function set_compression_quality($quality)
747  {
748    $this->quality = $quality;
749    return true;
750  }
751
752  function resize($width, $height)
753  {
754    $dest = imagecreatetruecolor($width, $height);
755
756    imagealphablending($dest, false);
757    imagesavealpha($dest, true);
758    if (function_exists('imageantialias'))
759    {
760      imageantialias($dest, true);
761    }
762
763    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
764
765    if ($result !== false)
766    {
767      imagedestroy($this->image);
768      $this->image = $dest;
769    }
770    else
771    {
772      imagedestroy($dest);
773    }
774    return $result;
775  }
776
777  function sharpen($amount)
778  {
779    $m = pwg_image::get_sharpen_matrix($amount);
780    return imageconvolution($this->image, $m, 1, 0);
781  }
782
783  function compose($overlay, $x, $y, $opacity)
784  {
785    $ioverlay = $overlay->image->image;
786    /* A replacement for php's imagecopymerge() function that supports the alpha channel
787    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
788
789    $ow = imagesx($ioverlay);
790    $oh = imagesy($ioverlay);
791
792                // Create a new blank image the site of our source image
793                $cut = imagecreatetruecolor($ow, $oh);
794
795                // Copy the blank image into the destination image where the source goes
796                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
797
798                // Place the source image in the destination image
799                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
800                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
801    imagedestroy($cut);
802    return true;
803  }
804
805  function write($destination_filepath)
806  {
807    $extension = strtolower(get_extension($destination_filepath));
808
809    if ($extension == 'png')
810    {
811      imagepng($this->image, $destination_filepath);
812    }
813    elseif ($extension == 'gif')
814    {
815      imagegif($this->image, $destination_filepath);
816    }
817    else
818    {
819      imagejpeg($this->image, $destination_filepath, $this->quality);
820    }
821  }
822
823  function destroy()
824  {
825    imagedestroy($this->image);
826  }
827}
828
829?>
Note: See TracBrowser for help on using the repository browser.