20

Basic scenario

I'm loading several images on the client side. Some of them are from another domain, some are not. Some I may be able to access using the crossOrigin attribute, some not.

The basic requirement is to retrieve a dataURL for the images where possible.

Question

After drawing the image to the canvas element (which I need to to to get a dataURL, right?), how can I check, without a try ... catch block, whether the canvas has been tainted? If the canvas is tainted, I can't use toDataURL() anymore (see MDN).

var image = new Image(),
    canvas = document.createElement( 'canvas' ),
    context = canvas.getContext( '2d' );

image.onload = function(){

  // adjust dimensions of canvas
  canvas.width = image.width;
  canvas.height = image.height;

  // insert image
  context.drawImage( image, 0, 0 );

  // how to check here?

  // get dataurl
  dataurl = tmpCanvas.toDataURL();

  // do stuff with the dataurl
};
image.src = src;  // some cross origin image
Sirko
  • 72,589
  • 19
  • 149
  • 183
  • This is a great question. I don't know of any way other than try/catch. But if you have a reference to the `image` variable, you could call `image.crossOrigin`. Expanding on this idea, you could create a global object that maps images to their corresponding crossOrigin variable. Checking if the image is tainted would then be a matter of iterating over the object and see what the crossOrigin values are. – monsur Mar 22 '14 at 15:06
  • Also, it looks like canvas contexts have an internal `origin-clean` variable: http://www.w3.org/TR/html5/scripting-1.html#concept-canvas-origin-clean but as far as i can tell, this isn't exposed to the user. – monsur Mar 22 '14 at 15:26
  • @monsur that is internal for the browser (the specs are for browser implementers). –  Mar 22 '14 at 18:41
  • @Cryptoburner Yup, that is true. But the discussion below indicates a good need for this property. It is something that should be brought up in the HTML5 spec. – monsur Mar 23 '14 at 16:46

2 Answers2

7

Here is a solution that doesn't add properties to native objects:

function isTainted(ctx) {
    try {
        var pixel = ctx.getImageData(0, 0, 1, 1);
        return false;
    } catch(err) {
        return (err.code === 18);
    }
}

Now simply check by doing this:

if (isTainted(ctx)) alert('Sorry, canvas is tainted!');

Edit: NOW i see you wanted a solution without try-catch. Though, this is the proper way to check as there is no origin clean flag exposed to user (it's for internal use only). Adding properties to native object is not recommended.

Duncan
  • 23
  • 8
  • Yep, since CORS errors are fatal this quick if-else test fails also :-(. I agree there should be an exposed flag to indicate a tainted canvas. Even thought the question explicitly asks not, I would probably use try-catch here also. After all the questioner is not trying to smooth over errors created by poor coding. He’s just trying to determine the state of the canvas. – markE Mar 22 '14 at 20:08
  • **Jumping on my soapbox** ;-) I would better say "modifying the functionality of existing properties of a native object is not recommended." IMHO, the nature of javascript is an open prototypical language that encourages you create and extend objects. That’s my 2-cents ;) – markE Mar 22 '14 at 20:09
  • @markE yes, it's dynamically (one reason why we love it!) but with our own objects ;) It does most likely no harm whatsoever in this case but in general it's considered bad practice if it can be solved other ways (which it can in this case). –  Mar 22 '14 at 20:13
  • **Again on soapbox :-)** Adding (not replacing) properties / methods on native objects is common. Even John Resig (creator of jQuery!) extends native objects (example: http://ejohn.org/blog/fast-javascript-maxmin/) Certainly, if adding properties is not to your liking, you can encapsulate the native object into your own object and extend your own object. Or just create an array that holds which images are tainted. Lots of workarounds to suite every programmers style! Anyway, SO's auto-moderator will get mad at us for this discussion--but it's been fun discussing with you! :-) – markE Mar 22 '14 at 20:57
  • @markE I don't mind changing objects if needed. My point is if "*you know what you're doing*" there's no problem. However, there will be beginners reading the posts and I think it's wise not to give the impression that it's a fully OK practice as experience has shown that it isn't (if you *don't* know what you're doing). It's better to be on the safe-side in places like SO IMO & when users are experienced enough they'll figure out what is and isn't good to do. But I didn't intend to start any discussion - it was meant as an advice which I think is a good advice. jQuery is btw a special case. –  Mar 22 '14 at 23:31
  • We just have different philosophies about adding properties to make native objects useful. I respect your philosophy :) – markE Mar 23 '14 at 01:50
  • Adding properties to native objects is widely considered bad practice because it blacklists property names that future versions of JavaScript can use without breaking (widespread) libraries. Just search `smooshMap` for an example. – Michael Theriot May 28 '18 at 04:22
1

Here's an indirect test for CORS tainting that doesn't use try-catch:

A Demo: http://jsfiddle.net/m1erickson/uDt2K/

It works by setting a image.tainted=true flag before loading the image

Then in image.onload, context.getImageData triggers/doesn't trigger a CORS violation.

If no violation occurs, then the tainted flag is set to false (image.tainted=false).

var img=new Image();

// set a "tainted flag to the image to true (initialize as tainted)
img.tainted=true;

img.onload=function(){

    // try an action that triggers CORS security

    var i=ctx.getImageData(1,1,1,1);

    // if we don't get here, we violated CORS and "tainted" remains true

    // if we get here, CORS is happy so set the "tainted" flag to false

    img.tainted=false;

};

// test with tainted image

img.src="http://pp-group.co.uk/wp/wp-content/uploads/2013/10/house-illustration-web.gif";

Since image.onload is asynchronous, your code outside image.onload will still execute even after a CORS violation.

Here's example code that:

  • creates an image object
  • tests if the image is CORS compliant
  • executes one callback if the image is compliant
  • executes another callback if the image is not compliant

Example Code:

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>
<script>
$(function(){

    // known CORS violator
    var src1="http://pp-group.co.uk/wp/wp-content/uploads/2013/10/house-illustration-web.gif";
    // known CORS compliant
    var src2="https://dl.dropboxusercontent.com/u/139992952/houseIcon.png";

    // callbacks depending on if the image causes tainted canvas
    function tainted(img){console.log("tainted:",img.src);}
    function notTainted(img){console.log("not tainted:",img.src);}

    // testing
    var image1=newImage(src1,tainted,notTainted);
    var image2=newImage(src2,tainted,notTainted);


    function newImage(src,callWhenTainted,callWhenNotTainted){

        // tmpCanvas to test CORS
        var tmpCanvas=document.createElement("canvas");
        var tmpCtx=tmpCanvas.getContext("2d");
        // tmpCanvas just tests CORS, so make it 1x1px
        tmpCanvas.width=tmpCanvas.height=1;

        var img=new Image();
        // set the cross origin flag (and cross our fingers!)
        img.crossOrigin="anonymous";
        img.onload=function(){
            // add a tainted property to the image 
            // (initialize it to true--is tainted)
            img.tainted=true;
            // draw the img on the temp canvas
            tmpCtx.drawImage(img,0,0);
            // just in case this onload stops on a CORS error...
            // set a timer to call afterOnLoad shortly
            setTimeout(function(){
                afterOnLoad(img,callWhenTainted,callWhenNotTainted);
            },1000);  // you can probably use less than 1000ms
            // try to violate CORS
            var i=tmpCtx.getImageData(1,1,1,1);
            // if we get here, CORS is OK so set tainted=false
            img.tainted=false;
        };
        img.src=src;
        return(img);
    }

    // called from image.onload
    // at this point the img.tainted flag is correct
    function afterOnLoad(img,callWhenTainted,callWhenOK){
        if(img.tainted){
            // it's tainted
            callWhenTainted(img);
        }else{
            // it's OK, do dataURL stuff
            callWhenOK(img);
        }
    }


}); // end $(function(){});
</script>
</head>
<body>
    <canvas id="canvas" width=360 height=281></canvas>
</body>
</html>
markE
  • 102,905
  • 11
  • 164
  • 176
  • Just in case it was not clear to readers, this is still throwing an uncaught error, it is just ignored by synchronous code because it throws in the asynchronous flow. In some environments uncaught asynchronous exceptions will halt the program (and you should catch them regardless). – Michael Theriot May 28 '18 at 04:44