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

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

update Piwigo headers to 2012, last change before the expected (or not) apocalypse

File size: 18.9 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 18-10
269                $amount = round(abs(-18 + ($amount * 0.08)), 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    return $this->image->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 0.9);
427  }
428
429  function sharpen($amount)
430  {
431    $m = pwg_image::get_sharpen_matrix($amount);
432                return  $this->image->convolveImage($m);
433  }
434 
435  function compose($overlay, $x, $y, $opacity)
436  {
437    // todo
438    return false;
439  }
440
441  function write($destination_filepath)
442  {
443    return $this->image->writeImage($destination_filepath);
444  }
445}
446
447// +-----------------------------------------------------------------------+
448// |            Class for ImageMagick external installation                |
449// +-----------------------------------------------------------------------+
450
451class image_ext_imagick implements imageInterface
452{
453  var $imagickdir = '';
454  var $source_filepath = '';
455  var $width = '';
456  var $height = '';
457  var $commands = array();
458
459  function __construct($source_filepath)
460  {
461    global $conf;
462    $this->source_filepath = $source_filepath;
463    $this->imagickdir = $conf['ext_imagick_dir'];
464
465    $command = $this->imagickdir.'identify -format "%wx%h" "'.realpath($source_filepath).'"';
466    @exec($command, $returnarray);
467    if(!is_array($returnarray) or empty($returnarray[0]) or !preg_match('/^(\d+)x(\d+)$/', $returnarray[0], $match))
468    {
469      die("[External ImageMagick] Corrupt image\n" . var_export($returnarray, true));
470    }
471
472    $this->width = $match[1];
473    $this->height = $match[2];
474  }
475
476  function add_command($command, $params=null)
477  {
478    $this->commands[$command] = $params;
479  }
480
481  function get_width()
482  {
483    return $this->width;
484  }
485
486  function get_height()
487  {
488    return $this->height;
489  }
490
491  function crop($width, $height, $x, $y)
492  {
493    $this->add_command('crop', $width.'x'.$height.'+'.$x.'+'.$y);
494    return true;
495  }
496
497  function strip()
498  {
499    $this->add_command('strip');
500    return true;
501  }
502
503  function rotate($rotation)
504  {
505    $this->add_command('rotate', -$rotation);
506    $this->add_command('orient', 'top-left');
507    return true;
508  }
509
510  function set_compression_quality($quality)
511  {
512    $this->add_command('quality', $quality);
513    return true;
514  }
515
516  function resize($width, $height)
517  {
518    $this->add_command('interlace', 'line');
519    $this->add_command('filter', 'Lanczos');
520    $this->add_command('resize', $width.'x'.$height.'!');
521    return true;
522  }
523
524  function sharpen($amount)
525  {
526    $m = pwg_image::get_sharpen_matrix($amount);
527   
528    $param ='convolve "'.count($m).':';
529    foreach ($m as $line)
530    {
531      $param .= ' ';
532      $param .= implode(',', $line);
533    }
534    $param .= '"';
535    $this->add_command('morphology', $param);
536    return true;
537  }
538 
539  function compose($overlay, $x, $y, $opacity)
540  {
541    $param = 'compose dissolve -define compose:args='.$opacity;
542    $param .= ' '.escapeshellarg(realpath($overlay->image->source_filepath));
543    $param .= ' -gravity NorthWest -geometry +'.$x.'+'.$y;
544    $param .= ' -composite';
545    $this->add_command($param);
546    return true;
547  }
548
549  function write($destination_filepath)
550  {
551    $exec = $this->imagickdir.'convert';
552    $exec .= ' "'.realpath($this->source_filepath).'"';
553
554    foreach ($this->commands as $command => $params)
555    {
556      $exec .= ' -'.$command;
557      if (!empty($params))
558      {
559        $exec .= ' '.$params;
560      }
561    }
562
563    $dest = pathinfo($destination_filepath);
564    $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'"';
565    @exec($exec, $returnarray);
566   
567    //echo($exec);
568    return is_array($returnarray);
569  }
570}
571
572// +-----------------------------------------------------------------------+
573// |                       Class for GD library                            |
574// +-----------------------------------------------------------------------+
575
576class image_gd implements imageInterface
577{
578  var $image;
579  var $quality = 95;
580
581  function __construct($source_filepath)
582  {
583    $gd_info = gd_info();
584    $extension = strtolower(get_extension($source_filepath));
585
586    if (in_array($extension, array('jpg', 'jpeg')))
587    {
588      $this->image = imagecreatefromjpeg($source_filepath);
589    }
590    else if ($extension == 'png')
591    {
592      $this->image = imagecreatefrompng($source_filepath);
593    }
594    elseif ($extension == 'gif' and $gd_info['GIF Read Support'] and $gd_info['GIF Create Support'])
595    {
596      $this->image = imagecreatefromgif($source_filepath);
597    }
598    else
599    {
600      die('[Image GD] unsupported file extension');
601    }
602  }
603
604  function get_width()
605  {
606    return imagesx($this->image);
607  }
608
609  function get_height()
610  {
611    return imagesy($this->image);
612  }
613
614  function crop($width, $height, $x, $y)
615  {
616    $dest = imagecreatetruecolor($width, $height);
617
618    imagealphablending($dest, false);
619    imagesavealpha($dest, true);
620    if (function_exists('imageantialias'))
621    {
622      imageantialias($dest, true);
623    }
624
625    $result = imagecopymerge($dest, $this->image, 0, 0, $x, $y, $width, $height, 100);
626
627    if ($result !== false)
628    {
629      imagedestroy($this->image);
630      $this->image = $dest;
631    }
632    else
633    {
634      imagedestroy($dest);
635    }
636    return $result;
637  }
638
639  function strip()
640  {
641    return true;
642  }
643
644  function rotate($rotation)
645  {
646    $dest = imagerotate($this->image, $rotation, 0);
647    imagedestroy($this->image);
648    $this->image = $dest;
649    return true;
650  }
651
652  function set_compression_quality($quality)
653  {
654    $this->quality = $quality;
655    return true;
656  }
657
658  function resize($width, $height)
659  {
660    $dest = imagecreatetruecolor($width, $height);
661
662    imagealphablending($dest, false);
663    imagesavealpha($dest, true);
664    if (function_exists('imageantialias'))
665    {
666      imageantialias($dest, true);
667    }
668
669    $result = imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $width, $height, $this->get_width(), $this->get_height());
670
671    if ($result !== false)
672    {
673      imagedestroy($this->image);
674      $this->image = $dest;
675    }
676    else
677    {
678      imagedestroy($dest);
679    }
680    return $result;
681  }
682
683  function sharpen($amount)
684  {
685    $m = pwg_image::get_sharpen_matrix($amount);
686                return imageconvolution($this->image, $m, 1, 0);
687  }
688 
689  function compose($overlay, $x, $y, $opacity)
690  {
691    $ioverlay = $overlay->image->image;
692    /* A replacement for php's imagecopymerge() function that supports the alpha channel
693    See php bug #23815:  http://bugs.php.net/bug.php?id=23815 */
694
695    $ow = imagesx($ioverlay);
696    $oh = imagesy($ioverlay);
697     
698                // Create a new blank image the site of our source image
699                $cut = imagecreatetruecolor($ow, $oh);
700
701                // Copy the blank image into the destination image where the source goes
702                imagecopy($cut, $this->image, 0, 0, $x, $y, $ow, $oh);
703
704                // Place the source image in the destination image
705                imagecopy($cut, $ioverlay, 0, 0, 0, 0, $ow, $oh);
706                imagecopymerge($this->image, $cut, $x, $y, 0, 0, $ow, $oh, $opacity);
707    imagedestroy($cut);
708    return true;
709  }
710
711  function write($destination_filepath)
712  {
713    $extension = strtolower(get_extension($destination_filepath));
714
715    if ($extension == 'png')
716    {
717      imagepng($this->image, $destination_filepath);
718    }
719    elseif ($extension == 'gif')
720    {
721      imagegif($this->image, $destination_filepath);
722    }
723    else
724    {
725      imagejpeg($this->image, $destination_filepath, $this->quality);
726    }
727  }
728
729  function destroy()
730  {
731    imagedestroy($this->image);
732  }
733}
734
735?>
Note: See TracBrowser for help on using the repository browser.