2022-03-08 15:55:41 +01:00
< ? php namespace ProcessWire ;
/**
* ProcessWire ImageSizerGD
*
* Code for IPTC , auto rotation and sharpening by Horst Nogajski .
* http :// nogajski . de /
*
* Other user contributions as noted .
*
* Copyright ( C ) 2016 - 2019 by Horst Nogajski and Ryan Cramer
* This file licensed under Mozilla Public License v2 . 0 http :// mozilla . org / MPL / 2.0 /
*
* https :// processwire . com
*
* @ method bool imSaveReady ( $im , $filename )
*
*/
class ImageSizerEngineGD extends ImageSizerEngine {
public static function getModuleInfo () {
return array (
'title' => 'GD Image Sizer' ,
'version' => 1 ,
'summary' => " Uses PHP’ s built-in GD library to resize images. " ,
'author' => 'Horst Nogajski' ,
);
}
/**
* @ var string
*
*/
protected $imageFormat ;
/**
* @ var int ?
*
*/
protected $imageDepth ;
/**
* @ var bool
*
*/
protected $gammaLinearized ;
/**
* Webp support available ?
*
* @ var bool | null
*
*/
static protected $webpSupport = null ;
/**
* Get formats GD and resize
*
* @ return array
*
*/
protected function validSourceImageFormats () {
return array ( 'JPG' , 'JPEG' , 'PNG' , 'GIF' );
}
/**
* Get an array of image file extensions this ImageSizerModule can create
*
* @ return array of uppercase file extensions , i . e . [ 'PNG' , 'JPG' ]
*
*/
protected function validTargetImageFormats () {
$formats = $this -> validSourceImageFormats ();
if ( $this -> supported ( 'webp' )) $formats [] = 'WEBP' ;
return $formats ;
}
/**
* Get library version string
*
* @ return string Returns version string or blank string if not applicable / available
* @ since 3.0 . 138
*
*/
public function getLibraryVersion () {
$gd = gd_info ();
return isset ( $gd [ 'GD Version' ]) ? $gd [ 'GD Version' ] : '' ;
}
/**
* Return whether or not GD can proceed - Is the current image ( sub ) format supported ?
*
* @ param string $action
* @ return bool
*
*/
public function supported ( $action = 'imageformat' ) {
// first we check parts that are mandatory for all $actions
if ( ! function_exists ( 'gd_info' )) return false ;
// and if it passes the mandatory requirements, we check particularly aspects here
switch ( $action ) {
case 'imageformat' :
// compare current imagefile infos fetched from ImageInspector
$requested = $this -> getImageInfo ( false );
switch ( $requested ) {
case 'gif-anim' :
case 'gif-trans-anim' :
// Animated GIF images are not supported, but GD renders the first image of the animation
#return false;
default :
return true ;
}
break ;
case 'webp' :
if ( self :: $webpSupport === null ) {
// only call it once
$gd = gd_info ();
self :: $webpSupport = isset ( $gd [ 'WebP Support' ]) ? $gd [ 'WebP Support' ] : false ;
}
return self :: $webpSupport ;
break ;
case 'install' :
/*
$gd = gd_info ();
$jpg = isset ( $gd [ 'JPEG Support' ]) ? $gd [ 'JPEG Support' ] : false ;
$png = isset ( $gd [ 'PNG Support' ]) ? $gd [ 'PNG Support' ] : false ;
$gif = isset ( $gd [ 'GIF Read Support' ]) && isset ( $gd [ 'GIF Create Support' ]) ? $gd [ 'GIF Create Support' ] : false ;
$freetype = isset ( $gd [ 'FreeType Support' ]) ? $gd [ 'FreeType Support' ] : false ;
$webp = isset ( $gd [ 'WebP Support' ]) ? $gd [ 'WebP Support' ] : false ;
$this -> config -> gdReady = true ;
*/
return true ;
default :
return false ;
}
}
/**
* Process the image resize
*
* @ param string $srcFilename Source file
* @ param string $dstFilename Destination file
* @ param int $fullWidth Current width
* @ param int $fullHeight Current height
* @ param int $finalWidth Requested final width
* @ param int $finalHeight Requested final height
* @ return bool
* @ throws WireException
*
*/
protected function processResize ( $srcFilename , $dstFilename , $fullWidth , $fullHeight , $finalWidth , $finalHeight ) {
$this -> modified = false ;
$isModified = false ;
if ( isset ( $this -> info [ 'bits' ])) $this -> imageDepth = $this -> info [ 'bits' ];
$this -> imageFormat = strtoupper ( str_replace ( 'image/' , '' , $this -> info [ 'mime' ]));
if ( ! in_array ( $this -> imageFormat , $this -> validSourceImageFormats ())) {
throw new WireException ( sprintf ( $this -> _ ( " loaded file '%s' is not in the list of valid images " ), basename ( $dstFilename )));
}
$image = null ;
$orientations = null ; // @horst
$needRotation = $this -> autoRotation !== true ? false : ( $this -> checkOrientation ( $orientations ) &&
( ! empty ( $orientations [ 0 ]) || ! empty ( $orientations [ 1 ])) ? true : false );
// check if we can load the sourceimage into ram
if ( self :: checkMemoryForImage ( array ( $this -> info [ 'width' ], $this -> info [ 'height' ], $this -> info [ 'channels' ])) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to load " );
}
switch ( $this -> imageType ) { // @teppo
case \IMAGETYPE_GIF :
$image = @ imagecreatefromgif ( $srcFilename );
break ;
case \IMAGETYPE_PNG :
$image = @ imagecreatefrompng ( $srcFilename );
break ;
case \IMAGETYPE_JPEG :
$image = @ imagecreatefromjpeg ( $srcFilename );
break ;
}
if ( ! $image ) return false ;
if ( $this -> imageType != \IMAGETYPE_PNG || ! $this -> hasAlphaChannel ()) {
// @horst: linearize gamma to 1.0 - we do not use gamma correction with pngs containing alphachannel, because GD-lib doesn't respect transparency here (is buggy)
$this -> gammaCorrection ( $image , true );
}
if ( $this -> rotate || $needRotation ) { // @horst
$degrees = $this -> rotate ? $this -> rotate : $orientations [ 0 ];
$image = $this -> imRotate ( $image , $degrees );
$isModified = true ;
if ( abs ( $degrees ) == 90 || abs ( $degrees ) == 270 ) {
// we have to swap width & height now!
$tmp = array ( $this -> getWidth (), $this -> getHeight ());
$this -> setImageInfo ( $tmp [ 1 ], $tmp [ 0 ]);
}
}
if ( $this -> flip || $needRotation ) {
$vertical = null ;
if ( $this -> flip ) {
$vertical = $this -> flip == 'v' ;
} else if ( $orientations [ 1 ] > 0 ) {
$vertical = $orientations [ 1 ] == 2 ;
}
if ( ! is_null ( $vertical )) {
$image = $this -> imFlip ( $image , $vertical );
$isModified = true ;
}
}
$zoom = $this -> getFocusZoomPercent ();
if ( $zoom > 1 ) {
// we need to configure a cropExtra call to respect the zoom factor
$this -> cropExtra = $this -> getFocusZoomCropDimensions ( $zoom , $fullWidth , $fullHeight , $finalWidth , $finalHeight );
$this -> cropping = false ;
}
// if there is requested to crop _before_ resize, we do it here @horst
if ( is_array ( $this -> cropExtra )) {
// check if we can load a second copy from sourceimage into ram
if ( self :: checkMemoryForImage ( array ( $this -> info [ 'width' ], $this -> info [ 'height' ], 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to load a copy for cropExtra " );
}
$imageTemp = imagecreatetruecolor ( imagesx ( $image ), imagesy ( $image )); // create an intermediate memory image
$this -> prepareImageLayer ( $imageTemp , $image );
imagecopy ( $imageTemp , $image , 0 , 0 , 0 , 0 , imagesx ( $image ), imagesy ( $image )); // copy our initial image into the intermediate one
imagedestroy ( $image ); // release the initial image
// get crop values and create a new initial image
list ( $x , $y , $w , $h ) = $this -> cropExtra ;
// check if we can load a cropped version into ram
if ( self :: checkMemoryForImage ( array ( $w , $h , 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to load a cropped version for cropExtra " );
}
$image = imagecreatetruecolor ( $w , $h );
$this -> prepareImageLayer ( $image , $imageTemp );
imagecopy ( $image , $imageTemp , 0 , 0 , $x , $y , $w , $h );
unset ( $x , $y , $w , $h );
$isModified = true ;
// now release the intermediate image and update settings
imagedestroy ( $imageTemp );
$imageTemp = null ;
$this -> setImageInfo ( imagesx ( $image ), imagesy ( $image ));
// $this->cropping = false; // ?? set this to prevent overhead with the following manipulation ??
}
// here we check for cropping, upscaling, sharpening
// we get all dimensions at first, before any image operation !
$bgX = $bgY = 0 ;
$bgWidth = $fullWidth ;
$bgHeight = $fullHeight ;
$resizeMethod = $this -> getResizeMethod ( $bgWidth , $bgHeight , $finalWidth , $finalHeight , $bgX , $bgY );
$thumb = null ;
// now lets check what operations are necessary:
if ( 0 == $resizeMethod ) {
// this is the case if the original size is requested or a greater size but upscaling is set to false
// current version is already the desired result, we only may have to compress JPEGs but leave GIF and PNG as is:
if ( ! $isModified && ! $this -> webpOnly && ! $this -> webpAdd && ( $this -> imageType == \IMAGETYPE_PNG || $this -> imageType == \IMAGETYPE_GIF )) {
$result = @ copy ( $srcFilename , $dstFilename );
if ( isset ( $image ) && is_resource ( $image )) @ imagedestroy ( $image ); // clean up
if ( isset ( $image )) $image = null ;
return $result ; // early return !
}
// process JPEGs
if ( self :: checkMemoryForImage ( array ( imagesx ( $image ), imagesy ( $image ), 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to copy the final image " );
}
$this -> sharpening = 'none' ; // we set sharpening to none, as the image only gets compressed, but not resized
$thumb = imagecreatetruecolor ( imagesx ( $image ), imagesy ( $image )); // create the final memory image
$this -> prepareImageLayer ( $thumb , $image );
imagecopy ( $thumb , $image , 0 , 0 , 0 , 0 , imagesx ( $image ), imagesy ( $image )); // copy our intermediate image into the final one
} else if ( 2 == $resizeMethod ) { // 2 = resize with aspect ratio
// this is the case if we scale up or down _without_ cropping
if ( self :: checkMemoryForImage ( array ( $finalWidth , $finalHeight , 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to resize to the final image " );
}
$thumb = imagecreatetruecolor ( $finalWidth , $finalHeight );
$this -> prepareImageLayer ( $thumb , $image );
imagecopyresampled ( $thumb , $image , 0 , 0 , 0 , 0 , $finalWidth , $finalHeight , $this -> image [ 'width' ], $this -> image [ 'height' ]);
} else if ( 4 == $resizeMethod ) { // 4 = resize and crop with aspect ratio, - or crop without resizing ($upscaling == false)
// we have to scale up or down and to _crop_
if ( self :: checkMemoryForImage ( array ( $bgWidth , $bgHeight , 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to resize to the intermediate image " );
}
$sourceX = 0 ;
$sourceY = 0 ;
$sourceWidth = $this -> image [ 'width' ];
$sourceHeight = $this -> image [ 'height' ];
$thumb2 = imagecreatetruecolor ( $bgWidth , $bgHeight );
$this -> prepareImageLayer ( $thumb2 , $image );
imagecopyresampled (
$thumb2 , // destination image
$image , // source image
0 , // destination X
0 , // destination Y
$sourceX , // source X
$sourceY , // source Y
$bgWidth , // destination width
$bgHeight , // destination height
$sourceWidth , // source width
$sourceHeight // source height
);
if ( self :: checkMemoryForImage ( array ( $finalWidth , $finalHeight , 3 )) === false ) {
throw new WireException ( basename ( $srcFilename ) . " - not enough memory to crop to the final image " );
}
$thumb = imagecreatetruecolor ( $finalWidth , $finalHeight );
$this -> prepareImageLayer ( $thumb , $image );
imagecopyresampled (
$thumb , // destination image
$thumb2 , // source image
0 , // destination X
0 , // destination Y
$bgX , // source X
$bgY , // source Y
$finalWidth , // destination width
$finalHeight , // destination height
$finalWidth , // source width
$finalHeight // source height
);
imagedestroy ( $thumb2 );
}
// early release of obsolete GD image object(s) to free memory before processing sharpening
if ( isset ( $image ) && is_resource ( $image )) @ imagedestroy ( $image ); // @horst
if ( isset ( $thumb2 ) && is_resource ( $thumb2 )) @ imagedestroy ( $thumb2 );
if ( isset ( $image )) $image = null ;
if ( isset ( $thumb2 )) $thumb2 = null ;
// optionally apply sharpening to the final thumb
if ( $this -> sharpening && $this -> sharpening != 'none' ) { // @horst
if ( \IMAGETYPE_PNG != $this -> imageType || ! $this -> hasAlphaChannel ()) {
$w = imagesx ( $thumb );
$h = imagesy ( $thumb );
if ( $this -> useUSM ) {
// calculate if there is enough memory available to apply the USM algorithm, if enabled
if ( true === ( $this -> useUSM = self :: checkMemoryForImage ( array ( $w , $h , 3 ), array ( $w , $h , 3 )))) {
// is needed for the USM sharpening function to calculate the best sharpening params
$this -> usmValue = $this -> calculateUSMfactor ( $finalWidth , $finalHeight );
$thumb = $this -> imSharpen ( $thumb , $this -> sharpening );
}
}
if ( ! $this -> useUSM ) {
if ( false !== self :: checkMemoryForImage ( array ( $w , $h , 3 ))) {
$thumb = $this -> imSharpen ( $thumb , $this -> sharpening );
}
}
}
}
// write to file(s)
if ( file_exists ( $dstFilename )) $this -> wire ( 'files' ) -> unlink ( $dstFilename );
$result = null ; // null=not yet known
switch ( $this -> imageType ) {
case \IMAGETYPE_GIF :
// correct gamma from linearized 1.0 back to 2.0
$this -> gammaCorrection ( $thumb , false );
// save the final GIF image file
if ( $this -> imSaveReady ( $thumb , $srcFilename )) $result = imagegif ( $thumb , $dstFilename );
break ;
case \IMAGETYPE_PNG :
// optionally correct gamma from linearized 1.0 back to 2.0
if ( ! $this -> hasAlphaChannel ()) $this -> gammaCorrection ( $thumb , false );
// save the final PNG image file and always use highest compression level (9) per @horst
if ( $this -> imSaveReady ( $thumb , $srcFilename )) $result = imagepng ( $thumb , $dstFilename , 9 );
break ;
case \IMAGETYPE_JPEG :
// correct gamma from linearized 1.0 back to 2.0
$this -> gammaCorrection ( $thumb , false );
if ( $this -> imSaveReady ( $thumb , $srcFilename )) {
// optionally apply interlace bit to the final image. this will result in progressive JPEGs
if ( $this -> interlace ) {
if ( 0 == imageinterlace ( $thumb , 1 )) {
// log that setting the interlace bit has failed ?
// ...
}
}
// save the final JPEG image file
$result = imagejpeg ( $thumb , $dstFilename , $this -> quality );
}
break ;
default :
$result = false ;
}
// release the last GD image object
if ( isset ( $thumb ) && is_resource ( $thumb )) @ imagedestroy ( $thumb );
if ( isset ( $thumb )) $thumb = null ;
if ( $result === null ) $result = $this -> webpResult ; // if webpOnly option used
return $result ;
}
/**
* Called before saving of image , returns true if save should proceed , false if not
*
* Also Creates a webp file when settings indicate it should .
*
* @ param resource $im
* @ param string $filename Source filename
* @ return bool
*
*/
protected function ___imSaveReady ( $im , $filename ) {
if ( $this -> webpOnly || $this -> webpAdd ) {
$this -> webpResult = $this -> imSaveWebP ( $im , $filename , $this -> webpQuality );
}
return $this -> webpOnly ? false : true ;
}
/**
* Create WebP image ( @ horst )
* Is requested by image options : [ " webpAdd " => true ] OR [ " webpOnly " => true ]
*
* @ param resource $im
* @ param string $filename
* @ param int $quality
*
* @ return boolean true | false
*
*/
protected function imSaveWebP ( $im , $filename , $quality = 90 ) {
if ( ! function_exists ( 'imagewebp' )) return false ;
$path_parts = pathinfo ( $filename );
$webpFilename = $path_parts [ 'dirname' ] . '/' . $path_parts [ 'filename' ] . '.webp' ;
if ( file_exists ( $webpFilename )) $this -> wire ( 'files' ) -> unlink ( $webpFilename );
return imagewebp ( $im , $webpFilename , $quality );
}
/**
* Rotate image ( @ horst )
*
* @ param resource $im
* @ param int $degree
*
* @ return resource
*
*/
protected function imRotate ( $im , $degree ) {
$degree = ( is_float ( $degree ) || is_int ( $degree )) && $degree > - 361 && $degree < 361 ? $degree : false ;
if ( $degree === false ) return $im ;
if ( in_array ( $degree , array ( - 360 , 0 , 360 ))) return $im ;
$angle = 360 - $degree ; // because imagerotate() expects counterclockwise angle rather than degrees
return @ imagerotate ( $im , $angle , imagecolorallocate ( $im , 0 , 0 , 0 ));
}
/**
* Flip image ( @ horst )
*
* @ param resource $im
* @ param bool $vertical ( default = false )
*
* @ return resource
*
*/
protected function imFlip ( $im , $vertical = false ) {
$sx = imagesx ( $im );
$sy = imagesy ( $im );
$im2 = @ imagecreatetruecolor ( $sx , $sy );
if ( $vertical === true ) {
@ imagecopyresampled ( $im2 , $im , 0 , 0 , 0 , ( $sy - 1 ), $sx , $sy , $sx , 0 - $sy );
} else {
@ imagecopyresampled ( $im2 , $im , 0 , 0 , ( $sx - 1 ), 0 , $sx , $sy , 0 - $sx , $sy );
}
return $im2 ;
}
/**
* Sharpen image ( @ horst )
*
* @ param resource $im
* @ param string $mode May be : none | soft | medium | strong
*
* @ return resource | bool
*
*/
protected function imSharpen ( $im , $mode ) {
// due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
// we have to bypass this for those who have to run on this PHP versions
// see: https://bugs.php.net/bug.php?id=66714
// and here under GD: http://php.net/ChangeLog-5.php#5.5.11
$buggyPHP = ( version_compare ( phpversion (), '5.5.8' , '>' ) && version_compare ( phpversion (), '5.5.11' , '<' )) ? true : false ;
if ( $buggyPHP && ! $this -> useUSM
&& self :: checkMemoryForImage ( array ( imagesx ( $im ), imagesy ( $im ), 3 ), array ( imagesx ( $im ), imagesy ( $im ), 3 )) !== true
) {
// we have not enough memory available for USM and cannot use the other algorithm because of the buggy PHP version
return $im ;
}
// USM method is used for buggy PHP versions
// for regular versions it can be omitted per: useUSM = false passes as pageimage option
// or set in the site/config.php under $config->imageSizerOptions: 'useUSM' => false | true
if ( $buggyPHP || $this -> useUSM ) {
switch ( $mode ) {
case 'none' :
return $im ;
break ;
case 'strong' :
$amount = 160 ;
$radius = 1.0 ;
$threshold = 7 ;
break ;
case 'medium' :
$amount = 130 ;
$radius = 0.75 ;
$threshold = 7 ;
break ;
case 'soft' :
default :
$amount = 100 ;
$radius = 0.5 ;
$threshold = 7 ;
}
// calculate the final amount according to the usmValue
$this -> usmValue = $this -> usmValue < 0 ? 0 : ( $this -> usmValue > 100 ? 100 : $this -> usmValue );
if ( 0 == $this -> usmValue ) return $im ;
$amount = intval ( $amount / 100 * $this -> usmValue );
// apply unsharp mask filter
return $this -> unsharpMask ( $im , $amount , $radius , $threshold );
}
// if we do not use USM, we use our default sharpening method,
// entirely based on GDs imageconvolution
switch ( $mode ) {
case 'none' :
return $im ;
break ;
case 'strong' :
$sharpenMatrix = array (
array ( - 1.2 , - 1 , - 1.2 ),
array ( - 1 , 16 , - 1 ),
array ( - 1.2 , - 1 , - 1.2 )
);
break ;
case 'medium' :
$sharpenMatrix = array (
array ( - 1.1 , - 1 , - 1.1 ),
array ( - 1 , 20 , - 1 ),
array ( - 1.1 , - 1 , - 1.1 )
);
break ;
case 'soft' :
default :
$sharpenMatrix = array (
array ( - 1 , - 1 , - 1 ),
array ( - 1 , 24 , - 1 ),
array ( - 1 , - 1 , - 1 )
);
}
// calculate the sharpen divisor
$divisor = array_sum ( array_map ( 'array_sum' , $sharpenMatrix ));
$offset = 0 ;
// TODO 4 -c errorhandling: Throw WireException?
if ( ! imageconvolution ( $im , $sharpenMatrix , $divisor , $offset )) return false ;
return $im ;
}
/**
* apply GammaCorrection to an image ( @ horst )
*
* with mode = true it linearizes an image to 1
* with mode = false it set it back to the originating gamma value
*
* @ param resource $image
* @ param bool $mode
*
*/
protected function gammaCorrection ( & $image , $mode ) {
if ( - 1 == $this -> defaultGamma || ! is_bool ( $mode )) return ;
if ( $mode ) {
// linearizes to 1.0
if ( imagegammacorrect ( $image , $this -> defaultGamma , 1.0 )) $this -> gammaLinearized = true ;
} else {
if ( ! isset ( $this -> gammaLinearized ) || ! $this -> gammaLinearized ) return ;
// switch back to original Gamma
if ( imagegammacorrect ( $image , 1.0 , $this -> defaultGamma )) unset ( $this -> gammaLinearized );
}
}
/**
* Unsharp Mask for PHP - version 2.1 . 1
*
* Unsharp mask algorithm by Torstein Hønsi 2003 - 07.
* thoensi_at_netcom_dot_no .
* Please leave this notice .
*
* http :// vikjavev . no / computing / ump . php
*
* @ param resource $img
* @ param int $amount
* @ param int $radius
* @ param int $threshold
* @ return resource
*
*/
protected function unsharpMask ( $img , $amount , $radius , $threshold ) {
// Attempt to calibrate the parameters to Photoshop:
if ( $amount > 500 ) $amount = 500 ;
$amount = $amount * 0.016 ;
if ( $radius > 50 ) $radius = 50 ;
$radius = $radius * 2 ;
if ( $threshold > 255 ) $threshold = 255 ;
$radius = abs ( round ( $radius )); // Only integers make sense.
if ( $radius == 0 ) {
return $img ;
}
$w = imagesx ( $img );
$h = imagesy ( $img );
$imgCanvas = imagecreatetruecolor ( $w , $h );
$imgBlur = imagecreatetruecolor ( $w , $h );
// due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
// we have to bypass this for those who have to run on this PHP versions
// see: https://bugs.php.net/bug.php?id=66714
// and here under GD: http://php.net/ChangeLog-5.php#5.5.11
$buggyPHP = ( version_compare ( phpversion (), '5.5.8' , '>' ) && version_compare ( phpversion (), '5.5.11' , '<' )) ? true : false ;
// Gaussian blur matrix:
//
// 1 2 1
// 2 4 2
// 1 2 1
//
//////////////////////////////////////////////////
if ( function_exists ( 'imageconvolution' ) && ! $buggyPHP ) {
$matrix = array (
array ( 1 , 2 , 1 ),
array ( 2 , 4 , 2 ),
array ( 1 , 2 , 1 )
);
imagecopy ( $imgBlur , $img , 0 , 0 , 0 , 0 , $w , $h );
imageconvolution ( $imgBlur , $matrix , 16 , 0 );
} else {
// Move copies of the image around one pixel at the time and merge them with weight
// according to the matrix. The same matrix is simply repeated for higher radii.
for ( $i = 0 ; $i < $radius ; $i ++ ) {
imagecopy ( $imgBlur , $img , 0 , 0 , 1 , 0 , $w - 1 , $h ); // left
imagecopymerge ( $imgBlur , $img , 1 , 0 , 0 , 0 , $w , $h , 50 ); // right
imagecopymerge ( $imgBlur , $img , 0 , 0 , 0 , 0 , $w , $h , 50 ); // center
imagecopy ( $imgCanvas , $imgBlur , 0 , 0 , 0 , 0 , $w , $h );
imagecopymerge ( $imgBlur , $imgCanvas , 0 , 0 , 0 , 1 , $w , $h - 1 , 33.33333 ); // up
imagecopymerge ( $imgBlur , $imgCanvas , 0 , 1 , 0 , 0 , $w , $h , 25 ); // down
}
}
if ( $threshold > 0 ) {
// Calculate the difference between the blurred pixels and the original
// and set the pixels
for ( $x = 0 ; $x < $w - 1 ; $x ++ ) { // each row
for ( $y = 0 ; $y < $h ; $y ++ ) { // each pixel
$rgbOrig = imagecolorat ( $img , $x , $y );
$rOrig = (( $rgbOrig >> 16 ) & 0xFF );
$gOrig = (( $rgbOrig >> 8 ) & 0xFF );
$bOrig = ( $rgbOrig & 0xFF );
$rgbBlur = imagecolorat ( $imgBlur , $x , $y );
$rBlur = (( $rgbBlur >> 16 ) & 0xFF );
$gBlur = (( $rgbBlur >> 8 ) & 0xFF );
$bBlur = ( $rgbBlur & 0xFF );
// When the masked pixels differ less from the original
// than the threshold specifies, they are set to their original value.
$rNew = ( abs ( $rOrig - $rBlur ) >= $threshold )
? max ( 0 , min ( 255 , ( $amount * ( $rOrig - $rBlur )) + $rOrig ))
: $rOrig ;
$gNew = ( abs ( $gOrig - $gBlur ) >= $threshold )
? max ( 0 , min ( 255 , ( $amount * ( $gOrig - $gBlur )) + $gOrig ))
: $gOrig ;
$bNew = ( abs ( $bOrig - $bBlur ) >= $threshold )
? max ( 0 , min ( 255 , ( $amount * ( $bOrig - $bBlur )) + $bOrig ))
: $bOrig ;
if (( $rOrig != $rNew ) || ( $gOrig != $gNew ) || ( $bOrig != $bNew )) {
$pixCol = imagecolorallocate ( $img , $rNew , $gNew , $bNew );
imagesetpixel ( $img , $x , $y , $pixCol );
}
}
}
} else {
for ( $x = 0 ; $x < $w ; $x ++ ) { // each row
for ( $y = 0 ; $y < $h ; $y ++ ) { // each pixel
$rgbOrig = imagecolorat ( $img , $x , $y );
$rOrig = (( $rgbOrig >> 16 ) & 0xFF );
$gOrig = (( $rgbOrig >> 8 ) & 0xFF );
$bOrig = ( $rgbOrig & 0xFF );
$rgbBlur = imagecolorat ( $imgBlur , $x , $y );
$rBlur = (( $rgbBlur >> 16 ) & 0xFF );
$gBlur = (( $rgbBlur >> 8 ) & 0xFF );
$bBlur = ( $rgbBlur & 0xFF );
$rNew = ( $amount * ( $rOrig - $rBlur )) + $rOrig ;
if ( $rNew > 255 ) {
$rNew = 255 ;
} else if ( $rNew < 0 ) {
$rNew = 0 ;
}
$gNew = ( $amount * ( $gOrig - $gBlur )) + $gOrig ;
if ( $gNew > 255 ) {
$gNew = 255 ;
} else if ( $gNew < 0 ) {
$gNew = 0 ;
}
$bNew = ( $amount * ( $bOrig - $bBlur )) + $bOrig ;
if ( $bNew > 255 ) {
$bNew = 255 ;
} else if ( $bNew < 0 ) {
$bNew = 0 ;
}
$rgbNew = ( $rNew << 16 ) + ( $gNew << 8 ) + $bNew ;
imagesetpixel ( $img , $x , $y , $rgbNew );
}
}
}
imagedestroy ( $imgCanvas );
imagedestroy ( $imgBlur );
return $img ;
}
/**
* Calculate USM factor
*
* Return an integer value indicating how much an image should be sharpened
* according to resizing scalevalue and absolute target dimensions
*
* @ param mixed $targetWidth width of the targetimage
* @ param mixed $targetHeight height of the targetimage
* @ param mixed $origWidth
* @ param mixed $origHeight
*
* @ return int
*
*/
protected function calculateUSMfactor ( $targetWidth , $targetHeight , $origWidth = null , $origHeight = null ) {
if ( null === $origWidth ) $origWidth = $this -> getWidth ();
if ( null === $origHeight ) $origHeight = $this -> getHeight ();
$w = ceil ( $targetWidth / $origWidth * 100 );
$h = ceil ( $targetHeight / $origHeight * 100 );
$resizingScalevalue = null ;
$target = null ;
$res = null ;
// select the resizing scalevalue with check for crop images
if ( $w == $h || ( $w - 1 ) == $h || ( $w + 1 ) == $h ) { // equal, no crop
$resizingScalevalue = $w ;
$target = $targetWidth ;
} else { // crop
if (( $w < $h && $w < 100 ) || ( $w > $h && $h >= 100 )) {
$resizingScalevalue = $w ;
$target = $targetWidth ;
} elseif (( $w < $h && $w >= 100 ) || ( $w > $h && $h < 100 )) {
$resizingScalevalue = $h ;
$target = $targetHeight ;
}
}
// adjusting with respect to the scalefactor
$resizingScalevalue = ( $resizingScalevalue * - 1 ) + 100 ;
$resizingScalevalue = $resizingScalevalue < 0 ? $resizingScalevalue * - 1 : $resizingScalevalue ;
if ( $resizingScalevalue > 0 && $resizingScalevalue < 10 ) $resizingScalevalue += 15 ;
else if ( $resizingScalevalue > 9 && $resizingScalevalue < 25 ) $resizingScalevalue += 20 ;
else if ( $resizingScalevalue > 24 && $resizingScalevalue < 40 ) $resizingScalevalue += 35 ;
else if ( $resizingScalevalue > 39 && $resizingScalevalue < 55 ) $resizingScalevalue += 20 ;
else if ( $resizingScalevalue > 54 && $resizingScalevalue < 70 ) $resizingScalevalue += 5 ;
else if ( $resizingScalevalue > 69 && $resizingScalevalue < 80 ) $resizingScalevalue -= 10 ;
// adjusting with respect to absolute dimensions
if ( $target < 50 ) $res = intval ( $resizingScalevalue / 18 * 3 );
else if ( $target < 100 ) $res = intval ( $resizingScalevalue / 18 * 4 );
else if ( $target < 200 ) $res = intval ( $resizingScalevalue / 18 * 6 );
else if ( $target < 300 ) $res = intval ( $resizingScalevalue / 18 * 8 );
else if ( $target < 400 ) $res = intval ( $resizingScalevalue / 18 * 10 );
else if ( $target < 500 ) $res = intval ( $resizingScalevalue / 18 * 12 );
else if ( $target < 600 ) $res = intval ( $resizingScalevalue / 18 * 15 );
else if ( $target > 599 ) $res = $resizingScalevalue ;
$res = $res < 0 ? $res * - 1 : $res ; // avoid negative numbers
return $res ;
}
/**
* Prepares a new created GD image resource according to the IMAGETYPE
*
* Intended for use by the resize () method
*
* @ param resource $im , destination resource needs to be prepared
* @ param resource $image , with GIF we need to read from source resource
*
*/
protected function prepareImageLayer ( & $im , & $image ) {
if ( $this -> imageType == IMAGETYPE_PNG ) {
// @adamkiss PNG transparency
imagealphablending ( $im , false );
imagesavealpha ( $im , true );
} else if ( $this -> imageType == IMAGETYPE_GIF ) {
// @mrx GIF transparency
$transparentIndex = imagecolortransparent ( $image );
2023-03-10 19:41:40 +01:00
if ( $transparentIndex >= 0 && $transparentIndex < imagecolorstotal ( $image )) {
$transparentColor = imagecolorsforindex ( $image , $transparentIndex );
if ( ! empty ( $transparentColor )) {
$transparentNew = imagecolorallocate ( $im , $transparentColor [ 'red' ], $transparentColor [ 'green' ], $transparentColor [ 'blue' ]);
$transparentNewIndex = imagecolortransparent ( $im , $transparentNew );
imagefill ( $im , 0 , 0 , $transparentNewIndex );
}
2022-03-08 15:55:41 +01:00
}
} else {
$bgcolor = imagecolorallocate ( $im , 0 , 0 , 0 );
imagefilledrectangle ( $im , 0 , 0 , imagesx ( $im ), imagesy ( $im ), $bgcolor );
imagealphablending ( $im , false );
}
}
/**
* calculation if there is enough memory available at runtime for loading and resizing an given imagefile
*
* @ param array $sourceDimensions - array with three values : width , height , number of channels
* @ param array | bool $targetDimensions - optional - mixed : bool true | false or array with three values :
* width , height , number of channels
* @ param int | float Multiply needed memory by this factor
*
* @ return bool | null if a calculation was possible ( true | false ), or null if the calculation could not be done
*
*/
static public function checkMemoryForImage ( $sourceDimensions , $targetDimensions = false , $factor = 1 ) {
// with this static we only once need to read from php.ini and calculate phpMaxMem,
// regardless how often this function is called in a request
static $phpMaxMem = null ;
if ( null === $phpMaxMem ) {
$sMem = trim ( strtoupper ( ini_get ( 'memory_limit' )), ' B' ); // trim B just in case it has Mb rather than M
switch ( substr ( $sMem , - 1 )) {
case 'M' :
$phpMaxMem = (( int ) $sMem ) * 1048576 ;
break ;
case 'K' :
$phpMaxMem = (( int ) $sMem ) * 1024 ;
break ;
case 'G' :
$phpMaxMem = (( int ) $sMem ) * 1073741824 ;
break ;
default :
$phpMaxMem = ( int ) $sMem ;
}
}
if ( $phpMaxMem <= 0 ) {
// we couldn't read the MaxMemorySetting or there isn't one set,
// so in both cases we do not know if there is enough or not
return null ;
}
// calculate $sourceDimensions
if ( ! isset ( $sourceDimensions [ 0 ]) || ! isset ( $sourceDimensions [ 1 ]) || ! isset ( $sourceDimensions [ 2 ]) ||
! is_int ( $sourceDimensions [ 0 ]) || ! is_int ( $sourceDimensions [ 1 ]) || ! is_int ( $sourceDimensions [ 2 ])) {
return null ;
}
// width * height * channels
$imgMem = ( $sourceDimensions [ 0 ] * $sourceDimensions [ 1 ] * $sourceDimensions [ 2 ]);
if ( true === $targetDimensions ) {
// we have to add ram for a copy of the sourceimage
$imgMem += $imgMem ;
} else if ( is_array ( $targetDimensions )) {
// we have to add ram for a targetimage
if ( ! isset ( $targetDimensions [ 0 ]) || ! isset ( $targetDimensions [ 1 ]) || ! isset ( $targetDimensions [ 2 ]) ||
! is_int ( $targetDimensions [ 0 ]) || ! is_int ( $targetDimensions [ 1 ]) || ! is_int ( $targetDimensions [ 2 ])) {
return null ;
}
$imgMem += ( $targetDimensions [ 0 ] * $targetDimensions [ 1 ] * $targetDimensions [ 2 ]);
}
// read current allocated memory
$curMem = memory_get_usage ( true ); // memory_get_usage() is always available with PHP since 5.2.1
// check if there is enough RAM loading the image(s), plus 3 MB for GD to use for calculations/transforms
$extraMem = 3 * 1048576 ;
$availableMem = $phpMaxMem - $curMem ;
$neededMem = ( $imgMem + $extraMem ) * $factor ;
return $availableMem >= $neededMem ;
}
/**
* Additional functionality on top of existing checkMemoryForImage function for the flip / rotate actions
*
* @ param string $filename Filename to check . Default is whatever was set to this ImageSizer .
* @ param bool $double Need enough for both src and dst files loaded at same time ? ( default = true )
* @ param int | float $factor Tweak factor ( multiply needed memory by this factor ), i . e . 2 for rotate actions . ( default = 1 )
* @ param string $action Name of action ( if something other than " action " )
* @ param bool $throwIfNot Throw WireException if not enough memory ? ( default = false )
* @ return bool
* @ throws WireException
*
*/
protected function hasEnoughMemory ( $filename = '' , $double = true , $factor = 1 , $action = 'action' , $throwIfNot = false ) {
$error = '' ;
if ( empty ( $filename )) $filename = $this -> filename ;
if ( $filename ) {
if ( $filename != $this -> filename || empty ( $this -> info [ 'width' ])) {
$this -> prepare ( $filename ); // to populate $this->info
}
} else {
$error = 'No filename to check memory for' ;
}
if ( ! $error ) {
$hasEnough = self :: checkMemoryForImage ( array (
$this -> info [ 'width' ],
$this -> info [ 'height' ],
$this -> info [ 'channels' ]
), $double , $factor );
if ( $hasEnough === false ) {
$error = sprintf ( $this -> _ ( 'Not enough memory for “%1$s” on image file: %2$s' ), $action , basename ( $filename ));
}
}
if ( $error ) {
if ( $throwIfNot ) {
throw new WireException ( $error );
} else {
$this -> error ( $error );
return false ;
}
}
return true ;
}
/**
* Process a rotate or flip action
*
* @ param string $srcFilename
* @ param string $dstFilename
* @ param string $action One of 'rotate' or 'flip'
* @ param int | string $value If rotate , specify int of degrees . If flip , specify one of 'vertical' , 'horizontal' or 'both' .
* @ return bool
* @ throws WireException
*
*/
private function processAction ( $srcFilename , $dstFilename , $action , $value ) {
$action = strtolower ( $action );
$ext = strtolower ( pathinfo ( $srcFilename , PATHINFO_EXTENSION ));
$useTransparency = true ;
$memFactor = 1 ;
$img = null ;
if ( empty ( $dstFilename )) $dstFilename = $srcFilename ;
if ( $action == 'rotate' ) $memFactor *= 2 ;
if ( ! $this -> hasEnoughMemory ( $srcFilename , true , $memFactor , $action , false )) return false ;
if ( $ext == 'jpg' || $ext == 'jpeg' ) {
$img = imagecreatefromjpeg ( $srcFilename );
$useTransparency = false ;
} else if ( $ext == 'png' ) {
$img = imagecreatefrompng ( $srcFilename );
} else if ( $ext == 'gif' ) {
$img = imagecreatefromgif ( $srcFilename );
}
if ( ! $img ) {
$this -> error ( " imagecreatefrom $ext failed " , Notice :: debug );
return false ;
}
if ( $useTransparency ) {
imagealphablending ( $img , true );
imagesavealpha ( $img , true );
}
$success = true ;
$method = '_processAction' . ucfirst ( $action );
$imgNew = $this -> $method ( $img , $value );
if ( $imgNew === false ) {
// action fail
$success = false ;
$this -> error ( $this -> className () . " . $method (img, $value ) returned fail " , Notice :: debug );
} else if ( $imgNew !== $img ) {
// a new img object was created
imagedestroy ( $img );
$img = $imgNew ;
if ( $useTransparency ) {
imagealphablending ( $img , true );
imagesavealpha ( $img , true );
}
} else {
// existing img object was updated
$img = $imgNew ;
}
if ( $success ) {
if ( $ext == 'png' ) {
$success = imagepng ( $img , $dstFilename , 9 );
} else if ( $ext == 'gif' ) {
$success = imagegif ( $img , $dstFilename );
} else {
$success = imagejpeg ( $img , $dstFilename , $this -> quality );
}
if ( ! $success ) $this -> error ( " image { $ext } () failed " , Notice :: debug );
}
imagedestroy ( $img );
return $success ;
}
/**
* Process flip action ( internal )
*
* @ param resource $img
* @ param string $flipType vertical , horizontal or both
* @ return bool | resource
*
*/
private function _processActionFlip ( & $img , $flipType ) {
if ( ! function_exists ( 'imageflip' )) {
$this -> error ( " Image flip requires PHP 5.5 or newer " );
return false ;
}
if ( ! in_array ( $flipType , array ( 'vertical' , 'horizontal' , 'both' ))) {
$this -> error ( " Image flip type must be one of: 'vertical', 'horizontal', 'both' " );
return false ;
}
$constantName = 'IMG_FLIP_' . strtoupper ( $flipType );
$flipType = constant ( $constantName );
if ( $flipType === null ) {
$this -> error ( " Unknown constant for image flip: $constantName " );
return false ;
}
$success = imageflip ( $img , $flipType );
return $success ? $img : false ;
}
/**
* Process rotate action ( internal )
*
* @ param resource $img
* @ param $degrees
* @ return bool | resource
*
*/
private function _processActionRotate ( & $img , $degrees ) {
$degrees = ( int ) $degrees ;
$angle = 360 - $degrees ; // imagerotate is anti-clockwise
$imgNew = imagerotate ( $img , $angle , 0 );
return $imgNew ? $imgNew : false ;
}
private function _processActionGreyscale ( & $img , $unused ) {
if ( $unused ) {}
imagefilter ( $img , IMG_FILTER_GRAYSCALE );
return $img ;
}
private function _processActionSepia ( & $img , $sepia = 55 ) {
imagefilter ( $img , IMG_FILTER_GRAYSCALE );
imagefilter ( $img , IMG_FILTER_BRIGHTNESS , - 30 );
imagefilter ( $img , IMG_FILTER_COLORIZE , 90 , ( int ) $sepia , 30 );
return $img ;
}
/**
* Process rotate of an image
*
* @ param string $srcFilename
* @ param string $dstFilename
* @ param int $degrees Clockwise degrees , i . e . 90 , 180 , 270 , - 90 , - 180 , - 270
* @ return bool
*
*/
protected function processRotate ( $srcFilename , $dstFilename , $degrees ) {
return $this -> processAction ( $srcFilename , $dstFilename , 'rotate' , $degrees );
}
/**
* Process vertical or horizontal flip of an image
*
* @ param string $srcFilename
* @ param string $dstFilename
* @ param string $flipType Specify vertical , horizontal , or both
* @ return bool
*
*/
protected function processFlip ( $srcFilename , $dstFilename , $flipType ) {
return $this -> processAction ( $srcFilename , $dstFilename , 'flip' , $flipType );
}
/**
* Convert image to greyscale
*
* @ param string $dstFilename If different from source file
* @ return bool
*
*/
public function convertToGreyscale ( $dstFilename = '' ) {
return $this -> processAction ( $this -> filename , $dstFilename , 'greyscale' , null );
}
/**
* Convert image to sepia
*
* @ param string $dstFilename If different from source file
* @ param float | int $sepia Sepia value
* @ return bool
*
*/
public function convertToSepia ( $dstFilename = '' , $sepia = 55 ) {
return $this -> processAction ( $this -> filename , $dstFilename , 'sepia' , $sepia );
}
}