Stop dreading Retina image support

This method was inpired by Estelle Weyl's Clown Car Technique for Responsive Images, though it uses Javascript instead of CSS media queries and allows for multiple images to be conveniently referenced in a single SVG. I was also inspired by @simurai and Erik Dahlstrom's work on SVG stacks.

How it works:

For this method, we make an 'images.svg' file containing named references to each of the site's images, as well as a viewBox setting describing their dimensions. For the purpose of this example, the images must be in the same directory as the images.svg file, and the filenames must match these image reference names, plus '-1x.png' and '-2x.png', or '-1x.jpg' and '-2x.jpg' for the regular and double-resolution versions. Here's what the svg looks like:

<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink" onload="generateImage()">
  <title>SVG Retina Image Demo</title>
  <desc>
    By @tpenzer - http://thepenzone.com/svg-image/
  </desc>
  <script type="application/ecmascript"> <![CDATA[
    function generateImage() {
      // parse the requested image ID from the location hash, get its container element
      var image_id = location.href.substring(location.href.indexOf('#') + 1);
      var container = document.getElementById(image_id);

      // for Retina displays, set 2x filename resolution key, else 1x
      if (window.devicePixelRatio > 1) {
        var filename_res_key = "-2x"
      } else {
        var filename_res_key = "-1x"
      };

      // if image container has a class set, and has class 'jpg', make it use a .jpg filename extension, else .png
      if ((container.getAttributeNS(null, 'class')) && container.getAttributeNS(null, 'class').indexOf('jpg') != "-1") {
        var filename_ext = ".jpg"
      } else {
        var filename_ext = ".png"
      };

      // split the viewBox string at every space, yielding an array of x, y, width and height values
      var view_box = container.getAttributeNS(null, 'viewBox').split(/s+/g);

      // create a new image element and set its attributes
      var image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
      image.setAttributeNS(null, "x", view_box[0]);
      image.setAttributeNS(null, "y", view_box[1]);
      image.setAttributeNS(null, "width", view_box[2]);
      image.setAttributeNS(null, "height", view_box[3]);
      image.setAttributeNS('http://www.w3.org/1999/xlink', "href", image_id + filename_res_key + filename_ext);

      //add the new image element to the container svg
      container.appendChild(image);
      }
  ]]> </script>
  <svg id="logo" viewBox="0 0 149 74"></svg>
  <svg id="promo" viewBox="0 0 200 150"></svg>
  <svg id="bkg" class="jpg" viewBox="0 0 960 720"></svg>
</svg>

There are three image references in the SVG, "logo", "promo", and "bkg", including viewBox values describing their dimensions, highlighted in green at the bottom. Note that "bkg" is a JPEG, and so it has an additional "jpg" class (png is the default, and doesn't need to be specified). The viewBox should be set to "0 0 width height", with the width and height pixel values for the 1x version of the image. There are images with filenames "logo-1x.png", "logo-2x.png", "promo-1x.png", "promo-2x.png", "bkg-1x.jpg" and "bkg-2x.jpg" in the same directory as the "images.svg" file. The javascript code will use the ID names, class name (.jpg images have class 'jpg'), filename keys (i.e. "-1x" and "-2x") and viewBox setting to create SVG image elements upon loading, generating a path to the correct .png or .jpg image based on these names combined with the detected pixel ratio. The parts highlighted in green are the only bits that should need modification for various uses. These images can then be rendered as such:

<div id="bkg" style="position:fixed;top:-50%;left:-50%;width:100%;height:100%">
  <object type="image/svg+xml" width="200%" height="200%" data="images/images.svg#bkg">
    <!--<img src="images/bkg-1x.jpg" width="200%" height="200%" />-->
  </object>
</div>

<div class="image-wrapper" style="width:149px;height:74px">
    <object type="image/svg+xml" width="100%" height="100%" data="images/images.svg#logo">
        <!--<img src="images/logo-1x.png" width="100%" height="100%" />-->
    </object>
</div>

<div class="image-wrapper" style="width:200px;height:150px">
    <object type="image/svg+xml" width="100%" height="100%" data="images/images.svg#promo">
        <!--<img src="images/promo-1x.png" width="100%" height="100%" />-->
    </object>
</div>

What's going on here?

SVG 'image' elements with references to the appropriate resolution png images are generated upon loading the svg image, so that only a single request is made for the desired image resource. In order to avoid various rendering issues, it's best to wrap the 'object' element in a 'div' with the dimensions of the image, and set the object to 100% width and height. The inline styling for the background '#bkg' image is a total hack to compensate for the inability to specify an SVG fragment ID like 'images.svg#bkg' for a CSS background-image; it's included for demonstration purposes only, and should probably not be used in production. Safari 7 and Safari for iOS 7 in particular are unamused by these shenanigans.

I included commented-out "img" elements inside the "object" elements to show how you would extend support to older browsers in exchange for an additional image request to your server. The browser will send multiple requests for the "images.svg" file, which is tiny, but still wastes resources. You can make browsers cache this file to minimize requests by adding a "manifest" attribute to your page's html element, as such:

<html manifest="myCache.appcache">

Then, you can add a file to the site directory, in this example named "myCache.appcache", with the following contents:

CACHE MANIFEST
/path/to/images.svg

NETWORK:
*

This will force browsers to cache the svg file locally, and request it from their local cache rather than your server. Note that this will cause Firefox to prompt users on whether they'd like your site to be able to store data on their computer.

So what are the pitfalls?

Note that you can easily remove the javascript requirement if you don't mind your Retina users making two image requests, by including image elements referencing their respective 1x assets in the images.svg file by default, using a bit of CSS to hide all but the :target image container, and then modifying their href attributes for Retina users with javascript, like so:

<?xml version="1.0" encoding="utf-8"?>
<svg id="images" class="image" version="1.1" xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink" onload="setImageHiRes()">
  <title>SVG Retina Image Demo</title>
  <desc>
    By @tpenzer - http://thepenzone.com/svg-image/
  </desc>
  <style>
    svg .image { display: none }
    svg .image:target { display: inherit }
  </style>
  <script type="application/ecmascript"> <![CDATA[
    function setImageHiRes() {
      if (window.devicePixelRatio > 1) {
        // parse the requested image ID from the location hash, get its image element
        var image_id = location.href.substring(location.href.indexOf('#') + 1);
        var container = document.getElementById(image_id);
        var image = container.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'image')[0];

        // if image container has a class of 'jpg', make it use a .jpg filename extension, else .png
        if (container.getAttributeNS(null, 'class').indexOf('jpg') != "-1") {
          var filename_ext = ".jpg"
        } else {
          var filename_ext = ".png"
        };

        // set image href to {image_id} + {2x filename key}
        image.setAttributeNS('http://www.w3.org/1999/xlink', "href", image_id + '-2x' + filename_ext);
      };
    }
  ]]> </script>
  <svg id="logo" class="image" viewBox="0 0 149 74">
    <image xlink:href="logo-1x.png" x="0" y="0" width="149" height="74"/>
  </svg>
  <svg id="promo" class="image" viewBox="0 0 200 150">
    <image xlink:href="promo-1x.png" x="0" y="0" width="200" height="150"/>
  </svg>
  <svg id="bkg" class="image jpg" viewBox="0 0 960 720">
    <image xlink:href="bkg-1x.jpg" x="0" y="0" width="960" height="720"/>
  </svg>
</svg>

How do I start using this awesome thing?

Download the SVG file (or download the non-JS-dependent version), modify the parts highlighted in green to suit your needs, stick it in your images directory, and link to the svg instead of the png images.

Discuss on Hacker News