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

Last change on this file since 13115 was 13115, checked in by rvelices, 12 years ago

improvement of picture title on picture page, drop boxes on index page ...
sharpening uses a zider scale range

File size: 19.6 KB
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 = null;
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  /** Returns a normalized convolution kernel for sharpening*/
266  static function get_sharpen_matrix($amount)
267  {
268                // Amount should be in the range of 24-10
269                $amount = round(abs(-24 + ($amount * 0.14)), 2);
270
271                $matrix = array
272                (
273                        array(-1,   -1,    -1),
274                        array(-1, $amount, -1),
275                        array(-1,   -1,    -1),
276                );
277
278    $norm = array_sum(array_map('array_sum', $matrix));
279
280    for ($i=0; $i<3; $i++)
281    {
282      $line = & $matrix[$i];
283      for ($j=0; $j<3; $j++)
284        $line[$j] /= $norm;
285    }
286
287                return $matrix;
288  }
289
290  private function get_resize_result($destination_filepath, $width, $height, $time=null)
291  {
292    return array(
293      'source'      => $this->source_filepath,
294      'destination' => $destination_filepath,
295      'width'       => $width,
296      'height'      => $height,
297      'size'        => floor(filesize($destination_filepath) / 1024).' KB',
298      'time'        => $time ? number_format((get_moment() - $time) * 1000, 2, '.', ' ').' ms' : null,
299      'library'     => $this->library,
300    );
301  }
302
303  static function is_imagick()
304  {
305    return (extension_loaded('imagick') and class_exists('Imagick'));
306  }
307
308  static function is_ext_imagick()
309  {
310    global $conf;
311
312    if (!function_exists('exec'))
313    {
314      return false;
315    }
316    @exec($conf['ext_imagick_dir'].'convert -version', $returnarray);
317    if (is_array($returnarray) and !empty($returnarray[0]) and preg_match('/ImageMagick/i', $returnarray[0]))
318    {
319      return true;
320    }
321    return false;
322  }
323
324  static function is_gd()
325  {
326    return function_exists('gd_info');
327  }
328
329  static function get_library($library=null, $extension=null)
330  {
331    global $conf;
332
333    if (is_null($library))
334    {
335      $library = $conf['graphics_library'];
336    }
337
338    // Choose image library
339    switch (strtolower($library))
340    {
341      case 'auto':
342      case 'imagick':
343        if ($extension != 'gif' and self::is_imagick())
344        {
345          return 'imagick';
346        }
347      case 'ext_imagick':
348        if ($extension != 'gif' and self::is_ext_imagick())
349        {
350          return 'ext_imagick';
351        }
352      case 'gd':
353        if (self::is_gd())
354        {
355          return 'gd';
356        }
357      default:
358        if ($library != 'auto')
359        {
360          // Requested library not available. Try another library
361          return self::get_library('auto', $extension);
362        }
363    }
364    return false;
365  }
366
367  function destroy()
368  {
369    if (method_exists($this->image, 'destroy'))
370    {
371      return $this->image->destroy();
372    }
373    return true;
374  }
375}
376
377// +-----------------------------------------------------------------------+
378// |                   Class for Imagick extension                         |
379// +-----------------------------------------------------------------------+
380
381class image_imagick implements imageInterface
382{
383  var $image;
384
385  function __construct($source_filepath)
386  {
387    // A bug cause that Imagick class can not be extended
388    $this->image = new Imagick($source_filepath);
389  }
390
391  function get_width()
392  {
393    return $this->image->getImageWidth();
394  }
395
396  function get_height()
397  {
398    return $this->image->getImageHeight();
399  }
400
401  function set_compression_quality($quality)
402  {
403    return $this->image->setImageCompressionQuality($quality);
404  }
405
406  function crop($width, $height, $x, $y)
407  {
408    return $this->image->cropImage($width, $height, $x, $y);
409  }
410
411  function strip()
412  {
413    return $this->image->stripImage();
414  }
415
416  function rotate($rotation)
417  {
418    $this->image->rotateImage(new ImagickPixel(), -$rotation);
419    $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
420    return true;
421  }
422
423  function resize($width, $height)
424  {
425    $this->image->setInterlaceScheme(Imagick::INTERLACE_LINE);
426    if ($this->get_width()%2 == 0 && $this->get_height()%2 == 0
427      && $this->get_width() > 3*$width)
428    {
429      $this->image->scaleImage($this->get_width()/2, $this->get_height()/2);
430    }
431    return $this->image->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 0.9);
432  }
433
434  function sharpen($amount)
435  {
436    $m = pwg_image::get_sharpen_matrix($amount);
437                return  $this->image->convolveImage($m);
438  }
439
440  function compose($overlay, $x, $y, $opacity)
441  {
442    $ioverlay = $overlay->image->image;
443    /*if ($ioverlay->getImageAlphaChannel() !== Imagick::ALPHACHANNEL_OPAQUE)
444    {
445      // Force the image to have an alpha channel
446      $ioverlay->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE);
447    }*/
448
449    global $dirty_trick_xrepeat;
450    if ( !isset($dirty_trick_xrepeat) && $opacity < 100)
451    {// NOTE: Using setImageOpacity will destroy current alpha channels!
452      $ioverlay->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA);
453      $dirty_trick_xrepeat = true;
454    }
455
456    return $this->image->compositeImage($ioverlay, Imagick::COMPOSITE_DISSOLVE, $x, $y);
457  }
458
459  function write($destination_filepath)
460  {
461    return $this->image->writeImage($destination_filepath);
462  }
463}
464
465// +-----------------------------------------------------------------------+
466// |            Class for ImageMagick external installation                |
467// +-----------------------------------------------------------------------+
468
469class image_ext_imagick implements imageInterface
470{
471  var $imagickdir = '';
472  var $source_filepath = '';
473  var $width = '';
474  var $height = '';
475  var $commands = array();
476
477  function __construct($source_filepath)
478  {
479    global $conf;
480    $this->source_filepath = $source_filepath;
481    $this->imagickdir = $conf['ext_imagick_dir'];
482
483    $command = $this->imagickdir.'identify -format "%wx%h" "'.realpath($source_filepath).'"';
484    @exec($command, $returnarray);
485    if(!is_array($returnarray) or empty($returnarray[0]) or !preg_match('/^(\d+)x(\d+)$/', $returnarray[0], $match))
486    {
487      die("[External ImageMagick] Corrupt image\n" . var_export($returnarray, true));
488    }
489
490    $this->width = $match[1];
491    $this->height = $match[2];
492  }
493
494  function add_command($command, $params=null)
495  {
496    $this->commands[$command] = $params;
497  }
498
499  function get_width()
500  {
501    return $this->width;
502  }
503
504  function get_height()
505  {
506    return $this->height;
507  }
508
509  function crop($width, $height, $x, $y)
510  {
511    $this->add_command('crop', $width.'x'.$height.'+'.$x.'+'.$y);
512    return true;
513  }
514
515  function strip()
516  {
517    $this->add_command('strip');
518    return true;
519  }
520
521  function rotate($rotation)
522  {
523    $this->add_command('rotate', -$rotation);
524    $this->add_command('orient', 'top-left');
525    return true;
526  }
527
528  function set_compression_quality($quality)
529  {
530    $this->add_command('quality', $quality);
531    return true;
532  }
533
534  function resize($width, $height)
535  {
536    $this->add_command('interlace', 'line');
537    $this->add_command('filter', 'Lanczos');
538    $this->add_command('resize', $width.'x'.$height.'!');
539    return true;
540  }
541
542  function sharpen($amount)
543  {
544    $m = pwg_image::get_sharpen_matrix($amount);
545
546    $param ='convolve "'.count($m).':';
547    foreach ($m as $line)
548    {
549      $param .= ' ';
550      $param .= implode(',', $line);
551    }
552    $param .= '"';
553    $this->add_command('morphology', $param);
554    return true;
555  }
556
557  function compose($overlay, $x, $y, $opacity)
558  {
559    $param = 'compose dissolve -define compose:args='.$opacity;
560    $param .= ' '.escapeshellarg(realpath($overlay->image->source_filepath));
561    $param .= ' -gravity NorthWest -geometry +'.$x.'+'.$y;
562    $param .= ' -composite';
563    $this->add_command($param);
564    return true;
565  }
566
567  function write($destination_filepath)
568  {
569    $exec = $this->imagickdir.'convert';
570    $exec .= ' "'.realpath($this->source_filepath).'"';
571
572    foreach ($this->commands as $command => $params)
573    {
574      $exec .= ' -'.$command;
575      if (!empty($params))
576      {
577        $exec .= ' '.$params;
578      }
579    }
580
581    $dest = pathinfo($destination_filepath);
582    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'"';
583    @exec($exec, $returnarray);
584
585    //echo($exec);
586    return is_array($returnarray);
587  }
588}
589
590// +-----------------------------------------------------------------------+
591// |                       Class for GD library                            |
592// +-----------------------------------------------------------------------+
593
594class image_gd implements imageInterface
595{
596  var $image;
597  var $quality = 95;
598
599  function __construct($source_filepath)
600  {
601    $gd_info = gd_info();
602    $extension = strtolower(get_extension($source_filepath));
603
604    if (in_array($extension, array('jpg', 'jpeg')))
605    {
606      $this->image = imagecreatefromjpeg($source_filepath);
607    }
608    else if ($extension == 'png')
609    {
610      $this->image = imagecreatefrompng($source_filepath);
611    }
612    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
613    {
614      $this->image = imagecreatefromgif($source_filepath);
615    }
616    else
617    {
618      die('[Image GD] unsupported file extension');
619    }
620  }
621
622  function get_width()
623  {
624    return imagesx($this->image);
625  }
626
627  function get_height()
628  {
629    return imagesy($this->image);
630  }
631
632  function crop($width, $height, $x, $y)
633  {
634    $dest = imagecreatetruecolor($width, $height);
635
636    imagealphablending($dest, false);
637    imagesavealpha($dest, true);
638    if (function_exists('imageantialias'))
639    {
640      imageantialias($dest, true);
641    }
642
643    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
644
645    if ($result !== false)
646    {
647      imagedestroy($this->image);
648      $this->image = $dest;
649    }
650    else
651    {
652      imagedestroy($dest);
653    }
654    return $result;
655  }
656
657  function strip()
658  {
659    return true;
660  }
661
662  function rotate($rotation)
663  {
664    $dest = imagerotate($this->image, $rotation, 0);
665    imagedestroy($this->image);
666    $this->image = $dest;
667    return true;
668  }
669
670  function set_compression_quality($quality)
671  {
672    $this->quality = $quality;
673    return true;
674  }
675
676  function resize($width, $height)
677  {
678    $dest = imagecreatetruecolor($width, $height);
679
680    imagealphablending($dest, false);
681    imagesavealpha($dest, true);
682    if (function_exists('imageantialias'))
683    {
684      imageantialias($dest, true);
685    }
686
687    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
688
689    if ($result !== false)
690    {
691      imagedestroy($this->image);
692      $this->image = $dest;
693    }
694    else
695    {
696      imagedestroy($dest);
697    }
698    return $result;
699  }
700
701  function sharpen($amount)
702  {
703    $m = pwg_image::get_sharpen_matrix($amount);
704                return imageconvolution($this->image, $m, 1, 0);
705  }
706
707  function compose($overlay, $x, $y, $opacity)
708  {
709    $ioverlay = $overlay->image->image;
710    /* A replacement for php's imagecopymerge() function that supports the alpha channel
711    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
712
713    $ow = imagesx($ioverlay);
714    $oh = imagesy($ioverlay);
715
716                // Create a new blank image the site of our source image
717                $cut = imagecreatetruecolor($ow, $oh);
718
719                // Copy the blank image into the destination image where the source goes
720                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
721
722                // Place the source image in the destination image
723                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
724                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
725    imagedestroy($cut);
726    return true;
727  }
728
729  function write($destination_filepath)
730  {
731    $extension = strtolower(get_extension($destination_filepath));
732
733    if ($extension == 'png')
734    {
735      imagepng($this->image, $destination_filepath);
736    }
737    elseif ($extension == 'gif')
738    {
739      imagegif($this->image, $destination_filepath);
740    }
741    else
742    {
743      imagejpeg($this->image, $destination_filepath, $this->quality);
744    }
745  }
746
747  function destroy()
748  {
749    imagedestroy($this->image);
750  }
751}
752
753?>
Note: See TracBrowser for help on using the repository browser.