如何从 XMLHttpRequest 响应设置 HTML5 canvas ImageData?

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

我想使用 XMLHttpRequest 加载一个 png 文件,然后使用响应在 canvas 对象中设置图像数据,从而完全不需要 Image 对象并可以直接访问图像数据。到目前为止,我的代码如下所示:

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);

我在这里做错了什么?将响应中的 ArrayBuffer 转换为 Uint8ClampedArray 是否有问题?我应该使用不同的数组类型吗?是 XMLHttpRequest 吗?这可能吗?

通过 XMLHttpRequest 加载图像

图像文件不是像素阵列

您获得的数据不是像素数组,而是图像数据。您可以直接读取数据并对其进行解码,但工作量很大,png 有许多不同的内部格式和压缩方法。当所有的代码都已经在浏览器中可用时,为什么还要费心。

通常我会把它留给浏览器来完成所有的获取但是因为图像上没有进度事件并且游戏可能需要大量图像数据我创建这个来处理有意义的加载问题进度显示。它与您尝试做的一样。

加载数据后,您需要让浏览器为您解码。为此,您需要将您拥有的数据转换为 DataURL。我在函数 arrayToImage 中执行此操作,该函数将类型化数组转换为具有适当图像 header 的数据 url。

然后只需创建图像并将源设置为数据 URL。它相当丑陋,因为它需要您创建数据缓冲区,然后是 url 字符串,然后浏览器再制作一份副本以最终获取图像。 (使用太多内存)如果你想把它作为一个 imageData 数组,你需要将图像渲染到 canvas 并从那里获取数据。

使用(实际)进度事件加载示例图像

下面是代码,如果图像不允许跨站点访问,它会失败,它唯一的好处是你会得到进度事件,它包含在代码片段中。

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