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); }
Thank you so much for this helpful article!
This help me much on relative problem.
Yes, thanks for the info. I was having to deal with this when I was working in the Codeigniter framework at my work. I knew what was going on, but the modified stamp alone didn’t quite work if I remember correctly… the ETag thing I never knew about. I wish I had found this info sooner, I had to abandon my dynamic/selectable theme idea for the project. Sucks because it worked, but our pipe was tight at the time, so more cachable the better. Maybe next time. lol. Thanks again for the example.
Thanks…Saved a lot of my time of going through all those RFC’s detail.
Thank you sir, thank you