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