praiadeseselle/wire/core/ImageSizerEngineGD.php

1184 lines
37 KiB
PHP
Raw Permalink Normal View History

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 PHPs 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);
// $transparentColor = $transparentIndex != -1 ? @imagecolorsforindex($image, $transparentIndex) : 0;
$transparentColor = $transparentIndex != -1 ? imagecolorsforindex($image, ($transparentIndex < imagecolorstotal($image) ? $transparentIndex : $transparentIndex - 1)) : 0;
if(!empty($transparentColor)) {
$transparentNew = imagecolorallocate($im, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
$transparentNewIndex = imagecolortransparent($im, $transparentNew);
imagefill($im, 0, 0, $transparentNewIndex);
}
} 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);
}
}