1 Comment on
Getting the browser to cache your dynamically generated images
Getting the browser to cache your dynamically generated images
When building a web application you sometimes need to dynamically generate the image. In my case I’m not using HTTP authentication so I have to secure the images another way. I tucked the images outside of the document root and pull them in through a php script as needed. At it’s simplest your php script merely sets the Content-Type, Content-Length and dumps the image.
$fh=@fopen('path/to/image.jpg','rb');
if ($fh) {
header('Content-Type: image/jpeg');
$s_arr = fstat($fh);
header('Content-Length: '.$s_arr['size']);
fpassthru($fh);
}
This is all well and good except that php is adding all sorts of headers to prevent the browser from caching. By overriding php’s headers and adding some of our own we get a much more intelligent solution that allows the browser to cache images locally if they have not changed. The first thing we need to do is generate an Entity Tag or Etag header.
Etags are unique way of identifying a file on your web server. Typically apache will generate etags for any static content like images or html files. It does not generate etags for php pages since ordinarily they shouldn’t be cached. So we have to make our own entity tag. It can be any sequence of numbers and letters as long as it can be used to uniquely identify this item on this server. In this case I’m using a database that can guarantee the id assigned to this image is unique.
Etags are not enough because this means the browser will cache the image as long as the etag is valid. It won’t necessarily detect a new image if it has the same etag. You could incorporate the file’s modification time into your etag generation so it changes when the file is changed or you can add a Last-Modified header. The HTTP spec, RFC2616, says you should send a Last-Modified header. In this case I generate a Last-Modified header from the file’s last modified inode information.
With both an etag and a last-modified date assigned to the image the browser can make intelligent decisions about whether to retrieve the image or not. The following php code excerpt gives you an idea of how this works.
// Grab all the HTTP headers since If-None-Match and If-Modified-Since are not grabbed by the globals
$headers = apache_request_headers();
// Check the If-None-Match and If-Modified-Since headers
if ((strpos($headers['If-None-Match'], "asset-{$this->assetId}")) &&
(gmstrftime("%a, %d %b %Y %T %Z",$this->assetDetail['lastUpdate']) ==
$headers['If-Modified-Since'])){
// They already have the most up to date copy of the image so tell them
header('HTTP/1.1 304 Not Modified');
header("Cache-Control: private");
// Turn off the no-cache pragma, expires and content-type header
header("Pragma: ");
header("Expires: ");
header("Content-Type: ");
// The Etag must be enclosed with double quotes
header('ETag: "asset-'.$this->assetId.'"');
exit;
} else {
// They need a new copy of the image so open it up
$fh=fopen($this->assetDetail[$path], 'rb');
// Set the content-type to something like image/jpeg and set the length
header("Content-Type: ".$this->assetDetail['mimeType']);
header("Content-Length: ".filesize($this->assetDetail[$path]));
// Change php's default caching mechanisms
header("Cache-Control: private");
header("Pragma: ");
header("Expires: ");
// Send the browser the last modified date and etag so they can cache it
header("Last-Modified: ".gmstrftime("%a, %d %b %Y %T %Z",$this->assetDetail['lastUpdate']));
header('ETag: "asset-'.$this->assetId.'"');
// Dump all the image data back to the browser
fpassthru($fh);
}
voicon said:
Thank you so much for this helpful article!
This help me much on relative problem.
:: 19 May 2008 at 3:26 pm ::