4

In Chrome I get the following error on jsfiddle when trying to test canvas drawing images from a remote url.

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

On S3 bucket I have the following CORS policy, which allows for cross resource sharing:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
</CORSRule>
</CORSConfiguration>

If I curl the image using jsfiddle as the origin I get:

curl -H 'Origin: https://fiddle.jshell.net' -I 'https://i.ezr.io/products3/472_ARDSCR.jpg?h=45&w=165&fit=scale'
HTTP/1.1 200 OK
Cache-Control: public,max-age=31536000
Last-Modified: Wed, 07 Feb 2018 23:42:47 GMT
Server: imgix-fe
Content-Length: 6371
Accept-Ranges: bytes
Date: Fri, 16 Mar 2018 17:10:20 GMT
Age: 3173253
Connection: keep-alive
Content-Type: image/jpeg
Access-Control-Allow-Origin: *
X-Content-Type-Options: nosniff
X-Served-By: cache-lax8630-LAX, cache-sea1027-SEA
X-Cache: HIT, HIT

var Rack = {
  init: function(params) {
    Rack.conf = params;
    Rack.bindEvents();
  },
  bindEvents: function() {
    Rack.conf.scaleDown.addEventListener("click", function() {
      Rack.clearCanvas();
      Rack.conf.scaleW = Rack.round(Rack.conf.scaleW - .1, 1);
      Rack.conf.scaleY = Rack.round(Rack.conf.scaleY - .1, 1);

      if (Rack.conf.scaleW > 0 && Rack.conf.scaleY > 0) {
        Rack.build();
      }
    });

    Rack.conf.resetScale.addEventListener("click", function() {
      Rack.clearCanvas();
      Rack.conf.scaleW = 1;
      Rack.conf.scaleY = 1;
      Rack.build(true);
    });

    Rack.getRackJSON();
    Rack.setNumImages();
    Rack.build();
  },
  getRackJSON: function() { // will send ajax call based on item being added to the rack, will return json rack
    Rack.conf.rack = {
      "awards": [{
          "src": "https://i.ezr.io/products3/472_ARDSCR.jpg?h=45&w=165&fit=scale",
          "name": "Army Distinguished Service Cross",
          "sku": "472_ARDSCR",
          "x": 0,
          "y": 0,
          "width": 165,
          "height": 45,
          "attachments": [{
            "src": "https://i.ezr.io/products3/913NP.png?h=20",
            "name": "Bronze Oak Leaf",
            "sku": "913NP",
            "x": 73,
            "y": 13,
            "width": 20,
            "height": 20
          }]
        },
        {
          "src": "https://i.ezr.io/products3/470_ARDDR.jpg?h=45&w=165&fit=scale",
          "name": "Department of Defense Distinguished Service",
          "sku": "470_ARDDR",
          "x": 165,
          "y": 0,
          "width": 165,
          "height": 45
        }
      ]
    };
  },
  setNumImages() {
    for (var i = 0; i < Rack.conf.rack.awards.length; i++) {
      var award = Rack.conf.rack.awards[i];

      ++Rack.conf.numImages;

      if (award.hasOwnProperty('attachments')) {
        Rack.conf.numImages += award.attachments.length;
      }
    }
  },
  loadImages: function(callback) {
    var numImagesLoaded = 0;

    for (var i = 0; i < Rack.conf.rack.awards.length; i++) {
      var award = Rack.conf.rack.awards[i];

      Rack.conf.images[award.sku] = new Image();
      Rack.conf.images[award.sku].onload = function() {
        if (++numImagesLoaded >= Rack.conf.numImages) {
          callback();
        }
      }
      Rack.conf.images[award.sku].src = award.src;

      if (award.hasOwnProperty('attachments')) {
        for (var j = 0; j < award.attachments.length; j++) {
          var attachment = award.attachments[j];

          Rack.conf.images[attachment.sku] = new Image();
          Rack.conf.images[attachment.sku].crossOrigin = 'anonymous';
          Rack.conf.images[attachment.sku].onload = function() {
            if (++numImagesLoaded >= Rack.conf.numImages) {
              callback();
            }
          }
          Rack.conf.images[attachment.sku].src = attachment.src;
        }
      }
    }
  },
  build: function(reset) {
    if (Rack.conf.outputType === 'jpg') {
      Rack.conf.ctx.fillStyle = "#ffffff";
      Rack.conf.ctx.fillRect(0, 0, Rack.conf.c.width, Rack.conf.c.height);
    }

    reset === true ? Rack.conf.ctx.setTransform(1, 0, 0, 1, 0, 0) : Rack.conf.ctx.scale(Rack.conf.scaleW, Rack.conf.scaleY);

    Rack.loadImages(function() {
      for (var i = 0; i < Rack.conf.rack.awards.length; i++) {
        var award = Rack.conf.rack.awards[i];

        Rack.conf.ctx.drawImage(Rack.conf.images[award.sku], award.x, award.y, award.width, award.height);

        if (award.hasOwnProperty('attachments')) {
          for (var j = 0; j < award.attachments.length; j++) {
            var attachment = award.attachments[j];

            Rack.conf.ctx.drawImage(Rack.conf.images[attachment.sku], attachment.x, attachment.y, attachment.width, attachment.height);
          }
        }
      }
    });

    Rack.conf.imageData = Rack.conf.c.toDataURL((Rack.conf.outputType === 'jpg' ? 'image/jpeg' : null), (Rack.conf.outputType === 'jpg' ? 1.0 : null));
  },
  clearCanvas: function() {
    Rack.conf.ctx.clearRect(0, 0, Rack.conf.c.width, Rack.conf.c.height);
  },
  round: function(number, precision) {
    var factor = Math.pow(10, precision);
    var tempNumber = number * factor;
    var roundedTempNumber = Math.round(tempNumber);
    return roundedTempNumber / factor;
  }
};

Rack.init({
  c: document.getElementById("myCanvas"),
  ctx: document.getElementById("myCanvas").getContext("2d"),
  outputType: 'png',
  scaleDown: document.getElementById('scale-down'),
  resetScale: document.getElementById('reset-scale'),
  images: {},
  numImages: 0,
  awards: null,
  imageData: null,
  scaleW: 1,
  scaleY: 1,
  rack: null
});
<canvas id="myCanvas" width="330" height="45">Your browser does not support the HTML5 canvas tag.</canvas>
<div>
  <button id="scale-down" style="cursor:pointer;">
        Scale Down
    </button>
  <button id="reset-scale" style="cursor:pointer;">
        Reset
    </button>
</div>
Partho63
  • 3,117
  • 2
  • 21
  • 39
Zelf
  • 1,723
  • 2
  • 23
  • 40
  • It is the image hosting server that allows cross domain by adding the appropriate CORS header to the response. If this is not present in the response from the server then you can not access the pixel data contained in the image. – Blindman67 Oct 28 '17 at 15:02
  • Access-Control-Allow-Origin: * is being returned so there should be no CORS problems. – Zelf Mar 16 '18 at 18:09
  • Problem solved: Was missing Rack.conf.images[award.sku].crossOrigin = 'anonymous'; – Zelf Mar 16 '18 at 19:16
  • These two urls probably explains whey you needed that. https://github.com/locomotivecms/engine/issues/1152 and https://stackoverflow.com/questions/20027839/todataurl-throw-uncaught-security-exception/27260503#27260503 – Tarun Lalwani Mar 16 '18 at 19:45

2 Answers2

10

You already know the answer to your question now, which is adding Rack.conf.images[award.sku].crossOrigin = 'anonymous';

Below is the thread with the similar issue explaining the need

https://github.com/locomotivecms/engine/issues/1152

toDataURL throw Uncaught Security exception

https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image

What is a "tainted" canvas?

Although you can use images without CORS approval in your canvas, doing so taints the canvas. Once a canvas has been tainted, you can no longer pull data back out of the canvas. For example, you can no longer use the canvas toBlob(), toDataURL(), or getImageData() methods; doing so will throw a security error.

This protects users from having private data exposed by using images to pull information from remote web sites without permission.

Community
  • 1
  • 1
Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
2

Problem solved: Was missing Rack.conf.images[award.sku].crossOrigin = 'anonymous';

Zelf
  • 1,723
  • 2
  • 23
  • 40