javascripthtmlxmlhttprequesthtml5-canvasputimagedata

How can I set an HTML5 canvas ImageData from an XMLHttpRequest response?


I would like to use XMLHttpRequest to load a png file and then use the response to set the image data in a canvas object, thus totally eliminating the need for an Image object and having direct access to the ImageData. So far my code looks like this:

var xhr = new XMLHttpRequest();
var context = document.createElement("canvas").getContext("2d"); // the context for the image we are loading
var display = document.getElementById("display").getContext("2d"); // the context of the canvas element in the html document

function load(event) {
  var imagedata = context.createImageData(64, 64); // 64 = w, h of image
  imagedata.data.set(new Uint8ClampedArray(this.response)); // the response of the load event
  context.putImageData(imagedata,0,0); // put the image data at the top left corner of the canvas

  display.drawImage(context.canvas, 0, 0, 64, 64, 0, 0, 64, 64); // draws a bunch of jumbled up pixels from my image in the top of my display canvas
}

xhr.addEventListener("load", load);
xhr.open("GET", "myimage.png");
xhr.responseType = "arraybuffer";
xhr.send(null);

What am I doing wrong here? is it a problem with converting the ArrayBuffer in the response to a Uint8ClampedArray? Should I be using different array types? Is it the XMLHttpRequest? Is this possible?


Solution

  • Loading images via XMLHttpRequest

    Image files are not pixel arrays

    The data you get is not a pixel array it is image data. You can read the data directly and decode it but that is a lot of work, png has many different internal formats and compression methods. And why bother when all the code to do that is already available within the browser.

    Normally I would I leave it up to the browser to do all the fetching but because there are no progress events on images and games can need a lot of image data I created this to handle the problem of loading with a meaningful progress display. It does the same as you are trying to do.

    Once you have the data loaded you need to get the browser to decode it for you. To do that you need to convert the data you have to a DataURL. I do that in the function arrayToImage which converts the typed array to a data url with the appropriate image header.

    Then it is just a matter of creating an image and setting the source to the data URL. It is rather ugly as it requires you to create the data buffer, then the url string, then the browser makes another copy to finally get the image. (way too much memory used) If you want it as an imageData array you need to render the image to a canvas and grab the data from there.

    Example image loading with (real) progress events

    Below is the code, it will fail if the image does not allow cross site access, and its only benefit is that you get progress events, which is included in the snippet.

    // creates an image from a binary array
    // buf   : is the image as an arrayBuffer
    // type  : is the mime image type "png", "jpg", etc...
    // returns a promise that has the image
    function arrayToImage(buf, type) {
        // define variables
        var url, chars, bWord, i, data, len, count, stream, wordMask, imagePromise;
        // define functions
        imagePromise = function (resolve, reject) { // function promises to return an image
            var image = new Image(); // create an image
            image.onload = function () { // it has loaded
                resolve(image); // fore fill the promise
            }
            image.onerror = function () { // something rotten has happened
                reject(image); // crossing the fingers
            }
            image.src = url; // use the created data64URL to ceate the image
        }
    
        wordMask = 0b111111; // mask for word base 64 word
        stream = 0; // to hold incoming bits;
        count = 0; // number of bits in stream;
        // 64 characters used to encode the 64 values of the base64 word
        chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    
        data = new Uint8Array(buf); // convert to byte array
        len = data.byteLength; // get the length;
        url = 'data:image/' + type.toLowerCase() + ';base64,'; // String to hold the image URL
    
        // get each byte and put it on the bit stream
        for (i = 0; i < len; i++) {
            stream |= data[i]; // add byte to bit stream
            count += 8; // add the number of bits added to stream
            if (count === 12) { // if there are two 6bit words on the stream
                url += chars[(stream >> 6) & wordMask] + chars[stream & wordMask]; // encode both words and add to base 64 string
                stream = 0; // stream is empty now so just zero
                count = 0; // no bits on the stream
            } else {
                url += chars[(stream >> (count - 6)) & wordMask]; // encode top 6 bits and add to base64 string
                count -= 6; //decrease the bit count by the 6 removed bits
                stream <<= 8; // make room for next 8 bits
            }
        }
        if (count > 0) { // there could be 2 or 4 remaining bits
            url += chars[(stream >> (count + 2)) & wordMask]; // shift them  back to B64 word size and encode
        }
        // data url constructed for image so lets promise to create it
        return new Promise(imagePromise); // return the promise
    }
    // loads an image via ajax providing progress data
    // WARNING cross domain images will fail if they have a CORS header prohibiting your domain from access
    // filename : url of the image file
    // progress : progress call back. This is called on progress events
    // returns a promise of an image
    var loadImage = function(filename,progress){
        // declare variables
        var imagePromise;
        // declare functions
        imagePromise = function(resolve, reject){  // promise an image
            // decalare vars;
            var ajax, image, load, failed;
            // decalare functions
            failed = function (reason) { reject("Shit happens"); } // pass on the bad news
            load = function (e) {  // handle load event
                // declare vars
                var type, loaded;
                // decalare functions
                loaded = function (image) { resolve(image);} // resolve the promise of an image
    
                if(e.currentTarget.status !== 200){ // anything but OK reject the promise and say sorry
                    reject("Bummer dude! Web says '"+e.currentTarget.status+"'");
                }else{
                    type = filename.split(".").pop(); // ok we have the image as a binary get the type
                    // now convert it to an image
                    arrayToImage(e.currentTarget.response,type)  // return a promise 
                        .then(loaded)   // all good resolve the promise we made
                        .catch(failed); // failed could be a bug in the soup.
                }
            };
            
            ajax = new XMLHttpRequest();  // create the thingy that does the thing
            ajax.overrideMimeType('text/plain; charset=x-user-defined'); // no not an image. 
            ajax.responseType = 'arraybuffer';  // we want it as an arraybuffer to save space and time
            ajax.onload = load;  // set the load function
            ajax.onerror = failed; // on error
            ajax.onprogress = progress; // set the progress callback
            ajax.open('GET', filename, true);  // point to the image url
            ajax.send();  // command the broswer to wrangle this image from the server gods
        }
        return new Promise(imagePromise);
    }
    
    
    // the progress display. Something that looks profesional but still hates the status quo.
    var displayProgress = function(event){ // event is the progress event 
        // decalre vars
        var w,h,x,y,p,str;
        
        w = ctx.canvas.width;  // get the canvas size
        h = ctx.canvas.height;
        x = w/2-w/4;          // locate the progress bar
        w /= 2;              // make it in the center
        y = h/2-10;
        
        if(event.lengthComputable){   // does the progress know whats coming
            p = event.loaded/event.total;   // yes so get the fraction found
            str = Math.floor(p*100)+"%";    // make it text for the blind
        }else{
            p = event.loaded/1024;   // dont know how much is comine so get number killobytes
            str = Math.floor(p) + "k"; // for the gods
            p /= 50;   // show it in blocks of 50k
        }
    
        ctx.strokeStyle = "white";  // draw the prgress bar in black and white
        ctx.fillStyle = "black"; 
        ctx.lineWidth = 2; // give it go fast lines
        ctx.beginPath();
        ctx.rect(x,y,w,20);   // set up the draw
        ctx.fill();  // fill 
        ctx.stroke(); // then stroke
    
        ctx.fillStyle = "white";  // draw text in white 
        ctx.font = "16px verdana"; // set the font
        ctx.textAlign = "center";  // centre it
        ctx.textBaseline = "middle";  // in the middle please
        ctx.fillText(str,x+w/2,y+10);  // draw the text in the center
    
        ctx.globalCompositeOperation = "difference"; // so the text is inverted when bar ontop
        ctx.beginPath();  
        ctx.fillRect(x+3,y+3,(p*(w-6))%w,14);  // draw the bar, make sure it cycles if we dont know what coming
    
        ctx.globalCompositeOperation = "source-over"; // resore the comp state
    }
    var canvas = document.createElement("canvas");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    document.body.appendChild(canvas);
    ctx = canvas.getContext("2d");
    
        // The image name. 
        var imageName = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Broadway_tower_edit.jpg/800px-Broadway_tower_edit.jpg";
        
        // lets load the image and see if all this actualy works.
        loadImage(imageName, displayProgress)
         .then(function (image) {  // well what do you know it works
             ctx.drawImage(image, 0, 0, ctx.canvas.width, ctx.canvas.height); // draw the image on the canvas to prove it
         })
        .catch(function (reason) {
            console.log(reason);  // did not load, that sucks!
        })