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

Last change on this file since 16147 was 16146, checked in by mistic100, 12 years ago

check if ilog() exists before using it, allowing the use the class outer i.php

File size: 21.0 KB
RevLine 
[11964]1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
[12922]5// | Copyright(C) 2008-2012 Piwigo Team                  http://piwigo.org |
[11964]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);
[12956]44
[12851]45  function sharpen($amount);
[12956]46
[12851]47  function compose($overlay, $x, $y, $opacity);
[11964]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();
[12956]99
[11964]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    }
[12749]109    $resize_dimensions = self::get_resize_dimensions($source_width, $source_height, $max_width, $max_height, $rotation, $crop, $follow_orientation);
[11964]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);
[12956]121
[11964]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    }
[12749]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    }
[12956]132
[11964]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
[12749]146  static function get_resize_dimensions($width, $height, $max_width, $max_height, $rotation=null, $crop=false, $follow_orientation=true)
[11964]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    }
[12749]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    }
[12956]185
[11964]186    $ratio_width  = $width / $max_width;
187    $ratio_height = $height / $max_height;
[12956]188    $destination_width = $width;
[11964]189    $destination_height = $height;
[12956]190
[11964]191    // maximal size exceeded ?
192    if ($ratio_width > 1 or $ratio_height > 1)
193    {
194      if ($ratio_width < $ratio_height)
[12956]195      {
[11964]196        $destination_width = round($width / $ratio_height);
197        $destination_height = $max_height;
198      }
199      else
[12956]200      {
201        $destination_width = $max_width;
[11964]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
[12749]211    $result = array(
[11964]212      'width' => $destination_width,
213      'height'=> $destination_height,
214      );
[12749]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;
[11964]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    }
[12956]235
[11964]236    if (!function_exists('exif_read_data'))
237    {
238      return null;
239    }
240
[13843]241    $rotation = 0;
[12956]242
[11964]243    $exif = exif_read_data($source_filepath);
[12956]244
[11964]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
[13843]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  {
[15551]278    switch($rotation_code%4)
[13843]279    {
280      case 0: return 0;
281      case 1: return 90;
282      case 2: return 180;
283      case 3: return 270;
284    }
285  }
286
[12851]287  /** Returns a normalized convolution kernel for sharpening*/
288  static function get_sharpen_matrix($amount)
289  {
[14649]290    // Amount should be in the range of 48-10
291    $amount = round(abs(-48 + ($amount * 0.38)), 2);
[12851]292
[13882]293    $matrix = array(
294      array(-1,   -1,    -1),
295      array(-1, $amount, -1),
296      array(-1,   -1,    -1),
297      );
[12956]298
[12851]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++)
[13882]305      {
[12851]306        $line[$j] /= $norm;
[13882]307      }
[12851]308    }
309
[13882]310    return $matrix;
[12851]311  }
312
[11964]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  {
[12581]328    return (extension_loaded('imagick') and class_exists('Imagick'));
[11964]329  }
330
331  static function is_ext_imagick()
332  {
333    global $conf;
334
335    if (!function_exists('exec'))
336    {
337      return false;
338    }
[12493]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]))
[11964]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);
[13843]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)
[13035]454    {
455      $this->image->scaleImage($this->get_width()/2, $this->get_height()/2);
456    }
[13843]457
[11964]458    return $this->image->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 0.9);
459  }
460
[12851]461  function sharpen($amount)
462  {
463    $m = pwg_image::get_sharpen_matrix($amount);
[13882]464    return  $this->image->convolveImage($m);
[12851]465  }
[12956]466
[12851]467  function compose($overlay, $x, $y, $opacity)
468  {
[12956]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);
[12851]484  }
485
[11964]486  function write($destination_filepath)
487  {
[13426]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) );
[11964]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
[12851]506  function __construct($source_filepath)
[11964]507  {
[12851]508    global $conf;
[11964]509    $this->source_filepath = $source_filepath;
[12851]510    $this->imagickdir = $conf['ext_imagick_dir'];
[11964]511
[15583]512    if (strpos(@$_SERVER['SCRIPT_FILENAME'], '/kunden/') === 0)  // 1and1
[15575]513    {
514      @putenv('MAGICK_THREAD_LIMIT=1');
515    }
516
[12851]517    $command = $this->imagickdir.'identify -format "%wx%h" "'.realpath($source_filepath).'"';
[12756]518    @exec($command, $returnarray);
519    if(!is_array($returnarray) or empty($returnarray[0]) or !preg_match('/^(\d+)x(\d+)$/', $returnarray[0], $match))
[11964]520    {
[12851]521      die("[External ImageMagick] Corrupt image\n" . var_export($returnarray, true));
[11964]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  {
[14649]557    if ($rotation==90 || $rotation==270)
558    {
559      $tmp = $this->width;
560      $this->width = $this->height;
561      $this->height = $tmp;
562    }
[11964]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
[12851]581  function sharpen($amount)
582  {
583    $m = pwg_image::get_sharpen_matrix($amount);
[12956]584
[12851]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  }
[12956]595
[12851]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
[11964]606  function write($destination_filepath)
607  {
[14649]608    $this->add_command('interlace', 'line'); // progressive rendering
[14723]609    // use 4:2:2 chroma subsampling (reduce file size by 20-30% with "almost" no human perception)
610    $this->add_command('sampling-factor', '4:2:2' );
[14649]611
[11964]612    $exec = $this->imagickdir.'convert';
613    $exec .= ' "'.realpath($this->source_filepath).'"';
614
615    foreach ($this->commands as $command => $params)
616    {
617      $exec .= ' -'.$command;
618      if (!empty($params))
619      {
620        $exec .= ' '.$params;
621      }
622    }
623
624    $dest = pathinfo($destination_filepath);
[16076]625    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'" 2>&1';
[12756]626    @exec($exec, $returnarray);
[12956]627
[16146]628    if (function_exists('ilog')) ilog($exec);
[14649]629    if (is_array($returnarray) && (count($returnarray)>0) )
630    {
[16146]631      if (function_exists('ilog')) ilog('ERROR', $returnarray);
[16076]632      foreach($returnarray as $line)
633        trigger_error($line, E_USER_WARNING);
[14649]634    }
[12756]635    return is_array($returnarray);
[11964]636  }
637}
638
639// +-----------------------------------------------------------------------+
640// |                       Class for GD library                            |
641// +-----------------------------------------------------------------------+
642
643class image_gd implements imageInterface
644{
645  var $image;
646  var $quality = 95;
647
648  function __construct($source_filepath)
649  {
650    $gd_info = gd_info();
651    $extension = strtolower(get_extension($source_filepath));
652
653    if (in_array($extension, array('jpg', 'jpeg')))
654    {
655      $this->image = imagecreatefromjpeg($source_filepath);
656    }
657    else if ($extension == 'png')
658    {
659      $this->image = imagecreatefrompng($source_filepath);
660    }
661    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
662    {
663      $this->image = imagecreatefromgif($source_filepath);
664    }
665    else
666    {
667      die('[Image GD] unsupported file extension');
668    }
669  }
670
671  function get_width()
672  {
673    return imagesx($this->image);
674  }
675
676  function get_height()
677  {
678    return imagesy($this->image);
679  }
680
681  function crop($width, $height, $x, $y)
682  {
683    $dest = imagecreatetruecolor($width, $height);
684
685    imagealphablending($dest, false);
686    imagesavealpha($dest, true);
687    if (function_exists('imageantialias'))
688    {
689      imageantialias($dest, true);
690    }
691
692    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
693
694    if ($result !== false)
695    {
696      imagedestroy($this->image);
697      $this->image = $dest;
698    }
699    else
700    {
701      imagedestroy($dest);
702    }
703    return $result;
704  }
705
706  function strip()
707  {
708    return true;
709  }
710
711  function rotate($rotation)
712  {
713    $dest = imagerotate($this->image, $rotation, 0);
714    imagedestroy($this->image);
715    $this->image = $dest;
716    return true;
717  }
718
719  function set_compression_quality($quality)
720  {
721    $this->quality = $quality;
722    return true;
723  }
724
725  function resize($width, $height)
726  {
727    $dest = imagecreatetruecolor($width, $height);
728
729    imagealphablending($dest, false);
730    imagesavealpha($dest, true);
731    if (function_exists('imageantialias'))
732    {
733      imageantialias($dest, true);
734    }
735
736    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
737
738    if ($result !== false)
739    {
740      imagedestroy($this->image);
741      $this->image = $dest;
742    }
743    else
744    {
745      imagedestroy($dest);
746    }
747    return $result;
748  }
749
[12851]750  function sharpen($amount)
751  {
752    $m = pwg_image::get_sharpen_matrix($amount);
[13882]753    return imageconvolution($this->image, $m, 1, 0);
[12851]754  }
[12956]755
[12851]756  function compose($overlay, $x, $y, $opacity)
757  {
758    $ioverlay = $overlay->image->image;
759    /* A replacement for php's imagecopymerge() function that supports the alpha channel
760    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
761
762    $ow = imagesx($ioverlay);
763    $oh = imagesy($ioverlay);
[12956]764
[12851]765                // Create a new blank image the site of our source image
766                $cut = imagecreatetruecolor($ow, $oh);
767
768                // Copy the blank image into the destination image where the source goes
769                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
770
771                // Place the source image in the destination image
772                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
773                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
[12865]774    imagedestroy($cut);
[12851]775    return true;
776  }
777
[11964]778  function write($destination_filepath)
779  {
780    $extension = strtolower(get_extension($destination_filepath));
781
782    if ($extension == 'png')
783    {
784      imagepng($this->image, $destination_filepath);
785    }
786    elseif ($extension == 'gif')
787    {
788      imagegif($this->image, $destination_filepath);
789    }
790    else
791    {
792      imagejpeg($this->image, $destination_filepath, $this->quality);
793    }
794  }
795
796  function destroy()
797  {
798    imagedestroy($this->image);
799  }
800}
801
[10641]802?>
Note: See TracBrowser for help on using the repository browser.