15

(Pre-emptive strike: If you're tempted to mark this as a duplicate, note that other questions seem to ask "why am I getting this error?" I know why I'm getting this error; I want to know how I can detect the error in my JavaScript code. It only appears in the Firebug console and, of course, is obvious to the user when the image is loaded.)

I am using picturefill for responsive images. I have a callback that is fired for the load event on the images. So the callback runs every time someone resizes the browser window such that a different image is loaded via picturefill.

Inside the callback, I'm converting the image data to a dataURL via canvas so that I can cache the image data in localStorage so its available to the user even when they are offline.

Note the part about "offline". That's why I cannot rely on the browser cache. And the HTML5 offline application cache doesn't meet my needs because the images are responsive. (See "Application Cache is a Douchebag" for the explanation of the incompatibility of responsive images with HTML offline application cache.)

On Firefox 14.0.1 on a Mac, the load image will fire if I resize the browser to something really big and then resize it back down to something small again before the large image has a chance to fully load. It ends up reporting "Image corrupt or truncated" in the Firebug console, but doesn't throw an exception or trigger an error event. No indication anything is wrong in the code. Just in the Firebug console. Meanwhile, it stores a truncated image in localStorage.

How can I reliably and efficiently detect this problem within JavaScript so that I don't cache that image?

Here's how I loop through the picturefill divs to find img tags that have been inserted by picturefill:

    var errorLogger = function () {
        window.console.log('Error loading image.');
        this.removeEventListener('load', cacheImage, false);
    };

    for( var i = 0, il = ps.length; i < il; i++ ){
        if( ps[ i ].getAttribute( "data-picture" ) !== null ){

            image = ps[ i ].getElementsByTagName( "img" )[0];
            if (image) {
                if ((imageSrc = image.getAttribute("src")) !== null) {
                    if (imageSrc.substr(0,5) !== "data:") {
                        image.addEventListener("load", cacheImage, false);
                        image.addEventListener('error', errorLogger, false);
                    }
                }
            }
        }
    }

And here's what the cacheImage() callback looks like:

var cacheImage = function () {
    var canvas,
        ctx,
        imageSrc;

    imageSrc = this.getAttribute("src");

    if ((pf_index.hasOwnProperty('pf_s_' + imageSrc)) ||
        (imageSrc.substr(0,5) === "data:") ||
        (imageSrc === null) || (imageSrc.length === 0)) {
            return;
    }

    canvas = w.document.createElement("canvas");
    canvas.width = this.width;
    canvas.height = this.height;

    ctx = canvas.getContext("2d");
    ctx.drawImage(this, 0, 0);
    try {
        dataUri = canvas.toDataURL();
    } catch (e) {
        // TODO: Improve error handling here. For now, if canvas.toDataURL()
        //   throws an exception, don't cache the image and move on.
        return;
    }

    // Do not cache if the resulting cache item will take more than 128Kb.
    if (dataUri.length > 131072) {
        return;
    }

    pf_index["pf_s_"+imageSrc] = 1;

    try {
        localStorage.setItem("pf_s_"+imageSrc, dataUri);
        localStorage.setItem("pf_index", JSON.stringify(pf_index));
    } catch (e) {
        // Caching failed. Remove item from index object so next cached item
        //   doesn't wrongly indicate this item was successfully cached.
        delete pf_index["pf_s_"+imageSrc];
    }
};

Lastly, here is the full text of what I am seeing in Firebug with the URL changed to protect the guilty:

Image corrupt or truncated: http://www.example.com/pf/external/imgs/extralarge.png

Trott
  • 66,479
  • 23
  • 173
  • 212
  • 2
    Doesn't it fire an `error` event on the `` element? – MaxArt Aug 13 '12 at 06:18
  • 2
    One might ask why you're trying to implement your own image caching in local storage rather than just let the browser cache do it's work? – jfriend00 Aug 13 '12 at 06:24
  • @jfriend00 I am caching the image for offline usage. I said this in the question, but I phrased it like an aside. I've edited the question to give the offline bit more emphasis. Anyway, the next obvious question is why not use HTML5 offline appcaching. For the explanation as to why responsive images and HTML5 offline appcaching are incompatible, see http://www.alistapart.com/articles/application-cache-is-a-douchebag/ – Trott Aug 13 '12 at 14:09
  • 2
    @MaxArt No, the error event does not fire. I've edited the question to include the error handler code and text indicating that it does not fire. I believe that the error event and load event are mutually exclusive on image elements. I suppose Firefox has chosen to fire the load event when a corrupt or truncated image is loaded. In any event, that's consistent with what I"m seeing. – Trott Aug 13 '12 at 21:27

2 Answers2

9

It appears that when changing the src attribute of the img tag, Firefox fires a load event. This is contrary to the HTML5 specification, which says that if the src attribute is changed, then any other fetch already in progress should be ended (which Firefox does), and no events should be sent. The load event should be sent only if the fetch was completed successfully. So I'd say that the fact that you get a load event is a Firefox bug.

However, the image should know if it is fully available or not, you could try to use the complete attribute:

if (this.complete === false) {
  return;
}

Unfortunately, Firefox has this.complete set to true instead, so that's not an option either.

The best option may be to create a new <img> element each time you wish to change the src attribute.

Trott
  • 66,479
  • 23
  • 173
  • 212
Sergiu Dumitriu
  • 11,455
  • 3
  • 39
  • 62
  • Unfortunately, this.complete is true at that time as well, so that doesn't get around the issue. I think what's happening there is that once .complete gets set to true, Firefox never sets it to false when src is changed. Harrumph. Thanks for mentioning the HTML5 spec. I'll have to check that out. I thought it was ambiguous on this, but if not, perhaps it is time to file a bug! – Trott Aug 17 '12 at 15:13
  • Indeed, I tried it myself now and the `complete` flag remains `true`, and it's read-only, it can't be manually changed back. So either wait for Firefox to fix this bug, or you can create a new `img` element each time you change the `src` and replace the old one. – Sergiu Dumitriu Aug 17 '12 at 17:35
  • Actually, while trying out the example, I noticed that I didn't get a `load` event when changing the `src` and the image wasn't fully loaded. Tested on FF 14.0.1 on Linux, using both png and jpg images. I created [this jsfiddle](http://jsfiddle.net/GfKCf/5/) to try it out, open Firebug and watch which events are logged in the console. – Sergiu Dumitriu Aug 17 '12 at 17:50
  • Another disadvantage (?) of reusing the same `img` element is that after the first load, you don't get to see how partial results as the image is streaming subsequently, it will be replaced completely once it finishes downloading. But that might be actually something that you want. – Sergiu Dumitriu Aug 17 '12 at 17:52
  • I'm seeing the same thing you are in the jsfiddle. That suggests that the problem isn't in the image event firing after all but somehow in the canvas interaction with the img. (Well, either that, or I'm just making a big mistake somewhere and not seeing it yet.) I will definitely have to mess more later today when I get some time. Gotta run off right now.... Thank you so much for all the looking into this! – Trott Aug 17 '12 at 18:38
  • OK, I think I got it. Looks to me like it's a race condition in Firefox and/or my code. http://jsfiddle.net/trott/GfKCf/6/ You have to click on the large image once it starts to appear. That will trigger the swapping in of a much smaller and faster loading image. But the code immediately swaps back in the large image. When the onload fires for the smaller image, it reports the larger image as the src attribute. One could make the case that this is a race condition in my code, but Firefox also caches the image it has truncated/corrupted, and that at least seems like a Firefox bug to me. – Trott Aug 18 '12 at 15:31
  • Actually, this shows the problem a bit better I think by pre-loading the smaller image: http://jsfiddle.net/trott/GfKCf/8/ – Trott Aug 18 '12 at 15:37
  • I'm going to go ahead and edit your answer to incorporate some of the info from these comments and then accept the answer. If you don't like my edits, feel free to undo. – Trott Aug 24 '12 at 17:42
  • So in the end you went with using multiple `` elements? – Sergiu Dumitriu Aug 24 '12 at 20:10
  • I didn't, but I have every reason to believe it would work. In the end, I went with User Agent sniffing and just not bothering doing anything with the image on resize if the user is using Firefox. Lame, but I didn't want to reach into the library I was wrapping (picturefill) where the src swapping is happening. https://github.com/trott/pickyfill – Trott Aug 24 '12 at 20:14
2

I was dumbfounded by this issue today myself. Everything was working fine (the images loaded visually as expected) except that the error kept showing up in Firefox error console - no such errors in IE or Chrome though.

In my case I was plugging an image into a div with innerHtml in an on complete jquery handler. The errors stopped when I preempted the jquery call with:

var image_holder = new Image();
image_holder.src = img_path;//path of image that's going to be plugged in  

It worked for me, but it still makes no sense. I assume it's something to do with timing as this code initiates the image to load before it gets to the code that actually plugs it into the page.

Trott
  • 66,479
  • 23
  • 173
  • 212
Sergey S
  • 21
  • 1