Image Resizing, Tinting and Caching

Despite the fact that PHP allows for all sort of image processing, not many people concern themselves with two important aspects of doing that sort of thing online: The first is simply caching the results, and the second is ensuring caching actually works from an HTTP standpoint.

Hashes for Caches

My technique for dealing with caching revolves around the md5 function (manual), which creates a virtually unique hexadecimal hash for anything you throw at it. So if I'm writing a function to process an image and cache the results, the first thing I do is:

<?
define( CACHE_PREFIX, '/tmp' ); // use a real directory, not this

function resizeImage( $szFilename,                // original file
                      $nMaxWidth, $nMaxHeight,
                      $bBranded = false,          // add copyright?
                      $nBorderWidth = 0,
                      $szBorderColor = "#ffffff", // obvious
                      $nAlpha = 0,
                      $szBlendColor = "") {       // tint it with some color
  $aParams = func_get_args(); // this has to be copied to a variable first
  $szCached = CACHE_PREFIX . md5(join("",$aParams)) . ".jpg";
?>

That way, every function invocation with different parameters will result in a different cache file (like /tmp/7b922d1c5de1fc5a3b20c6075d73b36a.jpg). For very large caches with tens of thousands of entries, you might want to split the above into sub-directories (/tmp/7b/92/2d1c5de1fc5a3b20c6075d73b36a.jpg), but that is not often necessary.

Basic Caching

Now let's assume that you already have a cached file. The right way to deal with that is:

  • Verify if the HTTP request is an explicit GET or a conditional If-Modified-Since request
  • If so, check if we can get away with just sending a 304 Not Modified reply and ending the script (freeing the CPU to do more useful stuff)
  • If not, pipe the file to the client (or send out a Location header for the client to redirect to it).

I prefer piping the file directly to the client for short files (like a 1K thumbnail image), since it cuts down on latency (if you redirect the client to the file, the client has to tear down the TCP connection to your server, perform another HTTP request (probably causing your server to spawn another thread to serve the request), etc.

Unless it's using HTTP 1.1, of course - but since I'm used to dealing with PDA and mobile phone browsers, I'd rather not trust the client's abilities much.

The code snippet for the above is:

<?
  if( file_exists( $szCached ) ) {
    @clearstatcache();
    $nModified = filemtime( $szCached );
    $nRequested = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    header("Last-Modified: " . gmdate("D, d M Y H:i:s",$nModified) . " GMT");
    if( $nModified == $nRequested ) {
      header( $_SERVER['SERVER_PROTOCOL'] . " 304 Not Modified");
      exit();
    }
    header( "Content-type: image/jpeg" );
    $hFile = fopen( $szCached, "r" );
    fpassthru( $hFile );
    fclose( $hFile );
    return;
 }
 // implicit else
  header("HTTP/1.0 404 File Not Found");
  echo( "No such image" );
  exit();
?>

(comparing actual cached file time with the original is missing here)

Doing Something With Images

Take the function header for resizeImage above. It obviously does a fair bit more than that. And how? Thanks to the GD library that comes bundled with PHP (or that you can compile and tweak for it), you have an entire range of options available.

But first, a little helper function to convert hexadecimal colors to individual RGB values:

<?
function hex2dec($hex) {
  $color = str_replace('#', '', $hex);
  $ret = array( 'r' => hexdec(substr($color, 0, 2)),
                'g' => hexdec(substr($color, 2, 2)),
                'b' => hexdec(substr($color, 4, 2)));
  return $ret;
}
?>

Scaling

Of course, the prudent course of action is to check it actually exists first:

<?
  if( file_exists( $szFilename ) ) {
    $hImage = imagecreatefromjpeg( $szFilename );
    $nImageWidth  = imagesx( $hImage );
    $nImageHeight = imagesy( $hImage );
    if( $nImageWidth > $nImageHeight ) {
      $nNewWidth  = $nMaxWidth;
      $nNewHeight = $nImageHeight / ($nImageWidth/$nMaxWidth);
    }
    else {
      $nNewHeight = $nMaxHeight;
      $nNewWidth  = $nImageWidth / ($nImageHeight/$nMaxHeight);
    }
    $hNewImage = imagecreatetruecolor( $nNewWidth, $nNewHeight );
?>

Adding a Border

<?
    if( $nBorderWidth ) {
      $aBorder = hex2dec( $szBorderColor );
      $hBorder = imagecolorallocate( $hNewImage,
                                     $aBorder['r'],
                                     $aBorder['g'],
                                     $aBorder['b'] );
      imagefilledrectangle( $hNewImage, 0, 0,
                            $nNewWidth, $nNewHeight, $hBorder );
    }
    imagecopyresampled( $hNewImage, $hImage,
                        $nBorderWidth, $nBorderWidth, 0, 0,
                        $nNewWidth - $nBorderWidth * 2,
                        $nNewHeight - $nBorderWidth * 2,
                        $nImageWidth, $nImageHeight );
?>

Tinting

<?
    if( $nAlpha ) {
      $aBlend = hex2dec( $szBlendColor );
      imagealphablending( $hNewImage, true );
      $hBlend = imagecolorallocatealpha( $hNewImage,
                                         $aBlend['r'],
                                         $aBlend['g'],
                                         $aBlend['b'],
                                         $nAlpha );
      imagefilledrectangle( $hNewImage, 0, 0,
                            $nNewWidth, $nNewHeight, $hBlend );
    }
?>

Adding a Copyright Overlay

<?
    if( $bBranded ) {
      $hLogo = imagecreatefrompng( LOGO_IMAGE );
      $nLogoWidth  = imagesx( $hLogo );
      $nLogoHeight = imagesy( $hLogo );
      imagealphablending( $hNewImage, true );
      imagecopy( $hNewImage, $hLogo,
                 $nBorderWidth,
                 $nNewHeight - $nBorderWidth - $nLogoHeight,
                 0, 0, $nLogoWidth, $nLogoHeight );
    }
?>

Setting cache headers

<?
    imagejpeg( $hNewImage, $szCached, 95 );
    imagedestroy( $hNewImage );
    imagedestroy( $hImage );
    $nModified = filemtime( $szCached );
    header( "Last-Modified: " .
            gmdate("D, d M Y H:i:s",$nModified) . " GMT");
    header( "Content-type: image/jpeg" );
    $hFile = fopen( $szCached, "r" );
    fpassthru( $hFile );
    fclose( $hFile );
  }
  header("HTTP/1.0 404 File Not Found");
  echo( "No such image" );
} // resizeImage
?>

Further Reading