Responsive Images Using Picturefill and PHP

Lukas White
Share

One of the key challenges with responsive web design, and a subject of much discussion in recent years, is how to deal with images. Setting a max-width on image elements enables designers to allow their size to adapt to the page dimensions, but in itself that approach can lead to far bigger images being downloaded than are required.

One solution to this is “source sets” – referencing separate image files at varying sizes (and by extension, file sizes) to be requested and displayed at different resolutions. Indeed, the srcset attribute is being implemented by Webkit. You can use a similar approach straight away and in a cross-browser compatible manner by using Javascript; one such method is the Picturefill plugin.

In essence, Picturefill allows you to specify different src attributes for an image, each image file corresponding to a different media query. Thus a large image will be fetched if – and only if – the screen size requires it, and likewise a mobile-optimised version of an image will be fetched and displayed as appropriate.

This approach requires some more effort, however – the images themselves need to be created at the appropriate sizes. That's the focus of this article.

What we'll Create

I'm going to demonstrate a simple application for generating responsive images, or image derivatives – i.e. different versions / sizes – of images, on demand.

In essence, we're going to “hijack” requests for a specific version of an image when the requested file doesn't exist. When it doesn't we'll create it, and save it to the file system for subsequent requests.

Getting Started

I'm basing this example on SlimBootstrap, which is a skeleton application which uses the Slim Framework. Alternatively, you can clone or download the complete code for this tutorial from Github.

Once you've cloned or downloaded the skeleton application, there are a few more steps before beginning coding.

In this example I'm using ImageMagick, so you'll need to ensure that it's installed along with the Imagick PHP extension – refer to the installation instructions for details according to your operating system. If you prefer, you could always rewrite the example using GD or a library such as Imagine.

Then you'll need to download the Picturefill library. I've placed the two relevant files – matchmedia.js and picturefill.js – in public/js/lib.

Create the configuration file config/config.php – there's a skeleton example in the same folder – and add the following:

'images.dir' => $basedir . 'public/img/',
'logs.dir' => $basedir . 'logs/'

Finally, ensure that the images directory is both writeable by the web server, and it can create sub-directories – chmod 775 should do the trick. It is recommended you do the same for the logs directory – so that if you do run into any errors, they get conveniently printed there.

Using Picturefill

Picturefill takes a “source set” referring to different versions of an image, and selects which one to download and display using media queries. There is a simple demo on Github.

To use it, you need to create a <span> element with the following structure:

<span data-picture data-alt="Image description goes here">
    <span data-src="img/small.jpg"></span>
    <span data-src="img/medium.jpg"     data-media="(min-width: 400px)"></span>
    <span data-src="img/large.jpg"      data-media="(min-width: 800px)"></span>
    <span data-src="img/extralarge.jpg" data-media="(min-width: 1000px)"></span>

    <!-- Fallback content for non-JS browsers. Same img src as the initial, unqualified source element. -->
    <noscript>
        <img src="img/small.jpg" alt="Image description goes here">
    </noscript>
</span>

The data-src attribute contains the URL to each image derivative, and the data-media attribute accepts any valid media query, for example min- or max- width, or min-resolution.

Image Paths and Routing

Here's an example of the sort of path we're going to provide:

/img/large/uploads/blog/photo.jpg

Breaking that down;

img is the images directory

large is the derivative; in the following example this
could also be small, medium or extralarge

uploads/path is a sub-directory – two-deep, in this example – for organising the images

test.jpg is the filename

If the file referenced by this path exists then the browser will just fetch it, bypassing any routes we have set up. But it's that route that we'll need to define, in order to intercept requests for derivatives that don't yet exist.

We can match all such requests using the following pattern:

$app->get(
    '/img/:parts+',
    function ($parts) use ($app, $c) {

The path above results in the $parts variable being populated as follows:

array(4) {
      [0]=>
          string(5) "large"
      [1]=>
          string(7) "uploads"
      [2]=>
          string(4) "blog"
      [3]=>
          string(9) "photo.jpg"
}

Extracting the relevant parts is easy, by whittling away at this array:

// Grab the filename
$filename = array_pop($parts);

// Grab the derivative
$derivative = array_shift($parts);

// Get the path
$path = implode("/", $parts);

Because we've taken the derivative and filename out of the array, it doesn't matter how many levels deep the file is, as we just implode the remaining array to create the path.

The next step is to check that the destination directory exists – and if not, create it.

// assemble the destination path
$destination_path = $c['config']['images.dir'] .     $derivative . '/' . $path;

// Create the directory, if required
if (!file_exists($destination_path)) {
    mkdir($destination_path, 0777, true);
}

The third argument to mkdir is important; it's going to be nexcessary to recursively create directories.
Configuring the Derivatives

Let's create a flexible configuration which for each derivative, defines the name (which will form part of the URL), the appropriate size, and the corresponding media query.

Here's an example in JSON:

{
    "jpeg_compression": 80,
    "sizes": {
        "small" : {            
            "width" : 180            
        },
        "medium" : {            
            "width" : 375,
            "query" : "(min-width: 400px)"
        },
        "large" : {            
            "width" : 480,
            "query" : "(min-width: 800px)"
        },
        "extralarge" : {            
            "width" : 768,
            "query" : "(min-width: 1000px)"
        }
    }
}

In this example I've only defined widths, but the resize function I'm going to use allows you to define either the width or the height – or both. It also provides cropping functions, but for simplicity I'll just use scaleImage – see the documentation for details.

Next Steps

The next step is to locate the file to process, load the configuration and then resize the image.

// Now get the source path
$source_path = $c['config']['images.dir'] . $path;

// grab the config
$config = json_decode(file_get_contents('../config/images.json'), true);

// get the specs from the config
$specs = $config['sizes'][$derivative];

// Create a new Imagick object
$image = new Imagick();

// Ping the image
$image->pingImage($source_path . '/' . $filename);

// Read the image
$image->readImage($source_path . '/' . $filename);

// Resize, by width & height OR width OR height, depending what's configured
$image->scaleImage(
    (isset($specs['width'])) ? $specs['width'] : 0,
    (isset($specs['height'])) ? $specs['height'] : 0
);

Note that the scaleImage function can accept a width, height, or both.

Now that we've resized the image appropriately, we need to do two things.

First, we need to save the image to the appropriate location, which means that any subsequent request will just grab the image itself – bypassing our resizing function.

Second, we still need to honour the original request which means outputting the image, setting the appropriate headers along the way.

// save the file, for future requests
$image->writeImage($destination_path . '/' . $filename);

// set the headers; first, getting the Mime type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $destination_path . '/' . $filename);
$app->response->headers->set('Content-Type', $mime_type);

// Get the file extension, so we know how to output the file
$path_info = pathinfo($filename);
$ext = $path_info['extension'];

// output the image
echo $image;

// Free up the image resources
$image->destroy();

That's all there is to it! I've cut a few corners for clarity, as there are a few things you'd need to think about in production:

  • sanity checking the configuration
  • ensuring that the files are indeed images
  • better error handling, generating an error image in certain circumstances
  • track any changes in the image derivative configuration, and regenerate the images as appropriate

Outputting Images

I've deliberately included the media queries in the server-side configuration; because Picturefill requires quite a lot of HTML to operate, generating it is probably a good candidate for a view helper. That's outside the scope of this article, but let me know in the comments or submit a pull request at Github if you come up with something suitable.

Summary

In this article I've described a possible solution to the issue of adaptive images by combining server-side, on-demand creation of image derivatives designed to be used with the Picturefill Javascript library. In time, a third-party Javascript library may well become redundant as standards such as srcset evolve, but the need for server-side solutions is likely to remain.

Of course there's no need to just use this code for using Picturefill or similar – it could also be used in, say, a content management system to generate different versions of an image for different displays. Can you think of anything else?