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