I'm using PHP Imagick to resize images at runtime. The site has an image upload feature and we can't trust the user to use web-friendly JPEGs, as during the mass-import there are many 3 to 5MB images, and even a few as large as 13MB. Each image gets turned into a thumbnail (200x200), and when going from list view into detailed view only one image is shown, so performance isn't a huge deal although we can't completely throw it by the wayside. Here's what we're doing so far:
$iMagick = new Imagick($file);
$iMagick->setImageResolution(72,72);
$iMagick->resampleImage(72,72,imagick::FILTER_UNDEFINED,1);
$geometry = $iMagick->getImageGeometry();
if ($geometry['height'] > 1920 || $geometry['width'] > 1080) {
$iMagick->scaleImage(1920, 0);
if($geometry['height'] > $resizeHeight) {
$iMagick->scaleImage(0, 1080);
}
}
$iMagick->setImageCompression(Imagick::COMPRESSION_JPEG);
$iMagick->setImageCompressionQuality($compression);
$iMagick->writeImage($file);
$Imagick->clear();
Side note: I just realized the flaw in my conditional logic here about height/width, so ignore that for the time being. I'll edit the question soon to reflect the correct condition comparisons. For those who didn't catch it it's scaling all images to 1920 wide even if it's supposed to be scaling the height, then after the scale it's sizing it back down to 1080 height.
The image I've used to test starts as a 3MB 2398×2400 image. Scaling it to 1079x1080 results in a 1.5MB image, then adding JPEG compression at quality 70 brings it down to 750KB. Using kraken.io the image was able to be compressed to just under 60KB. Are there any additional things I can do to this script since it needs to optimize the image at runtime?
I've read suggestions to use libjpeg, which is installed, but I can't find any documentation on what functions it enables or if there's a way to force Imagick to use it specifically. I'm not even sure that Imagick isn't using it already.
Edit: Solution
function itm_optimizeImage($file, $compression = 70, $maxDimensions = ['width' => null, 'height' => null]) {
$save = false;
$fi = new finfo(FILEINFO_MIME);
$mime = explode(';', $fi->file($file));
switch ($mime[0]) {
// possible to optimize other image types in the future
case 'image/jpeg':
try {
$iMagick = new Imagick($file);
if ($iMagick->getImageCompressionQuality() > $compression) {
$file = !itm_compressJPEG($file, $compression, $maxDimensions, $iMagick);
}
}
catch (Exception $e) {
error_log(__FUNCTION__ . " $path/$file failed: " . $e->getMessage());
return false;
}
if ($file) {
$pathParts = pathinfo($file);
rename($file, $pathParts['dirname'] . '/' . $pathParts['filename'] . '.large.' . $pathParts['extension']);
$iMagick->writeImage($file);
}
$iMagick->clear();
break;
}
return $file;
}
function itm_compressJPEG($file, $compression = 70, $maxDimensions = ['width' => null, 'height' => null], &$iMagick = null) {
try {
$iMagickCreated = true;
if ($iMagick) $iMagickCreated = false;
else $iMagick = new Imagick($file);
$iMagick->setImageResolution(72,72);
$iMagick->resampleImage(72,72,imagick::FILTER_UNDEFINED,1);
$geometry = $iMagick->getImageGeometry();
if (($geometry['width'] / $maxDimensions['width']) > ($geometry['height'] / $maxDimensions['height'])) {
// scale by width
$iMagick->scaleImage($maxDimensions['width'], 0);
} else {
// scale by height
$iMagick->scaleImage(0, $maxDimensions['height']);
}
$iMagick->setImageCompression(Imagick::COMPRESSION_JPEG);
$iMagick->setImageCompressionQuality($compression);
$iMagick->setImageFormat('jpg');
$iMagick->stripImage();
if ($iMagickCreated) {
$pathParts = pathinfo($file);
rename($file, $pathParts['dirname'] . '/' . $pathParts['filename'] . '.large.' . $pathParts['extension']);
$iMagick->writeImage($file);
$Imagick->clear();
}
return $file;
}
catch (Exception $e) {
error_log(__FUNCTION__ . " $path/$file failed: " . $e->getMessage());
return false;
}
}
Set the setImageCompressionQuality to a value of 70 and add these two lines after it.
$image->setImageFormat("jpg");
$image->stripImage();
This will dramatically reduce the size of the images.
Related
I want to get the resolution of an AI file but it only returns 72. However, the resolution is supposed to be 300. If I were to change the resolution of the image using imagick, it would come out to be something far less [the dimensions]. How can I get the actual resolution of a file?
In case I can't use imagick for that, is there a library or framework that can give me the precised resolution of a file (regardless what type)?
I have my code as:
try {
$imagick = new Imagick($imagePath);
$data = $imagick->identifyimage();
}
catch(Exception $e) {
echo json_encode('ERROR: ' . $e->getMessage());
}
if($data['units'] == 'PixelsPerCentimeter') {
$imageResolution = $imagick->getImageResolution(); // Temp Image Resolution
//echo 'pixels per cent lol';
if (!empty($imageResolution['y'])) {
$dpi = intval( round($imageResolution['y'] * 2.54, 2) );
}
}
else $dpi = $data['resolution']['x'];
$width = round($data['geometry']['width'] / $dpi, 3);
$height = round($data['geometry']['height'] / $dpi, 3);
My application allows users to upload bunch of images which are later sent as an email attachment. The problem shows up if file size of images that are being sent is bigger than email accept.
What I want to do is to reduce file size of image until it is lower then 5MB and this is how I tried to do it:
/*..some code...*/
$img_quality = 75;
while (filesize($path) >= 5242880) {
$img_string = file_get_contents($path);
$img = imagecreatefromstring($img_string);
$path = $this->getFilePath($file, $file_section, $entity_id);
imagejpeg($img, $path, $img_quality);
$img_quality--;
}
/*..some code...*/
//Functions I am calling
public function getFilePath($upload, $section, $id = null) {
$path = base_path('../upload').$this->downloadOverwrite($upload, $section, $id);
if(!$upload) {
return $path;
}
if(!file_exists($path)) {
return null;
}
$tmppath = #tempnam("tmp", "myapp");
file_put_contents($tmppath, file_get_contents($path));
return $tmppath;
}
public function downloadOverwrite($upload, $section, $id = null, $config = []) {
$section = !empty($upload['entity']) ? $upload['entity'] : $section;
$id = !empty($upload['entity_id']) ? $upload['entity_id'] : $id;
$path = empty($id) ? "/$section" : "/$section/$id";
if(!empty($upload)) $path .= "/{$upload['fs_name']}";
return $path;
}
This code actually works, but if image file size is too big, it takes too long before image is compressed to desired value. Is there any better solution to do this?
actually yes there is a package which is really popular for manipulating images in php which has good integration with laravel :
http://image.intervention.io/getting_started/installation
so with this package you do as below :
$ php composer.phar require intervention/image
and after adding aliases if laravel < 5.5 you publish vendor files :
$ php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravelRecent"
then you can replace image handling to this :
// create instance
$img = Image::make('public/foo.jpg');
// resize image to fixed size
$img->resize(300, 200);
to resize or do any thing like prevent upsizing like :
// prevent possible upsizing
$img->resize(null, 400, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
or any thing you would like to do according to documentation .
hope this helps
In case someone has similiar problem, here is the loop that I used in the end and that worked pretty well for me. May not be the best solution out there, but it's fair enough:
$image_width = getimagesize($path)[0];
while (filesize($path) >= 5242880) {
$image_width -= 50;
$img_string = file_get_contents($path);
$img = imagecreatefromstring($img_string);
$path = $this->getFilePath($file, $file_section, $entity_id);
$img = imagescale($img, $image_width);
imagejpeg($img, $path);
}
I am lowering the width of the image, while height is being lowered automatically respecting the aspect ratio. This results in pretty fast file size drop, while keeping the quality of image at approximately the same level as it was before scaling.
I've managed to install Imagemagick 7 on my server and set-up the appropriate PHP module in order to work with HEIC and WebP files using Imagick.
I can convert these files easily to other formats like PNG or JPEG without any problems.
Now, I try to convert a HEIC sequence (original file) to GIF, but the result is imperfect.
First off all, it seems, that Imagick isn't able to determine the delay between every image. This results in a lightning fast animation. So for now, I've hardcoded a delay of 40 ticks using Imagick::setImageDelay. I'm not sure, if I'm missing something, or if Imagick/Imagemagick isn't able to do this on HEIC sequences.
The next problem I'm experiencing is, that Imagick can't handle the original image dimensions properly. To get around this, I have to get the original image's dimensions and perform Imagick::cropImage and Imagick::thumbnailImage on every frame, before I finalize it with Imagick::setImagePage.
However, my biggest problem for now is, that the result has a broken, first frame:
So the final (broken) result looks like this:
While the expected result (generated by heic2any) is:
The (simplified) code I am using:
<?php
if (!in_array('HEIC', \Imagick::queryFormats("HEI*"))) {
throw new \Exception('Unsupported format');
}
$sourceFile = __DIR__ . '/3.heic';
$targetFile = __DIR__ . '/3.gif';
if (!file_exists($sourceFile)) {
try {
$content = file_get_contents('https://github.com/alexcorvi/heic2any/blob/master/demo/' . basename($sourceFile) . '?raw=true');
if ($content === false) {
throw new \Exception('Unable to download source file.');
}
file_put_contents($sourceFile, $content);
} catch (\Exception $e) {
throw new \Exception('Unable to download source file: ' . $e->getMessage());
}
if (!file_exists($sourceFile)) {
throw new \Exception('Cannot find source file.');
}
}
$im = new \Imagick($sourceFile);
$width = $im->getImageWidth();
$height = $im->getImageHeight();
$im = $im->coalesceImages();
foreach ($im as $frame) {
$im->setImageDelay(40);
$frame->cropImage($width, $height, 0, 0);
$frame->thumbnailImage($width, $height);
$frame->setImagePage($width, $height, 0, 0);
}
$im = $im->deconstructImages();
$im->writeImages($targetFile, true);
echo 'Size: ' . $width . 'x' . $height . '<br />Result:<br /><br /><img src="' . basename($targetFile) . '?v=' . time() . '" alt="" />';
So my questions are:
Where does the 1st frame come from?
How can I avoid it?
Is there anything to optimize the conversion (code)?
UPDATE:
There's another .HEIC sequence to play with (click) and this one can be converted without problems:
UPDATE:
Even conversion services like cloudconvert can't handle the image correctly and producing GIF animations like this:
Background:
I have been working on an issue whereby a file upload form will only accept certain images, it is not a problem with image size(dimensions) or with size(filesize) but I think i've found it's to do with image bit depth, as the PHP script resizes the image and applies a static (semi-transparant) watermark file to the image and saves the result.
I have gone through a long process today and yesterday working with this as the error that occurs is the server stating "Connection reset by server" . Which I think I've narrowed down to the server not having enough free memory to complete the image resource manipulations.
Issue:
Then, after all that, this error comes up on file upload submission:
IPS detected for "WEB PHP EXIF Process IFD TAG Remote Integer Overflow -2 (CVE-20/Buffer Over Flow"
Some googling only tells me that it's a securty vulnerabiliy for PHP < 4.2 or so, and doesn't relate to actually what's going on.
I have looked through my code and am very sure that
My query is to try and shed some light on what this error statement means (and how I can go about correcting it)?
Notes:
PHP 5.6.16
No PHP EXIF functions run on the page.
I can't see anywhere on the page that would cause an integer overflow (running to infinity, I guess)
Page does use imagecopy, imagealphablending and imagesavealpha But this error notice seems to appear before the script runs on the uploaded file.
This error only occurs on some uploaded images and not on others.
I have found no Apache error logs relating to this
I have no PHP errors recorded.
Code:
As I think the issue may occur somehow before the page loads I'm not sure how useful this code block will be but, take a look:
(also please note that I am aware this page is 5 years old and isn't the smartest programming, i have fixed it up in parts but am aware it can probably be further tidied)
<?php
session_start();
error_reporting(E_ALL);
ini_set('display_errors', 1);
///first set out variables.
$goodImage = 0;
$maxfilesize = (int)$_POST['MAX_FILE_SIZE'];
$gallery = (int)$_POST['galfolderid']
if (empty($gallery)) {
$_SESSION['message'] = $gallery . "<br/>Gallery Value has not been set.";
}
elseif (!is_numeric($gallery)) {
$_SESSION['message'] = $gallery . "<br/>Gallery Value is not a numeric value.";
}
elseif (!is_dir($_SERVER['DOCUMENT_ROOT']."/images/gallery/" . $gallery )) {
$_SESSION['message'] = $gallery . "<br/>Gallery Value, while being a number, is not a valid numeric value.";
}
if(!empty($_SESSION['message'])) {
header("Location:editgallery.php?gallery=".$galleryid);
exit;
}
for($count=0;$count<5;$count++) {
if ($_FILES['image']['error'][$count] == 0) {
clearstatcache();
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$imageType = finfo_file($finfo, $_FILES['image']['tmp_name'][$count]);
finfo_close($finfo);
unset($finfo);
if (strtolower($imageType) == "image/png") {
$imgtype = "PNG";
$original = imagecreatefrompng($_FILES['image']['tmp_name'][$count]);
} elseif (strtolower($imageType) == "image/jpeg" || strtolower($imageType) == "image/jpg") {
$imgtype = "JPG";
$original = imagecreatefromjpeg($_FILES['image']['tmp_name'][$count]);
} else {
//not a JPG or PNG type... set error.
$_SESSION['message'] = "Image " . ($count+1) . " does not appear to be a valid .JPG or .PNG file. ";
error_log($_SESSION['message']);
header("Location:editgallery.php");
exit;
}
unlink($_FILES['image']['tmp_name'][$count]);
$edgePadding = (int)$_POST['WMedge'][$count]; //integer
$watermarkScale = $_POST['WMscale'][$count]; //float
$h_position = $_POST['Hpos'][$count]; //text array
$v_position = $_POST['Vpos'][$count]; //text array
$Hoz = $h_position[0];
$Vez = $v_position[0];
if (!isset($watermarkScale) || empty($watermarkScale) || !is_numeric($watermarkScale)) {
$watermarkScale = 1.0;
}
elseif($watermarkScale > 2.0) {
$watermarkScale = 2.0;
}
elseif($watermarkScale < 0.5) {
$watermarkScale = 0.5;
}
if ((!isset($edgePadding) || empty($edgePadding) || !is_numeric($edgePadding)) && $edgePadding !== "0") {
$edgePadding = 5;
}
// be sure that the other options we need have some kind of value
if ($_FILES['image']['size'][$count] > $maxfilesize) {
$_SESSION['message'] .= "Image file ".($count+1)." appears to be too big. There is a limit of 2Mb on the
size of the image you can upload. Please click 'Back' on your browser and resubmit a valid image.";
error_log($_SESSION['message']);
//file too big.
}
//target name is the number of images in the LARGE gallery,
///cycle through imagenames to find the first "clear" image number.
$newimagename = 1;
clearstatcache();
/***
NOTE: Only possible place an integer oveflow might occur I think
***/
while (file_exists($_SERVER['DOCUMENT_ROOT'] . "/images/gallery/" . $gallery . "/S/" . $newimagename . ".jpg")) {
$newimagename++;
}
///newimagename is the new filename of the image.
$target_name = $newimagename . ".jpg";
$target = $_SERVER['DOCUMENT_ROOT'] . "/images/gallery/" . $gallery . "/L/" . $target_name;
$targetSmall = $_SERVER['DOCUMENT_ROOT'] . "/images/gallery/" . $gallery . "/S/" . $target_name;
//targetSM is used below to make the thumbnail image. for the SMALL gallery.
//full target directory and name.
// file upload success
// now file need resizing before the watermark is applied.
// resize new dimensions (Large- 800px)
$originalImageSize[0] = imagesx($original);
$originalImageSize[1] = imagesy($original);
/***
* Resize new dimensions for small thumbnail.
***/
if ($originalImageSize[0] > 133) {
$percent = 133 / $originalImageSize[0];
} else {
$percent = 1;
}
$newThumbHeight = $originalImageSize[1] * $percent;
$newThumbWidth = $originalImageSize[0] * $percent;
// Resample
$imageThumbFinal = imagecreatetruecolor($newThumbWidth, $newThumbHeight);
imagecopyresampled($imageThumbFinal, $original, 0, 0, 0, 0, $newThumbWidth, $newThumbHeight, $originalImageSize[0], $originalImageSize[1]);
// Output
imagejpeg($imageThumbFinal, $targetSmall, 80);
imagedestroy($imageThumbFinal);
unset($imageThumbFinal, $percent,$newThumbWidth,$newThumbHeight);
/***
* End Thumbnail generation.
*/
/***
* Resize main sized image (max 800px wide)
***/
if ($originalImageSize[0] > 800) {
$percent = 800 / $originalImageSize[0];
} else {
$percent = 1;
}
$newPictureHeight = $originalImageSize[1] * $percent;
$newPictureWidth = $originalImageSize[0] * $percent;
// Resample main image to reduce max size.
$imageCleanFinal = imagecreatetruecolor($newPictureWidth, $newPictureHeight);
imagecopyresampled($imageCleanFinal, $original, 0, 0, 0, 0, $newPictureWidth, $newPictureHeight, $originalImageSize[0], $originalImageSize[1]);
/***
* Now resize the watermark and save onto the final image.
***/
//full target directory and name.
$watermark = $_SERVER['DOCUMENT_ROOT'] . "/images/watermark2.png";
$wmTarget = substr($watermark, 0, -3) . "tmp"; //image/watermark2.tmp is resized.
$sourceWaterImage = imagecreatefrompng($watermark);
$waterMarkWidth = imagesx($sourceWaterImage);
$waterMarkHeight = imagesy($sourceWaterImage);
$destinationHeight = $waterMarkHeight * $watermarkScale;
$destinationWidth = $waterMarkWidth * $watermarkScale;
$destinationHeight = floor($destinationHeight);
$destinationWidth = floor($destinationWidth);
$destinationWatermarkImage = imagecreatetruecolor($destinationWidth, $destinationHeight);
imagealphablending($destinationWatermarkImage, FALSE);
imagesavealpha($destinationWatermarkImage, TRUE);
imagecopyresampled($destinationWatermarkImage, $sourceWaterImage, 0, 0, 0, 0, $destinationWidth,
$destinationHeight, $waterMarkWidth, $waterMarkHeight);
imagedestroy($sourceWaterImage);
unset($sourceWaterImage);
clearstatcache();
/***
* Set watermark location on final image.
***/
$differenceX = $newPictureWidth - $destinationWidth;
$differenceY = $newPictureHeight - $destinationHeight;
switch ($Hoz) {
// find the X coord for placement
case 'left':
$placementX = $edgePadding;
break;
case 'center':
$placementX = round($differenceX / 2);
break;
case 'right':
$placementX = $newPictureWidth - ($destinationWidth + $edgePadding);
break;
default:
$placementX = 0;
break;
}
switch ($Vez) {
// find the Y coord for placement
case 'top':
$placementY = $edgePadding;
break;
case 'middle':
$placementY = round($differenceY / 2);
break;
case 'bottom':
$placementY = $newPictureHeight - ($destinationHeight + $edgePadding);
break;
default:
$placementY = 0;
break;
}
/***
* Finish set Watermark location
***/
imagecopy($imageCleanFinal,
$destinationWatermarkImage,
$placementX,
$placementY,
0,
0,
$destinationWidth,
$destinationHeight);
imagejpeg($imageCleanFinal, $target, 80);
/***
* Image final save
***/
// Output
imagejpeg($imageCleanFinal, $target, 80);
imagedestroy($imageCleanFinal);
imagedestroy($destinationWatermarkImage);
unset($imageCleanFinal, $percent);
unlink($wmTarget);
$output[$goodImage]['targetName'] = $target_name;
$output[$goodImage]['targetAddr'] = $target;
$goodImage++;
} elseif (!isset($_FILES['image']['name'][$count]) || ($_FILES['image']['error'][$count] != 0 && $_FILES['image']['error'][$count] != 4)) {
//error in file upload.
$_SESSION['message'] .= " There is a file upload error number " . $_FILES['image']['error'][$count] . ". for image ".($count+1).".
Please try resubmitting a valid image.";
error_log($_SESSION['message']);
}
}
if (empty($_SESSION['message'])){
$_SESSION['message'] = $goodImage." New Image(s) Uploaded";
}
// display resulting image for download
header("Location:editgallery.php?special=yes&gallery=".$galleryid);
exit;
Server Details:
Apache 2.4
WebHostManger & CPanel (combined) version 54.0.19
PHP 5.6 using Mod suPHP 0.7.2
2 core 2.2GHz (so x2) intel with
1896Mb/2097Mb physical memory (used/available)
+4095Mb memory Swap File. (~6010Mb total)
I have a function that uploads files up to 8MB but now I also want to compress or at least rescale larger images, so my output image won't be any bigger than 100-200 KB and 1000x1000px resolution. How can I implement compress and rescale (proportional) in my function?
function uploadFile($file, $file_restrictions = '', $user_id, $sub_folder = '') {
global $path_app;
$new_file_name = generateRandomString(20);
if($sub_folder != '') {
if(!file_exists('media/'.$user_id.'/'.$sub_folder.'/')) {
mkdir('media/'.$user_id.'/'.$sub_folder, 0777);
}
$sub_folder = $sub_folder.'/';
}
else {
$sub_folder = '';
}
$uploadDir = 'media/'.$user_id.'/'.$sub_folder;
$uploadDirO = 'media/'.$user_id.'/'.$sub_folder;
$finalDir = $path_app.'/media/'.$user_id.'/'.$sub_folder;
$fileExt = explode(".", basename($file['name']));
$uploadExt = $fileExt[count($fileExt) - 1];
$uploadName = $new_file_name.'_cache.'.$uploadExt;
$uploadDir = $uploadDir.$uploadName;
$restriction_ok = true;
if(!empty($file_restrictions)) {
if(strpos($file_restrictions, $uploadExt) === false) {
$restriction_ok = false;
}
}
if($restriction_ok == false) {
return '';
}
else {
if(move_uploaded_file($file['tmp_name'], $uploadDir)) {
$image_info = getimagesize($uploadDir);
$image_width = $image_info[0];
$image_height = $image_info[1];
if($file['size'] > 8000000) {
unlink($uploadDir);
return '';
}
else {
$finalUploadName = $new_file_name.'.'.$uploadExt;
rename($uploadDirO.$uploadName, $uploadDirO.$finalUploadName);
return $finalDir.$finalUploadName;
}
}
else {
return '';
}
}
}
For the rescaling I use a function like this:
function dimensions($width,$height,$maxWidth,$maxHeight)
// given maximum dimensions this tries to fill that as best as possible
{
// get new sizes
if ($width > $maxWidth) {
$height = Round($maxWidth*$height/$width);
$width = $maxWidth;
}
if ($height > $maxHeight) {
$width = Round($maxHeight*$width/$height);
$height = $maxHeight;
}
// return array with new size
return array('width' => $width,'height' => $height);
}
The compression is done by a PHP function:
// set limits
$maxWidth = 1000;
$maxHeight = 1000;
// read source
$source = imagecreatefromjpeg($originalImageFile);
// get the possible dimensions of destination and extract
$dims = dimensions(imagesx($source),imagesy($source),$maxWidth,$maxHeight);
// prepare destination
$dest = imagecreatetruecolor($dims['width'],$dims['height']);
// copy in high-quality
imagecopyresampled($dest,$source,0,0,0,0,
$width,$height,imagesx($source),imagesy($source));
// save file
imagejpeg($dest,$destinationImageFile,85);
// clear both copies from memory
imagedestroy($source);
imagedestroy($dest);
You will have to supply $originalImageFile and $destinationImageFile. This stuff comes from a class I use, so I edited it quite a lot, but the basic functionality is there. I left out any error checking, so you still need to add that. Note that the 85 in imagejpeg() denotes the amount of compression.
you can use a simple one line solution through imagemagic library the command will like this
$image="path to image";
$res="option to resize"; i.e 25% small , 50% small or anything else
exec("convert ".$image." -resize ".$res." ".$image);
with this you can rotate resize and many other image customization
Take a look on imagecopyresampled(), There is also a example that how to implement it, For compression take a look on imagejpeg() the third parameter helps to set quality of the image, 100 means (best quality, biggest file) and if you skip the last option then default quality is 75 which is good and compress it.