How do I set crossOrigin attribute when using canvas.toDataURL?
Asked Answered
C

2

9

So I'm trying to create a print map function for an OpenLayers 3 application I'm building. I'm aware of their example but whenever I attempt to use it I run into the dreaded tainted canvas issue. I've read the whole internet and come across folks saying first to set CORS correctly (done and done) but also to do:

          var img = new Image();
          img.setAttribute('crossOrigin', 'anonymous');
          img.src = url;

The above is described here.

My question is, I've never really used toDataURL() before and I'm not really sure how I make sure the image being created has the crossOrigin attribute correctly set before it slams into the:

Error: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

Any thoughts?

I have seen this. My question is how they incorporate that into a function that works. Something like:

    var printMap = function(){
     var img = new Image();
     img.setAttribute('crossOrigin', 'anonymous');
     img.src = url;
     img.onload = function() {
      var canvas = document.getElementsByTagName('canvas');
      var dataURL = canvas.toDataURL("image/png");
      console.log(dataURL);
     };
   };
Caesium answered 6/1, 2016 at 18:2 Comment(8)
Setting the property on the <img> object only helps if the server hosting the image URL responds with a proper Access-Control-Allow-Origin header.Excelsior
More information here at MDN.Excelsior
@Excelsior Perhaps you didn't click on the links I provided. Or even really read what I wrote. I already know about making sure the header has the proper Access-Control-Allow-Origin. I already read the link you provided. I'm looking for someone who has successfully pulled this off. Because I haven't been able to do so.Caesium
I did click on the link you provided. That document itself contains a link to the MDN page I linked above. The linked duplicate question has lots of information; have you read the answers there? Note that currently only Chrome and Firefox support the image "crossOrigin" attribute semantics (according to MDN).Excelsior
Also, and this may not make a difference, but the example code at MDN sets the "crossOrigin" property directly on the Image instance and not via .setAttribute - img.crossOrigin = "Anonymous";Excelsior
Here's a working jsfddle. - edit seems to work whether the property is set directly or via .setAttribute().Excelsior
@Excelsior There seems to be some greater issue with my code that I'll have to nail down. Thanks for your advice and your JSFiddle. I appreciate it!Caesium
Hello please help me #41662172Vieira
G
8

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.

Gilliangilliard answered 7/1, 2016 at 2:31 Comment(1)
I juste saw your detailed answer, I see you have good knowloedge about this kind of bug, could you have a look at my question please ? #46610300Discussion
C
6

So Pointy and Kaiido both had valid ways of making this work but they both missed that this was an OpenLayers issue (and in the case of Pointy, not a duplicate question).

The answer was to do this:

            source = new ol.source.TileWMS({
              crossOrigin: 'anonymous'
            });

Basically you had to tell the map AND the layers that you wanted crossOrigin: anonymous. Otherwise your canvas would still be tainted. The more you know!

Caesium answered 7/1, 2016 at 19:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.