aFarkas / lazysizes

High performance and SEO friendly lazy loader for images (responsive and normal), iframes and more, that detects any visibility changes triggered through user interaction, CSS or JavaScript without configuration.
MIT License
17.54k stars 1.73k forks source link

Support for BGP & FLIF #202

Open technopagan opened 8 years ago

technopagan commented 8 years ago

As far as my tests show, non-standard image formats such as BGP or FLIF do not get lazy-loaded fully.

The behaviour I see is that the image does in fact get loaded async, but it is ALWAYS loaded, regardless of whether I scroll towards its position inside the test page or not (tested in Firefox 42).

Is this due to LazySizes or is it a compound effect of BGP's / FLIF's JS-based decoder that's necessary to display these images?

Of course, in an ideal world, I'd love for such non-standard image formats to be handled just as smoothly by LazySizes as JPEGs etc are.

aFarkas commented 8 years ago

@technopagan

I have no experience here. But it should work. Could you put a simplified testcase together. In this case, I can also check how to get the job done if there are steps needed in a JS decoder.

technopagan commented 8 years ago

@aFarkas i already started digging: it's the BGP polyfill, not LazySizes.

I changed BPG's decoder polyfill "bpgdec8.js" (https://github.com/mirrorer/libbpg/blob/master/html/bpgdec8.js) to work on 'data-src' instead of img src:

My altered version of lines 90+91 of 'bpgdec8.js' in readable form:

window.onload = function() {
    var a, d, c, e, f, j, g;
    e = document.images;
    d = e.length;
    f = [];

    for (a = 0; a < d; a++){
        c = e[a];
        if( c.hasAttribute('data-src') ){
            j = c.getAttribute('data-src');
            ".bpg" == j.substr(-4, 4).toLowerCase() && (f[f.length] = c);
        }
    }

    d = f.length;
    for (a = 0; a < d; a++) {
        c = f[a];
        if( c.hasAttribute('data-src') ){
            j = c.getAttribute('data-src');
            e = document.createElement("canvas");
            c.id && (e.id = c.id);
            c.className && (e.className = c.className);
            if (g = c.getAttribute("width") | 0) e.style.width = g + "px";
            if (g = c.getAttribute("height") | 0) e.style.height = g + "px";
            c.parentNode.replaceChild(e, c);
            g = e.getContext("2d");
            c = new BPGDecoder(g);
            c.onload = function(a, c) {
                function d() {
                    var a =
                        e.n;
                    ++a >= f.length && (0 == e.loop_count || e.q < e.loop_count ? (a = 0, e.q++) : a = -1);
                    0 <= a && (e.n = a, c.putImageData(f[a].img, 0, 0), setTimeout(d, f[a].duration))
                }
                var e = this,
                    f = this.frames,
                    g = f[0].img;
                a.width = g.width;
                a.height = g.height;
                c.putImageData(g, 0, 0);
                1 < f.length && (e.n = 0, e.q = 0, setTimeout(d, f[0].duration))
            }.bind(c, e, g);
            c.load(j)
        }
    }
};

The thing is that the c.onload function of the polyfill seems to be loading the image all by itself, regardless of lazysizes.

I realize that this is hardly your problem, but any help to get the BGP polyfill working with lazysizes would be greatly appreciated!

technopagan commented 8 years ago

@aFarkas The simplified test case can be found here: http://tobias.is/nifty/imgload/lazysizes-bpg.html

aFarkas commented 8 years ago

You shouldn't mess around with the data-src in the bpg lib. Instead you should either use a MutationObserver for a src mutate or use the lazybeforeunveil event to create your own transform.

The later could look something like this (Code is not tested):

(function(){
    'use strict';
    var regBgp = /\.bpg$/i;

    var swapImage = function(img, src){
        var ctx, BPGimg;
        var canvas = document.createElement('canvas');

        canvas.className = img.className;

        img.parentNode.replaceChild(canvas, img);

        ctx = canvas.getContext("2d");
        BPGimg = new BPGDecoder(ctx);

        //
        BPGimg.onload = function() {
            /* draw the image to the canvas */
            canvas.width = this.imageData.width;
            canvas.height = this.imageData.height;
            ctx.putImageData(this.imageData, 0, 0);
        };
        BPGimg.load(src);
    };

    document.addEventListener('lazybeforeunveil', function(e){
        var src = e.target.getAttribute(lazySizesConfig.srcAttr) || '';
        if(regBgp.test(src)){
            e.preventDefault();
            swapImage(e.target, src);
        }
    });
})();

Note: Maybe the bgp lib author can/should provide a convenient method that can be used as swapImage replacement.

aFarkas commented 8 years ago

@technopagan

Does it actually work for you now? Should I try to re-create an example?

technopagan commented 8 years ago

@aFarkas Thanks for checking back with me! That's very kind of you!

Flu is keeping me from fully commiting to this at the moment, but I've grabbed my laptop after your question nontheless to see if I can get it working.

I've updated the demo page at http://tobias.is/nifty/imgload/lazysizes-bpg.html in a way that I thought would work: Unveilhook to load the BPG decoder async with lazysizes, then load & execute the code you posted above. The BPG image itself is also being lazy-loaded.

The browser is now fetching all resources correctly & at the right async'ed time. The BPG image does not render, however. The JS console is empty of errors.

I have to admit that I'm out of my depth atm. JS is not my forté. Can you have another look at http://tobias.is/nifty/imgload/lazysizes-bpg.html to spot what I've missed?

aFarkas commented 8 years ago

Just checked you demo. It works sometimes, but has async problems in 99% of all cases. The reason at the time of the image transformation the lazybeforeunveil hook has to be already fully loaded as also the bgp decoder.

If you would add them directly it would work. You could also do something like:

[data-script="customload.js"] {
    padding-top: 999px;
}

Here is how you can do it more lazy/async, but more robust:

  1. Remove the unveilhook plugin (we don't need it)
  2. Add loadJS instead (http://cdn.rawgit.com/filamentgroup/loadJS/master/loadJS.js)
  3. Add customload directly to your page with the following content:
(function(){
    'use strict';
    var SCRIPTURL = 'bpgdec8_orginal.js';
    var regBgp = /\.bpg$/i;
    var getBPGDecoder = (function(){
        var loading = false;
        var cbs = [];

        var call = function(){
            while(cbs.length){
                cbs.shift()(window.BPGDecoder);
            }
        };
        return function(cb){
            if(window.BPGDecoder){
                cb(window.BPGDecoder);
                return;
            }

            cbs.push(cb);

            if(!loading){
                loading = true;
                loadJS(SCRIPTURL, call);
            }

        };
    })();
    var swapImage = function(img, src){

        getBPGDecoder(function(){
            var ctx, BPGimg;
            if(img && img.parentNode){return;}

            var canvas = document.createElement('canvas');

            canvas.className = img.className;

            img.parentNode.replaceChild(canvas, img);

            ctx = canvas.getContext("2d");
            BPGimg = new BPGDecoder(ctx);

            //
            BPGimg.onload = function() {
                /* draw the image to the canvas */
                canvas.width = this.imageData.width;
                canvas.height = this.imageData.height;
                ctx.putImageData(this.imageData, 0, 0);
            };
            BPGimg.load(src);
        });

    };

    document.addEventListener('lazybeforeunveil', function(e){
        var src = e.target.getAttribute(lazySizesConfig.srcAttr) || '';
        if(regBgp.test(src)){
            e.preventDefault();
            swapImage(e.target, src);
        }
    });
})();

Hope this helps and get well soon!

technopagan commented 8 years ago

@aFarkas I've worked in your first suggestion (sufficient for my use case) and made good progress. Thank you!

May I ask for your assistance one final time? It's the last bit in my row of tests.

I have now combined lazysizes + picturefill + BPG Decoder, using the picture element in my source: http://tobias.is/nifty/imgload/04-bpg.html

The BPG images are once more not showing. It's not a race condition like the last time. I asssume it's down to the customload JS function, but my JS skill is too weak to confirm it or fix it.

Can you point me into the right direction once more?

(And can I buy you a drink next time I'm in Berlin? ;-) )

aFarkas commented 8 years ago

@technopagan I know, what 's the problem and I will try to put something together in the next 4-10h.

aFarkas commented 8 years ago

Sorry, will need to do this tomorrow. No time today.

technopagan commented 8 years ago

@aFarkas No worries! You're a great help & I appreciate your expertise very much! Thank you!

aFarkas commented 8 years ago

@technopagan

Unfortunately your last example isn't online anymore, so I couldn't test it.

(function(){
    'use strict';
    var SCRIPTURL = 'bpgdec8_orginal.js';
    var regBgp = /\.bpg$/i;
    var getBPGDecoder = (function(){
        var loading = false;
        var cbs = [];

        var call = function(){
            while(cbs.length){
                cbs.shift()(window.BPGDecoder);
            }
        };
        return function(cb){
            if(window.BPGDecoder){
                cb(window.BPGDecoder);
                return;
            }

            cbs.push(cb);

            if(!loading){
                loading = true;
                loadJS(SCRIPTURL, call);
            }

        };
    })();
    var swapImage = function(img, src){

        getBPGDecoder(function(){
            var ctx, BPGimg;
            if(img && img.parentNode){return;}

            var canvas = document.createElement('canvas');

            canvas.className = img.className;

            img.parentNode.replaceChild(canvas, img);

            ctx = canvas.getContext("2d");
            BPGimg = new BPGDecoder(ctx);

            //
            BPGimg.onload = function() {
                /* draw the image to the canvas */
                canvas.width = this.imageData.width;
                canvas.height = this.imageData.height;
                ctx.putImageData(this.imageData, 0, 0);
            };
            BPGimg.load(src);
        });

    };

    //start: new
    var slice = [].slice;
    var getCurrentSrc = function(img){
        var sources = [img];
        var parent = img.parentNode;

        if(parent.nodeName.toLowerCase() == 'picture'){
            sources = slice.call(parent.querySelectorAll('source, img')).filter(function(source){
                var media = source.getAttribute('media');
                return (!media || window.matchMedia(media));
            });
        }

        return sources[0] ?
            sources[0].getAttribute(lazySizesConfig.srcAttr) || sources[0].getAttribute(lazySizesConfig.srcsetAttr) || '' :
            ''
        ;
    };
    //end: new

    document.addEventListener('lazybeforeunveil', function(e){
        var src = getCurrentSrc(e.target);
        if(regBgp.test(src)){
            e.preventDefault();
            swapImage(e.target, src);
        }
    });
})();

The basic addition is getCurrentSrc, which tries to get the matching src from src or srcset. One (big) caveat: It doesn't work with resize and it doesn't work with multiple image candidates inside of one srcset.

But I assume, you are only testing things.

technopagan commented 8 years ago

@aFarkas Thank you! Sorry for the missing test case - I renamed them.

The newest test case is: http://tobias.is/nifty/imgload/0x-bpg.html and incorporates your new code. Unfortunately, it doesn't work: lazy loading is now broken completely & of course without BPG images bing loaded, there's no way to see if the BPG decoder would not work.

Is this because I have several resolutions definded inside the element?

technopagan commented 8 years ago

@aFarkas I've made some progress in debugging this & now BPGs are loaded & rendered. However, as you mentioned, the code currently picks the first candidate inside a element: JPEGs are being picked up correctly (940px width images for large browser window size, 480px for small browser window size), but the BPGs are always the 48px version because the 480px is the first listed resolution inside each picture element.

What can I do to make this test honor the functionality of the picture element?

(the current state is at http://tobias.is/nifty/imgload/0x-bpg.html)

technopagan commented 8 years ago

@aFarkas I suspect it comes down to detecting the viewport (var viewport = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;), reading the media attribute within each source array and then comparing if the current viewport is bigger or smaller than the media attribute for each of the three resolutions inside the picture element. Then use the correct resolution BPG to be returned from the getCurrentSrc function.

Is this a good approach? I'd greatly appreciate your insights. :)

aFarkas commented 8 years ago

@technopagan Sorry, I actually wrote something for you 2 days ago, but somehow I missed to press the comment button.

I saw you moved on with your code. I will look into this tomorrow. Currently plenty of work.

aFarkas commented 8 years ago

Is this a good approach? I'd greatly appreciate your insights. :)

We already have some code for this in place. But I made a mistake. You need to change the following line:

return (!media || window.matchMedia(media));

to:

return (!media || window.matchMedia(media).matches);