If the crossOrigin
property/attribute is supported by the browser (it is now in FF, Chrome, latest Safari and Edge ), but the server doesn't answer with the proper headers (Access-Control-Allow-Origin: *
), then the img's onerror
event fires.
So we can just handle this event and remove the attribute if we want to draw the image anyway.
For browsers that don't handle this attribute, the only way o test if the canvas is tainted is to call the toDataURL
into a try catch block.
Here is an example :
var urls =
["http://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png",
"http://lorempixel.com/200/200"];
var tainted = false;
var img = new Image();
img.crossOrigin = 'anonymous';
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
var load_handler = function() {
canvas.width = 200;
canvas.height = 200;
ctx.fillStyle = 'white';
ctx.font = '15px sans-serif';
ctx.drawImage(this, 0, 0, 200, 200*(this.height/this.width));
// for browsers supporting the crossOrigin attribute
if (tainted) {
ctx.strokeText('canvas tainted', 20, 100);
ctx.fillText('canvas tainted', 20, 100);
} else {
// for others
try {
canvas.toDataURL();
} catch (e) {
tainted = true;
ctx.strokeText('canvas tainted after try catch', 20, 100);
ctx.fillText('canvas tainted after try catch', 20, 100);
}
}
};
var error_handler = function() {
// remove this onerror listener to avoid an infinite loop
this.onerror = function() {
return false
};
// certainly that the canvas was tainted
tainted = true;
// we need to removeAttribute() since chrome doesn't like the property=undefined way...
this.removeAttribute('crossorigin');
this.src = this.src;
};
img.onload = load_handler;
img.onerror = error_handler;
img.src = urls[0];
btn.onclick = function() {
// reset the flag
tainted = false;
// we need to create a new canvas, or it will keep its marked as tainted flag
// try to comment the 3 next lines and switch multiple times the src to see what I mean
ctx = canvas.cloneNode(true).getContext('2d');
canvas.parentNode.replaceChild(ctx.canvas, canvas);
canvas = ctx.canvas;
// reset the attributes and error handler
img.crossOrigin = 'anonymous';
img.onerror = error_handler;
img.src = urls[+!urls.indexOf(img.src)];
};
<button id="btn"> change image src </button><br>
But since toDataURL
can be a really heavy call for just a check and that code in try catch is deoptimized, a better alternative for older browsers is to create a 1px*1px tester canvas, draw the images on it first and call its toDataURL in the try-catch block :
var urls = ["http://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png", "http://lorempixel.com/200/200"];
var img = new Image();
img.crossOrigin = 'anonymous';
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
//create a canvas only for testing if our images will taint our canvas or not;
var taintTester = document.createElement('canvas').getContext('2d');
taintTester.width = 1;
taintTester.height = 1;
var load_handler = function() {
// our image flag
var willTaint = false;
// first draw on the tester
taintTester.drawImage(this, 0, 0);
// since it's only one pixel wide, toDataURL is way faster
try {
taintTester.canvas.toDataURL();
} catch (e) {
// update our flag
willTaint = true;
}
// it will taint the canvas
if (willTaint) {
// reset our tester
taintTester = taintTester.canvas.cloneNode(1).getContext('2d');
// do something
ctx.fillStyle = 'rgba(0,0,0,.7)';
ctx.fillRect(0, 75, ctx.measureText('we won\'t diplay ' + this.src).width + 40, 60);
ctx.fillStyle = 'white';
ctx.font = '15px sans-serif';
ctx.fillText('we won\'t diplay ' + this.src, 20, 100);
ctx.fillText('canvas would have been tainted', 20, 120);
} else {
// all clear
canvas.width = this.width;
canvas.height = this.height;
ctx.fillStyle = 'white';
ctx.font = '15px sans-serif';
ctx.drawImage(this, 0, 0);
}
};
var error_handler = function() {
// remove this onerror listener to avoid an infinite loop
this.onerror = function() {
return false
};
// we need to removeAttribute() since chrome doesn't like the property=undefined way...
this.removeAttribute('crossorigin');
this.src = this.src;
};
img.onload = load_handler;
img.onerror = error_handler;
img.src = urls[0];
btn.onclick = function() {
// reset the attributes and error handler
img.crossOrigin = 'anonymous';
img.onerror = error_handler;
img.src = urls[+!urls.indexOf(img.src)];
};
<button id="btn">change image src</button>
Note
Cross-origin requests are not the only way to taint a canvas :
In IE < Edge, drawing an svg on the canvas will taint the canvas for security issues, in the same way, latest Safari does taint the canvas if a <foreignObject>
is present in an svg drawn onto the canvas and last, any UA will taint the canvas if an other tainted canvas is painted to it.
So the only solution in those cases to check if the canvas is tainted is to try-catch
, and the best is to do so on a 1px by 1px test canvas.
<img>
object only helps if the server hosting the image URL responds with a properAccess-Control-Allow-Origin
header. – Excelsior.setAttribute
-img.crossOrigin = "Anonymous";
– Excelsior.setAttribute()
. – Excelsior