Introduction

A major mistake some people make when creating an automated thumbnail generator is that they point their thumbnail <img> elements directly at their thumbnail script. This works, but it forces every single thumbnail request to go through that PHP script which causes needless overhead.

A more efficient option is to actually create a "thumbnails" folder and setup a 404 error handler that will attempt to generate and cache thumbnails on the fly before actually sending a 404 status code. That way the very next and future requests will simply bypass all scripting and return the image as if it's always been there.

The code for this is actually very simple. The thumbnail generator and the code for setting a 404 error handler in htaccess is less than one KB.

Here's a snapshot of my filesystem for this setup.

/backgrounds/cat-one/181ab7662fd27121c232eefeb319d4a9.jpg
/backgrounds/cat-two/181ab7662fd27121c232eefeb319d4a9.jpg
/thumbnails/
/thumbnails/.htaccess
/thumbnails/thumbnail.php

Here's the contents of htaccess within the /thumbnails/ directory.

RewriteEngine off
 
ErrorDocument 404 /thumbnails/thumbnail.php

And here is the PHP within /thumbnails/thumbnail.php

<?php
if(preg_match('#^/?thumbnails/([a-z0-9-]+)/([a-f0-9]{32})\.jpg$#i', $_SERVER['REQUEST_URI'], $img))
{
    $cat        = $img[1];
    $img        = $img[2];
    $img_path   = "../backgrounds/$cat/$img.jpg";
    
    if(file_exists($img_path))
    {
        if(!is_dir($cat))
        {
            mkdir($cat);
        }
        $thumb_path = "$cat/$img.jpg";
        
        $image    = imagecreatefromjpeg($img_path);
        $new_image  = imagecreatetruecolor(167, 250);
        imagecopyresampled($new_image, $image, 0, 0, 0, 0, 167, 250, 320, 480);
        imagedestroy($image);
        
        imagejpeg($new_image, $thumb_path);
 
        header('HTTP/1.1 200'); // override the 404 response
        header('Content-Type: image/jpeg');
        flush(); @ob_flush();
        imagejpeg($new_image);
        flush(); @ob_flush();
        imagedestroy($new_image);
        exit;
    }
}
?>

The htaccess Part

The htaccess part is pretty straight forward.

RewriteEngine off
ErrorDocument 404 /thumbnails/thumbnail.php

Basically the first line turns off mod_rewrite in this directory because it's not needed in this directory. It's not something that must be done, but if you're using it in your DocumentRoot and unless you plan on using mod_rewrite in here later on, it's a good idea to turn off like you would a light in a room nobody is in.

The second line tells Apache where to go if it can't find a file anywhere within the /thumbnails/ directory, or any sub-directories. In this case it's the thumbnail generator.

The Security Part

The script is wrapped in an IF statement that checks the syntax of the requested image address.

if(preg_match('#^/?thumbnails/([a-z0-9-]+)/([a-f0-9]{32})\.jpg$#i', $_SERVER['REQUEST_URI'], $img))
{
    //...
}

In my case I'm using a very strict format for my images so I'm able to use a very secure pattern with preg_match to be sure the script isn't vulernable to any kind of URI injection exploits. This part of the script you'll need to modify to fit your file naming scheme.

Does The File Exist

preg_match, when successfull, will provide me with clean input I can use to see in the image I'm creating a thumbnail for even exists.

    $cat        = $img[1];
    $img        = $img[2];
    $img_path   = "../backgrounds/$cat/$img.jpg";
    
    if(file_exists($img_path))
    {
        //...
    }

A New Subdirectory

Now that I know there is a fullsize image to create a thumbnail for, I can check to see if a subdirectory by that name exists within the /thumbnails/ folder and create it if it doesn't exist. After which I can create a relative thumbnail image path from the clean input.

        if(!is_dir($cat))
        {
            mkdir($cat);
        }
        $thumb_path = "$cat/$img.jpg";

Generate The Thumbnail

Once I'm sure the fullsize image exists, that there's a place to put a thumbnail, and I have a path/filename to save the thumbnail under I can actually get to generating the thumbnail.

        $image    = imagecreatefromjpeg($img_path);
        $new_image  = imagecreatetruecolor(167, 250);
        imagecopyresampled($new_image, $image, 0, 0, 0, 0, 167, 250, 320, 480);
        imagedestroy($image);

A lot of thumbnail scripts seem to store widths and heights in variables, but for this simple little script I just place the numbers since it's not hard to remember what is what in this situation.

If you remember that imagecreatetruecolor takes (width,height) you can easily tell where the width/height arguments in imagecopyresampled go and if you remember that thumbnails are generally smaller than fullsize images you can see where the original images width/height go.

Cache For Later

I'm not sure technically whether this is actually cacheing, or if it's just generating static content. I look at it as simply scripting automatic content creation, but I suppose you can call it whatever you want.

        imagejpeg($new_image, $thumb_path);

Easy enough, it just saves the image to the thumbnail pather generated earlier.
Note that the GD thumbnail image hasn't been destroyed yet, just the fullsized one has.
Reason being we still need to send a copy of the thumbnail to the browser and there's no sense in keeping the fullsized image around any longer.

Oh Yeah, You Were Looking For Something

Now that the thumbnail has been generated and saved where browsers can just go straight to it later on, it's time to send the image to the browser that made the request to trigger this script in the first place.

        header('HTTP/1.1 200');
        header('Content-Type: image/jpeg');
        flush(); @ob_flush();
        imagejpeg($new_image);
        flush(); @ob_flush();
        imagedestroy($new_image);
        exit;

In order to prevent the browser from bailing, we return an HTTP 200 status for the request. (thanks effim)
Then, since I'm using JPEG image thumbnails I send a simple image/jpeg content type.
Next I flush the headers out to the browser so the browser doesn't have to wait for the PHP buffer to fill up with image data before sending that header.
Then I use imagejpeg without a filename argument so that it will dump the image data out directly to the browser, after which I flush again then finally destroy the thumbnail images GD representation in memory.

Conclusion

Static content is always served faster than dynamically generated content. Any time you can serve static over dynamic it's good to do so, any time you can save yourself from having to generate that static content manually is just, well, good.

I actually include three more lines in the copies of this script I use live that load and copy a watermark onto thumbnails, but I'll leave figuring out how to do that up to you. 😁

This page was published on It was last revised on

0

4 Comments

  • Votes
  • Oldest
  • Latest
Commented
Updated

Actually, that's a very bad (though good intentioned) recommendation. HTTP status codes were put into place for a reason, and whenever possible the correct status code should be reported to the browser/client. When you report a 404, search engines will ignore the image, and I wouldn't be surprised if more than a few mobile browsers ditched the response (no need to load a 404 page for an image).

Instead, what you should do to make your suggestion viable is to use a rewrite rule that only rewrites to the generating script if the file does not exist.

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ thumbnails/thumbnail.php [NC,L]

To anybody not familiar, the lines each do as follows:

Condition: Request filename is a regular file with > 0 bytes, OR
Condition: Request filename is a symbolic link, OR
Condition: Request filename is a directory, THEN
Don't rewrite the request. Stop rewriting.
Rewrite all requests to /thumbnails/thumbnail.php

😉

add a comment
0
JO
184 4
Commented
Updated

I disagree.

That rewriterule would be evaluated for every single image request.

By catching the 404, and actually generating a static file during the request, the worst case scenario is that the first person to view the image gets a 404 response if they're on a browser that bails. Chances are they'll just refresh the page or image.

Every other visitor will get the static image without any rewriterule processing.

I've been using this technique for months without any problems at all. The primary audience in on Safari-iPhone/iPod.

add a comment
0
Commented
Updated

That rewrite rule is actually run for every single request to every single website that uses the Zend Framework MVC (that's a lot). Apache is very efficient at handling rewrites, and if the file exists only a single rewrite condition is evaluated, and that's 'does the file exist'. There's really not much of a performance hit. In fact, there's more of a performance hit for using .htaccess instead of putting the rewrite rules in the httpd.conf (in the proper place, of course) than there is from running the rewrite rules.

To me, semantics are pretty important. I'm not a die-hard, and I'll use an arbitrary div here or there when it helps styling significantly, but I try to create semantic systems whenever it's feasible. Sending a 404 request to a browser for a file that does exist is something that I feel is too sloppy and patched together, especially when a more elegant solution exists.

While it might not present any glaring real world problems, I don't see why you would opt to use the 404 over the rewrite solution when there isn't a performance hit.

add a comment
0
JO
184 4
Commented
Updated

I actually think using the rewrite is the sloppy method myself. Especially when something designed for handling 404 errors exits.
I'm going to stick with using a 404 handler for, handling 404 errors.

I am going to listen about the status code though, I've added a header() line to make sure the script returns a 200 status code with the image. 🙂

header('HTTP/1.1 200'); // override the 404 response
add a comment
0