Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Suppressing 404s in retina.js library

We use the js lib retina.js which swaps low quality images with "retina" images (size times 2). The problem is, that retina.js throws a 404 for every "retina" image which can't be found.

We own a site where users can upload their own pictures which are most likely not in a retina resolution.

Is there no way to prevent the js from throwing 404s?

If you don't know the lib. Here is the code throwing the 404:

http = new XMLHttpRequest;
http.open('HEAD', this.at_2x_path);
http.onreadystatechange = function() {
    if (http.readyState != 4) {
        return callback(false);
    }

    if (http.status >= 200 && http.status <= 399) {
        if (config.check_mime_type) {
            var type = http.getResponseHeader('Content-Type');
            if (type == null || !type.match(/^image/i)) {
                return callback(false);
            }
        }

        RetinaImagePath.confirmed_paths.push(that.at_2x_path);
        return callback(true);
    } else {
        return callback(false);
    }
}
http.send();
like image 504
j7nn7k Avatar asked Dec 04 '12 13:12

j7nn7k


5 Answers

There are a few options that I see, to mitigate this.

Enhance and persist retina.js' HTTP call results caching

For any given '2x' image that is set to swap out a '1x' version, retina.js first verifies the availability of the image via an XMLHttpRequest request. Paths with successful responses are cached in an array and the image is downloaded.

The following changes may improve efficiency:

  • Failed XMLHttpRequest verification attempts can be cached: Presently, a '2x' path verification attempt is skipped only if it has previously succeeded. Therefore, failed attempts can recur. In practice, this doesn't matter much beacuse the verification process happens when the page is initially loaded. But, if the results are persisted, keeping track of failures will prevent recurring 404 errors.

  • Persist '2x' path verification results in localStorage: During initialization, retina.js can check localStorage for a results cache. If one is found, the verification process for '2x' images that have already been encountered can be bypassed and the '2x' image can either be downloaded or skipped. Newly encounterd '2x' image paths can be verified and the results added to the cache. Theoretically, while localStorage is available, a 404 will occur only once for an image on a per-browser basis. This would apply across pages for any page on the domain.

Here is a quick workup. Expiration functionality would probably need to be added.

https://gist.github.com/4343101/revisions

Employ an HTTP redirect header

I must note that my grasp of "server-side" matters is spotty, at best. Please take this FWIW

Another option is for the server to respond with a redirect code for image requests that have the @2x characters and do not exist. See this related answer.

In particular:

If you redirect images and they're cacheable, you'd ideally set an HTTP Expires header (and the appropriate Cache-Control header) for a date in the distant future, so at least on subsequent visits to the page users won't have to go through the redirect again.

Employing the redirect response would get rid of the 404s and cause the browser to skip subsequent attempts to access '2x' image paths that do not exist.

retina.js can be made more selective

retinajs can be modified to exclude some images from consideration.

A pull request related to this: https://github.com/imulus/retinajs/commit/e7930be

Per the pull request, instead of finding <img> elements by tag name, a CSS selector can be used and this can be one of the retina.js' configurable options. A CSS selector can be created that will filter out user uploaded images (and other images for which a '2x' variant is expected not to exist).

Another possibility is to add a filter function to the configurable options. The function can be called on each matched <img> element; a return true would cause a '2x' variant to be downloaded and anything else would cause the <img> to be skipped.

The basic, default configuration would change from the current version to something like:

var config = {
  check_mime_type: true,
  retinaImgTagSelector: 'img',
  retinaImgFilterFunc: undefined
};

The Retina.init() function would change from the current version to something like:

Retina.init = function(context) {
  if (context == null) context = root;

  var existing_onload = context.onload || new Function;

  context.onload = function() {
    // uses new query selector
    var images = document.querySelectorAll(config.retinaImgTagSelector), 
        retinaImages = [], i, image, filter;

    // if there is a filter, check each image
    if (typeof config.retinaImgFilterFunc === 'function') {
      filter = config.retinaImgFilterFunc;
      for (i = 0; i < images.length; i++) {
        image = images[i];
        if (filter(image)) {
          retinaImages.push(new RetinaImage(image));
        }
      }
    } else {
      for (i = 0; i < images.length; i++) {
        image = images[i];
        retinaImages.push(new RetinaImage(image));
      }
    }
    existing_onload();
  }
};

To put it into practice, before window.onload fires, call:

window.Retina.configure({

  // use a class 'no-retina' to prevent retinajs
  // from checking for a retina version
  retinaImgTagSelector : 'img:not(.no-retina)',

  // or, assuming there is a data-owner attribute
  // which indicates the user that uploaded the image:
  // retinaImgTagSelector : 'img:not([data-owner])',

  // or set a filter function that will exclude images that have
  // the current user's id in their path, (assuming there is a
  // variable userId in the global scope)
  retinaImgFilterFunc: function(img) {
    return img.src.indexOf(window.userId) < 0;
  }
});

Update: Cleaned up and reorganized. Added the localStorage enhancement.

like image 126
tiffon Avatar answered Nov 01 '22 08:11

tiffon


Short answer: Its not possible using client-side JavaScript only

After browsing the code, and a little research, It appears to me that retina.js isn't really throwing the 404 errors.

What retina.js is actually doing is requesting a file and simply performing a check on whether or not it exists based on the error code. Which actually means it is asking the browser to check if the file exists. The browser is what gives you the 404 and there is no cross browser way to prevent that (I say "cross browser" because I only checked webkit).

However, what you could do if this really is an issue is do something on the server side to prevent 404s altogether.

Essentially this would be, for example, /retina.php?image=YOUR_URLENCODED_IMAGE_PATH a request to which could return this when a retina image exists...

{"isRetina": true, "path": "YOUR_RETINA_IMAGE_PATH"}}

and this if it doesnt...

{"isRetina": false, "path": "YOUR_REGULAR_IMAGE_PATH"}}

You could then have some JavaScript call this script and parse the response as necessary. I'm not claiming that is the only or the best solution, just one that would work.

like image 24
KTastrophy Avatar answered Nov 01 '22 09:11

KTastrophy


Retina JS supports the attribute data-no-retina on the image tag. This way it won't try to find the retina image.

Helpful for other people looking for a simple solution.

<img src="/path/to/image" data-no-retina />
like image 7
John Ballinger Avatar answered Nov 01 '22 09:11

John Ballinger


I prefer a little more control over which images are replaced.

For all images that I've created a @2x for, I changed the original image name to include @1x. (* See note below.) I changed retina.js slightly, so that it only looks at [name]@1x.[ext] images.

I replaced the following line in retina-1.1.0.js:

retinaImages.push(new RetinaImage(image));

With the following lines:

 if(image.src.match(/@1x\.\w{3}$/)) {
    image.src = image.src.replace(/@1x(\.\w{3})$/,"$1");
    retinaImages.push(new RetinaImage(image));
}

This makes it so that retina.js only replaces @1x named images with @2x named images.

(* Note: In exploring this, it seems that Safari and Chrome automatically replace @1x images with @2x images, even without retina.js installed. I'm too lazy to track this down, but I'd imagine it's a feature with the latest webkit browsers. As it is, retina.js and the above changes to it are necessary for cross-browser support.)

like image 3
MrTemple Avatar answered Nov 01 '22 09:11

MrTemple


One of solutions is to use PHP:

replace code from 1st post with:

        http = new XMLHttpRequest;
        http.open('HEAD', "/image.php?p="+this.at_2x_path);
        http.onreadystatechange = function() {
            if (http.readyState != 4) {
                return callback(false);
            }

            if (http.status >= 200 && http.status <= 399) {
                if (config.check_mime_type) {
                    var type = http.getResponseHeader('Content-Type');
                    if (type == null || !type.match(/^image/i)) {
                        return callback(false);
                    }
                }

                RetinaImagePath.confirmed_paths.push(that.at_2x_path);
                return callback(true);
            } else {
                return callback(false);
            }
        }
        http.send();

and in yours site root add file named "image.php":

<?php
 if(file_exists($_GET['p'])){
  $ext = explode('.', $_GET['p']);
  $ext = end($ext);
  if($ext=="jpg") $ext="jpeg";
  header("Content-Type: image/".$ext);
  echo file_get_contents($_GET['p']);
 }
?>
like image 2
Pawel Pawlowski Avatar answered Nov 01 '22 08:11

Pawel Pawlowski