Efficient JPEG Image Resizing in PHP

88,054

Solution 1

People say that ImageMagick is much faster. At best just compare both libraries and measure that.

  1. Prepare 1000 typical images.
  2. Write two scripts -- one for GD, one for ImageMagick.
  3. Run both of them a few times.
  4. Compare results (total execution time, CPU and I/O usage, result image quality).

Something which the best everyone else, could not be the best for you.

Also, in my opinion, ImageMagick has much better API interface.

Solution 2

Here's a snippet from the php.net docs that I've used in a project and works fine:

<?
function fastimagecopyresampled (&$dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h, $quality = 3) {
    // Plug-and-Play fastimagecopyresampled function replaces much slower imagecopyresampled.
    // Just include this function and change all "imagecopyresampled" references to "fastimagecopyresampled".
    // Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting.
    // Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain.
    //
    // Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero.
    // Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect.
    // 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized.
    // 2 = Up to 95 times faster.  Images appear a little sharp, some prefer this over a quality of 3.
    // 3 = Up to 60 times faster.  Will give high quality smooth results very close to imagecopyresampled, just faster.
    // 4 = Up to 25 times faster.  Almost identical to imagecopyresampled for most images.
    // 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled.

    if (empty($src_image) || empty($dst_image) || $quality <= 0) { return false; }
    if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) {
        $temp = imagecreatetruecolor ($dst_w * $quality + 1, $dst_h * $quality + 1);
        imagecopyresized ($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h);
        imagecopyresampled ($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality);
        imagedestroy ($temp);
    } else imagecopyresampled ($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
    return true;
}
?>

http://us.php.net/manual/en/function.imagecopyresampled.php#77679

Solution 3

phpThumb uses ImageMagick whenever possible for speed (falling back to GD if necessary) and seems to cache pretty well to reduce the load on the server. It's pretty lightweight to try out (to resize an image, just call phpThumb.php with a GET query that includes the graphic filename and output dimensions), so you might give it a shot to see if it meets your needs.

Solution 4

For larger images use libjpeg to resize on image load in ImageMagick and thereby significantly reducing memory usage and improving performance, it is not possible with GD.

$im = new Imagick();
try {
  $im->pingImage($file_name);
} catch (ImagickException $e) {
  throw new Exception(_('Invalid or corrupted image file, please try uploading another image.'));
}

$width  = $im->getImageWidth();
$height = $im->getImageHeight();
if ($width > $config['width_threshold'] || $height > $config['height_threshold'])
{
  try {
/* send thumbnail parameters to Imagick so that libjpeg can resize images
 * as they are loaded instead of consuming additional resources to pass back
 * to PHP.
 */
    $fitbyWidth = ($config['width_threshold'] / $width) > ($config['height_threshold'] / $height);
    $aspectRatio = $height / $width;
    if ($fitbyWidth) {
      $im->setSize($config['width_threshold'], abs($width * $aspectRatio));
    } else {
      $im->setSize(abs($height / $aspectRatio), $config['height_threshold']);
    }
    $im->readImage($file_name);

/* Imagick::thumbnailImage(fit = true) has a bug that it does fit both dimensions
 */
//  $im->thumbnailImage($config['width_threshold'], $config['height_threshold'], true);

// workaround:
    if ($fitbyWidth) {
      $im->thumbnailImage($config['width_threshold'], 0, false);
    } else {
      $im->thumbnailImage(0, $config['height_threshold'], false);
    }

    $im->setImageFileName($thumbnail_name);
    $im->writeImage();
  }
  catch (ImagickException $e)
  {
    header('HTTP/1.1 500 Internal Server Error');
    throw new Exception(_('An error occured reszing the image.'));
  }
}

/* cleanup Imagick
 */
$im->destroy();

Solution 5

From you quesion, it seems you are kinda new to GD, I will share some experence of mine, maybe this is a bit off topic, but I think it will be helpful to someone new to GD like you:

Step 1, validate file. Use the following function to check if the $_FILES['image']['tmp_name'] file is valid file:

   function getContentsFromImage($image) {
      if (@is_file($image) == true) {
         return file_get_contents($image);
      } else {
         throw new \Exception('Invalid image');
      }
   }
   $contents = getContentsFromImage($_FILES['image']['tmp_name']);

Step 2, get file format Try the following function with finfo extension to check file format of the file(contents). You would say why don't you just use $_FILES["image"]["type"] to check file format? Because it ONLY check file extension not file contents, if someone rename a file originally called world.png to world.jpg, $_FILES["image"]["type"] will return jpeg not png, so $_FILES["image"]["type"] may return wrong result.

   function getFormatFromContents($contents) {
      $finfo = new \finfo();
      $mimetype = $finfo->buffer($contents, FILEINFO_MIME_TYPE);
      switch ($mimetype) {
         case 'image/jpeg':
            return 'jpeg';
            break;
         case 'image/png':
            return 'png';
            break;
         case 'image/gif':
            return 'gif';
            break;
         default:
            throw new \Exception('Unknown or unsupported image format');
      }
   }
   $format = getFormatFromContents($contents);

Step.3, Get GD resource Get GD resource from contents we have before:

   function getGDResourceFromContents($contents) {
      $resource = @imagecreatefromstring($contents);
      if ($resource == false) {
         throw new \Exception('Cannot process image');
      }
      return $resource;
   }
   $resource = getGDResourceFromContents($contents);

Step 4, get image dimension Now you can get image dimension with the following simple code:

  $width = imagesx($resource);
  $height = imagesy($resource);

Now, Let's see what variable we got from the original image then:

       $contents, $format, $resource, $width, $height
       OK, lets move on

Step 5, calculate resized image arguments This step is related to your question, the purpose of the following function is to get resize arguments for GD function imagecopyresampled(), the code is kinda long, but it works great, it even has three options: stretch, shrink, and fill.

stretch: output image's dimension is the same as the new dimension you set. Won't keep height/width ratio.

shrink: output image's dimension won't exceed the new dimension you give, and keep image height/width ratio.

fill: output image's dimension will be the same as new dimension you give, it will crop & resize image if needed, and keep image height/width ratio. This option is what you need in your question.

   function getResizeArgs($width, $height, $newwidth, $newheight, $option) {
      if ($option === 'stretch') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
         $src_w = $width;
         $src_h = $height;
         $src_x = 0;
         $src_y = 0;
      } else if ($option === 'shrink') {
         if ($width <= $newwidth && $height <= $newheight) {
            return false;
         } else if ($width / $height >= $newwidth / $newheight) {
            $dst_w = $newwidth;
            $dst_h = (int) round(($newwidth * $height) / $width);
         } else {
            $dst_w = (int) round(($newheight * $width) / $height);
            $dst_h = $newheight;
         }
         $src_x = 0;
         $src_y = 0;
         $src_w = $width;
         $src_h = $height;
      } else if ($option === 'fill') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         if ($width / $height >= $newwidth / $newheight) {
            $src_w = (int) round(($newwidth * $height) / $newheight);
            $src_h = $height;
            $src_x = (int) round(($width - $src_w) / 2);
            $src_y = 0;
         } else {
            $src_w = $width;
            $src_h = (int) round(($width * $newheight) / $newwidth);
            $src_x = 0;
            $src_y = (int) round(($height - $src_h) / 2);
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
      }
      if ($src_w < 1 || $src_h < 1) {
         throw new \Exception('Image width or height is too small');
      }
      return array(
          'dst_x' => 0,
          'dst_y' => 0,
          'src_x' => $src_x,
          'src_y' => $src_y,
          'dst_w' => $dst_w,
          'dst_h' => $dst_h,
          'src_w' => $src_w,
          'src_h' => $src_h
      );
   }
   $args = getResizeArgs($width, $height, 150, 170, 'fill');

Step 6, resize image Use $args, $width, $height, $format and $resource we got from above into the following function and get the new resource of the resized image:

   function runResize($width, $height, $format, $resource, $args) {
      if ($args === false) {
         return; //if $args equal to false, this means no resize occurs;
      }
      $newimage = imagecreatetruecolor($args['dst_w'], $args['dst_h']);
      if ($format === 'png') {
         imagealphablending($newimage, false);
         imagesavealpha($newimage, true);
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
      } else if ($format === 'gif') {
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
         imagecolortransparent($newimage, $transparentindex);
      }
      imagecopyresampled($newimage, $resource, $args['dst_x'], $args['dst_y'], $args['src_x'], $args['src_y'], $args['dst_w'], $args['dst_h'], $args['src_w'], $args['src_h']);
      imagedestroy($resource);
      return $newimage;
   }
   $newresource = runResize($width, $height, $format, $resource, $args);

Step 7, get new contents, Use the following function to get contents from the new GD resource:

   function getContentsFromGDResource($resource, $format) {
      ob_start();
      switch ($format) {
         case 'gif':
            imagegif($resource);
            break;
         case 'jpeg':
            imagejpeg($resource, NULL, 100);
            break;
         case 'png':
            imagepng($resource, NULL, 9);
      }
      $contents = ob_get_contents();
      ob_end_clean();
      return $contents;
   }
   $newcontents = getContentsFromGDResource($newresource, $format);

Step 8 get extension, Use the following function to get extension of from image format(note, image format is not equal to image extension):

   function getExtensionFromFormat($format) {
      switch ($format) {
         case 'gif':
            return 'gif';
            break;
         case 'jpeg':
            return 'jpg';
            break;
         case 'png':
            return 'png';
      }
   }
   $extension = getExtensionFromFormat($format);

Step 9 save image If we have a user named mike, you can do the following, it will save to the same folder as this php script:

$user_name = 'mike';
$filename = $user_name . '.' . $extension;
file_put_contents($filename, $newcontents);

Step 10 destroy resource Don't forget destroy GD resource!

imagedestroy($newresource);

or you can write all your code into a class, and simply use the following:

   public function __destruct() {
      @imagedestroy($this->resource);
   }

TIPS

I recommend not to convert file format that user upload, you will meet many problems.

Share:
88,054

Related videos on Youtube

maxsilver
Author by

maxsilver

Technical Product Manager, Software Developer. Experience with Ruby + Rails and Java + Android. Previously worked in VB.net + MS-SQL and PHP + MySQL.

Updated on April 03, 2020

Comments

  • maxsilver
    maxsilver about 4 years

    What's the most efficient way to resize large images in PHP?

    I'm currently using the GD function imagecopyresampled to take high resolution images, and cleanly resize them down to a size for web viewing (roughly 700 pixels wide by 700 pixels tall).

    This works great on small (under 2 MB) photos and the entire resize operation takes less than a second on the server. However, the site will eventually service photographers who may be uploading images up to 10 MB in size (or images up to 5000x4000 pixels in size).

    Doing this kind of resize operation with large images tends to increase the memory usage by a very large margin (larger images can spike the memory usage for the script past 80 MB). Is there any way to make this resize operation more efficient? Should I be using an alternate image library such as ImageMagick?

    Right now, the resize code looks something like this

    function makeThumbnail($sourcefile, $endfile, $thumbwidth, $thumbheight, $quality) {
        // Takes the sourcefile (path/to/image.jpg) and makes a thumbnail from it
        // and places it at endfile (path/to/thumb.jpg).
    
        // Load image and get image size.
        $img = imagecreatefromjpeg($sourcefile);
        $width = imagesx( $img );
        $height = imagesy( $img );
    
        if ($width > $height) {
            $newwidth = $thumbwidth;
            $divisor = $width / $thumbwidth;
            $newheight = floor( $height / $divisor);
        } else {
            $newheight = $thumbheight;
            $divisor = $height / $thumbheight;
            $newwidth = floor( $width / $divisor );
        }
    
        // Create a new temporary image.
        $tmpimg = imagecreatetruecolor( $newwidth, $newheight );
    
        // Copy and resize old image into new image.
        imagecopyresampled( $tmpimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height );
    
        // Save thumbnail into a file.
        imagejpeg( $tmpimg, $endfile, $quality);
    
        // release the memory
        imagedestroy($tmpimg);
        imagedestroy($img);
    
  • JasonDavis
    JasonDavis almost 15 years
    Do you know what you would put for $dst_x, $dst_y, $src_x, $src_y ?
  • Walf
    Walf about 13 years
    Shouldn't you replace $quality + 1 with ($quality + 1)? As it is, you're just resizing with a useless extra pixel. Where's the check to short circuit when $dst_w * $quality is > $src_w?
  • Andomar
    Andomar about 13 years
    Copy/pasted from suggested edit: This is Tim Eckel, the author of this function. The $quality + 1 is correct, it's used to avoid a one pixel wide black border, not change the quality. Also, this function is plug-in compatible with imagecopyresampled, so for questions on syntax, see the imagecopyresampled command, it's identical.
  • Tomas
    Tomas almost 13 years
    but this is not part if standard PHP as it seems... so it won't be avaliable on most hostings :(
  • Tomas
    Tomas almost 13 years
    I don't see any reason for point 3 - use GD for medium sized. Why not to use ImageMagick for them too? That would simplify code a lot.
  • Tomas
    Tomas almost 13 years
    how is this solution better than the one proposed in the question? you are still using the GD library with the same functions.
  • Flo
    Flo over 12 years
    looks to me like it is only a php script you only have to have php gd and imagemagick
  • Xeoncross
    Xeoncross about 12 years
    @Tomas, according to Tim Eckel it's how the GD functions are being used in this sample that makes the difference.
  • 0b10011
    0b10011 almost 12 years
    @Tomas, actually, it's using imagecopyresized() as well. Basically, it's resizing the image to a manageable size first (final dimensions multiplied by quality), then resampling it, rather than simply resampling the full-size image. It can result in a lower quality final image, but it uses far less resources for larger images than imagecopyresampled() alone as the resampling algorithm only has to deal with an image the size of 3x the final dimensions by default, compared to the full-size image (which may be far larger, especially for photos being resized for thumbnails).
  • ColinM
    ColinM almost 12 years
    Much better than cron would be a script that uses inotifywait so that the resizing will begin instantly instead of waiting for the cron job to start.
  • w5m
    w5m over 11 years
    It is indeed a PHP script rather than an extension that you have to install, so is good for shared hosting environments. I was running into an "Allowed memory size of N bytes exhausted" error when trying to upload jpeg images < 1MB with dimensions of 4000x3000. Using phpThumb (and thereby ImageMagick) solved the issue and was very easy to incorporate into my code.
  • Abhi Beckert
    Abhi Beckert almost 11 years
    On servers I've worked with, GD often runs out of RAM and crashes, while ImageMagick never does.
  • Alvaro
    Alvaro about 8 years
    resulting in black images whenever I use quality 3 on big images.
  • Single Entity
    Single Entity over 7 years
    I can't disagree more. I find imagemagick a nightmare to work with. I get 500 server errors for large images frequently. Admittedly the GD library would crash earlier. But still, we're only talking 6Mb images sometimes, and 500 errors are just the worst.
  • Single Entity
    Single Entity over 7 years
    It always returns a black image for me, tested on images >3Mb only