Frank
Frank

Reputation: 2230

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?

Upvotes: 2

Views: 1902

Answers (1)

Blindman67
Blindman67

Reputation: 54109

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!
    })

Upvotes: 3

Related Questions