Monday, 27 January 2014

Sizing images for a responsive website, oh and retina too.

One of the issues with responsive sites is mixing the fluid width elements with fixed sized ones. I came across this problem recently, while adding a news feature (containing images) to a responsive site.

Here is my attempt at a no-compromises approach, where the image would always look great, regardless of the size of the screen.

Basic Implementation

Here's a summary of the basic idea:

The Server Resizes Images on the fly
This is key to making it work, and is not so frightening as I thought. As I'm currently in the .NET world, I came across a library called ImageResizer which is intended to do exactly this. The library claims to be almost as fast as the underlying GDI+ graphics operations, and can spit out your image in any shape or size you want, with nice cropping options.

Here's the basic idea. Create a service / web form which generates the image on a fly by resizing it and sending the result through the HTTP response. Here's an example in C#, but the principle is the same in Java.

var filename = Request("filename");
        var resizeSettings = ResizeSettings() {
            MaxWidth = Request("width"),
            MaxHeight = 261,
            Mode = FitMode.Crop
        };
        Response.ContentType = "image/jpeg";
        Response.Cache.SetCacheability(HttpCacheability.Public);
        Response.Cache.SetMaxAge(New TimeSpan(7, 0, 0, 0));
        GetImageService().GenerateThumbnails(filename, resizeSettings, Response.OutputStream);
        Response.Flush();

GetImageService() simply wraps image resizer and maps the filename to the image's actual location.

The Client decides the width of the image
The client needs to ask for an image in the right size. The image filename is generated dynamically by the Javascript. When the article model loads, the image filename is also returned. The client then adds parameters asking for the correct width.

I'm using Knockout JS on the front end, so its easy to encapsulate all this within a custom binding. Here's a simple example:

ko.bindingHandlers.dynamicImage = {
        init: function () {

        },
        update: function (element, valueAccessor) {
            var filename = ko.utils.unwrapObservable(valueAccessor()),
                width = Math.min(jQuery("main").width() || 600),
                height = defaultHeight,
                src = "article-image.aspx?" + jQuery.param({
                    filename: filename,
                    width: width,
                    height: height
                });

            jQuery(element)
                .css("background-image", "url(" + src + ")")
        }
    };
The knockout markup looks a bit like this:

<figure>
  <div class="article-image" data-bind="attr: { title: caption }, dynamicImage: filename">
    <span class="attribution" data-bind="text: attribution"></span>
  </div>
  <figcaption data-bind="text: caption"></figcaption>
</figure>


I'm using a background image within a <div>, rather than an <img> tag for a reason, see notes at the end.




A little CSS ensures the image appears as a correctly sized placeholder while the image loads.

.article-image {
    padding-top: 50%; /* 2:1 ratio */
    width: 100%;
    background-color: $image-placeholder-background-color;
}

The no-repeat will stop the image tiling if the user maximises or otherwise increases the size of their browser window.

News article on a desktop browser

On mobile devices

Enhancements

Supporting High Pixel Density "Retina" Screens
Server
It's not just iPhones anymore. Most android have high density screens to varying degrees, and my lovely Mac Pro has one too, so it seems worthwhile to get this right. Fortunately the generate-on-the-fly approach makes it very easy.

I modified the server script to accept another parameter "retina". If this is set to "true" then the requested width and height are doubled.

if (retina) {
   width *= 2
   height *= 2
   quality = 50
}

In addition, as people have noticed, you can also dramatically ramp down the JPEG quality setting on retina images. In my script it takes it down to 50, although you can probably go lower. This keeps the retina images on a par (or sometimes even smaller than) the 1:1 image sizes.

Front End
The image filename builder needs to send the retina request. It needs to know if it is a retina device. You can do this in various ways using Modernizr queries, but since the image change is fairly harmless in modern browsers, I think you can get away with:

var retina = window.devicePixelRatio && window.devicePixelRatio > 1;
...
src = "article-image.aspx?" + jQuery.param({
                    filename: filename,
                    width: width,
                    height: height,
                    retina: retina
                });

jQuery(element)
                .css("background-image", "url(" + src + ")")
                .css("background-size", width + "px " + height + "px");

Then it's just a case of ensuring the background size is maintained with an additional CSS property.

The advantage of this approach is that the browser only needs to download a single image.

Giving the server a break
ImageResizer comes with various options for caching generated images, although you could roll your own too. Asking for images at arbitrary widths, however, may reduce cache hits though. Here are some suggestions:

  • Give your article a max width. I went for 600px, which in any case is about as wide as an article can be before readability starts to suffer. This limits the number of sizes that can be requested, and also allows you to resize images to 1200px upon upload, as this is the largest (retina) size that can be requested.
  • Round up your width requests to the nearest 50px, reducing the number of possible size requests. By placing your image inside a div with background-image, the browser will automatically crop and center the image if it is a little larger.



No comments:

Post a Comment