'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); // $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); } }