Best practice for properly deliver images to retina display screens using Apache, PHP and CSS3

Best practice for properly deliver images to retina display screens using Apache, PHP and CSS3

A bit of history: Apple and Retina Display.

If you just want to go to the code, click here.

When Apple launched the iPhone 4, they introduced this new high pixel density display that they called “Retina Display” (first acquired from Sharp, later from Samsung and other providers), where each pixel size was 1/4 of an iPhone 3G display’s pixel.

To understand this clearly, ‘Pixel Density’ is nothing but the amount of pixels a display can fit in a fixed distance. Normally measured in Pixels Per Inch (PPI).

One of the things we noticed at first with the iPhone 4 was that some website’s images —at least at that very moment— were blurry while some others were really sharp. This happened because although the logical resolution of the iPhone 4 is 480 x 320 px, its physical resolution is 960 x 640 px. If you try to display an image that is 480 x 320 pixels in an iPhone 4 fullscreen, each image’s pixel will fit in 4 pixels square of the device’s display.

Of course other brands were not far behind and soon included high pixel density displays in new versions of their devices. Actually, now many other brands has devices with higher pixel density displays than Apple does. That’s why when technically talking about applications or website development, instead of talking about ‘Retina Display’, ‘Super LCD’ or ‘IPS’, we should talk about ‘High Pixel Density Display’.

In order to take advantage of this high pixel density display and show beautiful sharp images, we must use bigger images so each image pixel fits each physical pixel of the display. But doing this for every device is a waste of bandwidth (both for server and client) since we will be increasing each page load time on devices with lower pixel density displays in vain.

Getting to the point…

JavaScript gives us the ability to detect the device’s pixel ratio, which is the ratio between the device’s display logical and physical linear resolution. A device with a pixel ratio of 2 means that 2 physical linear pixels equals 1 logical linear pixel.

As a standard –before the file extension– we should add a suffix in the form ‘@[devicePixelRatio]x’ to the image filename depending on the device’s pixel ratio where it should be delivered to. Ie: having an image named ‘logo.png’ that we want to create a version to be delivered to device’s with a pixel ratio of 2, as a standard, the filename of this version should be ‘logo@2x.png’. If we want to create another version of the same file for device’s with a pixel ratio of 3, we should name this file ‘logo@3x.png’.

So here’s what I do…

<!-- place this between the head tags, before any css file link. -->
<script type="text/javascript">
eval('var dpr = Math.ceil(('+((window.devicePixelRatio === undefined) ? 1 : window.devicePixelRatio)+’)*100)/100;’);dpr=dpr>3?3:dpr;document.cookie=‘devicePixelRatio=‘+dpr+'; path=/‘;
</script>

Using the function window.devicePixelRatio in JavaScript we will detect the device’s pixel ratio. Since some device’s has a non-conventional pixel ratio like 1.5, we will convert any float number to its immediate next integer.

The trick is to set a cookie with this pixel ratio value, before any css file is called, and then use ModRewrite in Apache to deliver the right file.

In the JavaScript above I’m setting a cookie named ‘devicePixelRatio’. Later, using Apache ModRewrite, I detect this cookie and use a PHP file as a proxy to properly deliver the right file.

<IfModule mod_rewrite.c>
RewriteEngine On

# replace with the path of your project
RewriteBase /retina/

RewriteCond %{HTTP_COOKIE} devicePixelRatio=[2-9]
RewriteCond %{REQUEST_FILENAME} (.*/)?([^/]+)\.(gif|jpg|jpeg|png)$ [NC]
RewriteRule ^(.*)$ img-proxy.php [QSA,L]
</IfModule>

In the PHP file ‘img-proxy.php’ we have…

<?php
/**
 * Project: retina
 * Created by: Martín Rafael González.
 * http://www.devtin.io
 * Stalk me: @tin_r
 * Date: 02/2015
 */

ini_set('display_errors','0');

$extToMime = array(
    'gif' => 'image/gif',
    'jpg' => 'image/jpeg',
    'jpeg' => 'image/jpeg',
    'png' => 'image/png'
);

$dpr = $_COOKIE['devicePixelRatio'];

// get the absolute path of the requested file
$fileReq = $_SERVER['DOCUMENT_ROOT'].$_SERVER['REQUEST_URI'];

// split into [path][filename] / [extension name]: $m[1] = [path][filename] | $m[2] = [extension name]
@preg_match('/^(.+)\.([a-z]{3,4})$/i',$fileReq,$m);

// check if the max resolution file exists... otherwise look for the most immediate lower resolution
while (!file_exists($m[1].'@'.$dpr.'x.'.$m[2]) && $dpr > 2)
    $dpr--;

// if a file with high resolution doesn't exists, roll back to the original
$fileReq = file_exists($m[1].'@'.$dpr.'x.'.$m[2]) ? $m[1].'@'.$dpr.'x.'.$m[2] : $fileReq;

$lmt = filemtime($fileReq);;
$etag = md5_file($fileReq);

@header('Content-Type: '.$extToMime[strtolower($m[2])]);
@header('Etag: '.$etag);

// avoid the file from being pulled during every request, instead use the client's cache...
if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lmt || trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
    @header("HTTP/1.1 304 Not Modified");
    exit;
}

// deliver the file
readfile($fileReq);

Therefor, calling the image’s original filename in our css or straight up in an img element, will return us the appropriate file for the device’s density. When using the image as a background, since we’re working with different dimensions image’s, we must set the CSS ‘background-size’ property to a fixed dimension, most of the time fixed to the size of the original file (remember to use ‘-moz-background-size’, ‘-webkit-background-size’ and ‘-o-background-size’ for cross browser compatibility) but that’s gonna depend on how you built the website and how it adapts to different dimension’s displays.

.logo { 
     display:block;
     position:relative;
     width:100px;
     height:100px;
     background:url(images/logo.png) center center no-repeat;
     background-size:100px 100px;
}

You can get a demo of the project by clicking below.

Download Project