Displaying Retina-Quality Images

Displaying Retina-Quality Images

The Client-Side Component

As I was brainstorming ideas for how to tackle this, I came up with a few different solutions that essentially work, but weren't perfect. They all had pros and cons, but deal breaker #1 for me was always that they didn't prevent the unnecessary loading of separate images. Deal breaker #2 was the need to rely on CSS3, which isn't a viable in all browsers (yet).

The goal was simple: How do I tell the server which image I want to serve with only one HTTP request. I want the image I want, so give it to me when I ask for it.

I thought, "Wouldn't it be great if I could define a custom HTTP header for the whole page, then have that header get sent with every image request?" Of course, we can't do that with current technology (outside of an XHR request, but that's a can of worms I'm not prepared to open for simple image loading).

Then it dawned on me. Cookies! Cookies get sent with every server request (as long as the request is being made to the same server) and then the server can use that information to determine which image it should serve. So, we just need to set that cookie before any images are requested to make sure it's available for each of those requests.

Here's what that looks like:

 
<script>
	function getDevicePixelRatio() { 
		if (typeof window.devicePixelRatio === 'undefined') {
			return 1;
		}
		return window.devicePixelRatio; 
	}
	// Use your own flavor of setCookie()
	setCookie('devicePixelRatio',getDevicePixelRatio()); 
</script>
 

This is key: We add this code in the of the document to be sure it's set and ready to go before the browser starts loading images. That means it should come before any CSS files, too. We now have a cookie named "devicePixelRatio" set & ready to send to the server for every request. Don't worry — the browser does this for you.

Next, when we use an image anywhere on our site, we always define its dimensions. This is the space that it occupies on the screen and it's essential for retina-quality images to work. Retina images are simply twice the size of a standard image, then squeezed down into the standard image's footprint. This is done the same 'ole way we all know and love:

 
/* Inline: */
<img src="/path/to/proxy.cfm?image=/path/to/images/logo.png" height="100" width="200">
 
/* CSS: */
#myimage {
	background-image: url(/path/to/proxy.cfm?image=/path/to/images/logo.png);
	height: 100px;
	width: 200px;
}
 

See that weird file named proxy.cfm? All images will now be routed through a server-side script to help process them. Don't worry about it just yet.

That's all there is to the client-side portion of this solution. Now, we turn our attention to how the server handles these requests.

The Server-Side Component

To make this work, the server needs to know how to handle the request. The cookie "devicePixelRatio" is sent with every request to the server, but we need a proxy script to read this cookie and then determine which version of the image to serve up. There are a lot of ways to make this part very robust and complicated, but for the scope of this post, we'll keep it very simple. Two possible outcomes: Retina or Standard.

Before we go any further, let's dive right into the code. We can pick it apart in a moment:

 
<!---
If the correct parameters aren't passed in,
we short-circuit the process
--->
<cfif not structKeyExists(url,"image")>
	The URL parameter "image" is required.
	<cfabort>
</cfif>
 
<!---
Set some variables to your liking.
--->
<cfset img = {}>
<cfset retinaSuffix = "_2x">
<cfset serverPath = getDirectoryFromPath(getCurrentTemplatePath())>
 
<!---
Break the image name apart to get the important pieces.
--->
<cfset img.standard = url.image>
<cfset img.ext = listLast(img.standard,".")>
<cfset img.retina = replace(img.standard,"." & img.ext,retinaSuffix & "." & img.ext)>
 
<!---
The path should reflect the server-side location of the files,
not the web-based path. Using getCurrentTemplatePath() above,
we're saying that the image path passed in is relative to
wherever the proxy file lives. If you put it directly into your
image directory, then the image URL parameter won't need a path at all.
--->
<cfset img.path = serverPath & img.standard>
 
<!---
Here's the important part. Determine whether or
not we should serve up the retina version of the
image. Also, check to make sure it exists.
--->
<cfif structKeyExists(cookie,"devicePixelRatio")>
	<cfif cookie.devicePixelRatio gt 1>
		<!--- Override the path, setting it to the retina version --->
		<cfset img.path = serverPath & img.retina>
	</cfif>
	<cfif not fileExists(img.path)>
		<!--- Fall back to standard version --->
		<cfset img.path = serverPath & img.standard>
	</cfif>
</cfif>
 
<cfcontent type="image/#img.ext#" file="#img.path#" reset="true">
 

Ok, there's a lot going on here. At the most basic level, we're simply checking the value of the cookie to determine which version of an image we want to serve. Beyond that, we add some error checking and filename manipulation to serve the proper image.

If the devicePixelRatio is 1, we use the Standard version of the image (the one which was passed on the URL query). If it's greater than one, we append the retinaSuffix to the filename and server up the Retina version.

At this point, I should probably point out that the Retina version of each image should already exist on the server, so create these upfront. You'll want them to be two times as large (dimensionally) as the standard version. If the standard version is 100x200, the retina version will be 200x400. There's nothing more complicated than that.

In my example earlier, "/path/to/proxy.cfm?image=/path/to/images/logo.png" would mean we have logo.png and logo_2x.png ready to go.

That's it! You're all set. Your visitors will now receive retina-quality images when their device can handle it, and standard images when it can't. All with a single HTTP request per image — the way it should be.

Summary

We've now set up all of the pieces we need:

  1. Client-side detection.
  2. Server-side proxy script.
  3. Created 2 sets of images: Standard & Retina.

You should be able to call a test file directly in the browser to make sure it's working correctly.

To recap the process:

  1. Determine client pixel ratio & set a cookie.
  2. Image requests are routed through proxy script.
  3. Cookie gets sent with each request automatically.
  4. Server-side script serves up correct version of the image, based on pixel ratio.
  5. Profit!

The idea is straightforward, but explaining it step-by-step can make it seem complicated. I hope that I haven't taken anything for granted. If anything is unclear, I encourage you to leave a comment and I'll do my best to help.

I would also love for other developer's to take this concept and rewrite the server-side portion in other languages, like PHP, Java, etc. I'll happily link to those solutions, so we can cover more websites.

Happy coding!

Pages: 1 2

About the Author

Rob has been in web development for over 10 years, 9 of which have been focused on being a ColdFusion Application Developer. Project Management, eCommerce Consulting, and Marketing Consulting are also in the quiver. If you like what I have to say, consider following me on Twitter or reading more about me here: About Rob O'Brien