11

I've learnt a lot in the last 48 hours about cross domain policies, but apparently not enough.

Following on from this question. My HTML5 game supports Facebook login. I'm trying to download profile pictures of people's friends. In the HTML5 version of my game I get the following error in Chrome.

detailMessage: "com.google.gwt.core.client.JavaScriptException: (SecurityError) ↵ stack: Error: Failed to execute 'texImage2D' on 'WebGLRenderingContext': Tainted canvases may not be loaded.

As I understand it, this error occurs because I'm trying to load an image from a different domain, but this can be worked around with an Access-Control-Allow-Origin header, as detailed in this question.

The URL I'm trying to download from is

https://graph.facebook.com/1387819034852828/picture?width=150&height=150

Looking at the network tab in Chrome I can see this has the required access-control-allow-origin header and responds with a 302 redirect to a new URL. That URL varies, I guess depending on load balancing, but here's an example URL.

https://fbcdn-profile-a.akamaihd.net/hprofile-ak-xap1/v/t1.0-1/c0.0.160.160/p160x160/11046398_1413754142259317_606640341449680402_n.jpg?oh=6738b578bc134ff207679c832ecd5fe5&oe=562F72A4&gda=1445979187_2b0bf0ad3272047d64c7bfc2dbc09a29

This URL also has the access-control-allow-origin header. So I don't understand why this is failing.

Being Facebook, and the fact that thousands of apps, games and websites display users profile pictures, I'm assuming this is possible. I'm aware that I can bounce through my own server, but I'm not sure why I should have to.

Answer

I eventually got cross domain image loading working in libgdx with the following code (which is pretty hacky and I'm sure can be improved). I've not managed to get it working with the AssetDownloader yet. I'll hopefully work that out eventually.

public void downloadPixmap(final String url, final DownloadPixmapResponse response) {
    final RootPanel root = RootPanel.get("embed-html");
    final Image img = new Image(url);
    img.getElement().setAttribute("crossOrigin", "anonymous");
    img.addLoadHandler(new LoadHandler() {

        @Override
        public void onLoad(LoadEvent event) {
            HtmlLauncher.application.getPreloader().images.put(url, ImageElement.as(img.getElement()));
            response.downloadComplete(new Pixmap(Gdx.files.internal(url)));
            root.remove(img);
        }
    });
    root.add(img);
}

interface DownloadPixmapResponse {
    void downloadComplete(Pixmap pixmap);

    void downloadFailed(Throwable e);
}
Community
  • 1
  • 1
Will Calderwood
  • 4,393
  • 3
  • 39
  • 64
  • How are you trying to use the image? placing it in an img tag seems to work, http://jsfiddle.net/f0dhLsLq/ and in a canvas: http://jsfiddle.net/f0dhLsLq/1/ – Kevin B Jun 19 '15 at 21:46
  • @KevinB The texImage2D method loads the image into a texture on the GPU. So the image has been downloaded successfully at this point. This is purely a client side thing I believe. – Will Calderwood Jun 19 '15 at 21:46
  • Relevant documentation: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL#Cross-domain_textures seems it wouldn't be possible then, since it's a cross-domain image? *"Tainted (write-only) 2D canvases can't be used as WebGL textures. A 2D becomes tainted, for example, when a cross-domain image is drawn on it."* I've never used any of this so i'm not sure. – Kevin B Jun 19 '15 at 21:49
  • @KevinB There seems to be a lot of contradiction. This in 2011 https://hacks.mozilla.org/2011/06/cross-domain-webgl-textures-disabled-in-firefox-5/ "The WebGL spec has been updated to disallow using cross-domain images as WebGL textures, ..... A non-normative section has also been added allowing cross-domain images that have CORS approval. Using CORS in this case is a way for servers to explicitly say when an image is OK to be read by cross-domain scripts. CORS support for WebGL is a high priority for us and will be implemented very soon." I'll do more research. – Will Calderwood Jun 19 '15 at 22:01
  • Reading this http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html it looks like I might need to set the crossOrigin attribute on the image. That's not exposed in GWT so it might be a hack to get this working. :-( – Will Calderwood Jun 19 '15 at 22:18
  • It is not work in Mozilla Firefox – Nikolay Korotkov Nov 02 '16 at 10:37

3 Answers3

21

are you setting the crossOrigin attribute on your img before requesting it?

var img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://graph.facebook.com/1387819034852828/picture?width=150&height=150"; 

It's was working for me when this question was asked. Unfortunately the URL above no longer points to anything so I've changed it in the example below

var img = new Image();
img.crossOrigin = "anonymous";   // COMMENT OUT TO SEE IT FAIL
img.onload = uploadTex;
img.src = "https://i.imgur.com/ZKMnXce.png"; 

function uploadTex() {
  var gl = document.createElement("canvas").getContext("webgl");
  var tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  try {
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
    log("DONE: ", gl.getError());
  } catch (e) {
    log("FAILED to use image because of security:", e);
  }
}

function log() {
  var div = document.createElement("div");
  div.innerHTML = Array.prototype.join.call(arguments, " ");
  document.body.appendChild(div);
}
<body></body>

How to check you're receiving the headers

Open your devtools, pick the network tab, reload the page, select the image in question, look at both the REQUEST headers and the RESPONSE headers.

enter image description here

The request should show your browser sent an Origin: header

The response should show you received

Access-Control-Allow-Methods: GET, OPTIONS, ...
Access-Control-Allow-Origin: *

Note, both the response AND THE REQUEST must show the entries above. If the request is missing Origin: then you didn't set img.crossOrigin and the browser will not let you use the image even if the response said it was ok.

If your request has the Origin: header and the response does not have the other headers than that server did not give permission to use the image to display it. In other words it will work in an image tag and you can draw it to a canvas but you can't use it in WebGL and any 2d canvas you draw it into will become tainted and toDataURL and getImageData will stop working

gman
  • 100,619
  • 31
  • 269
  • 393
  • This was the next thing I was going to try. The game is written in libgdx which wraps GWT. The way it all hangs together I can't see an easy way to set the crossOrigin attribute before the image is requested. Just to confirm, are you able to draw your image to a WebGL canvas? – Will Calderwood Jun 20 '15 at 17:15
  • Yes I'm able to upload my image to a WebGL canvas. See example above. – gman Jun 22 '15 at 00:59
  • That's good to know. I'm suspecting this is the answer. I'm trying to work out how to achieve this within libgdx so I can test properly. Thanks. – Will Calderwood Jun 22 '15 at 02:07
  • For me it gives another error. Image from origin 'http://thumb101.shutterstock.com' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8383' is therefore not allowed access. – Syamesh K Jun 13 '16 at 05:58
  • That means `thumbs101.shuttlestock.com` does not give permission to use the image in the browser in WebGL. You'd have to contact `shuttlestock.com` and ask them to update their server to give permission or host the images somewhere else. imgur and flickr both currently give permission for these uses but most sites do not. – gman Jun 13 '16 at 06:42
  • @gman - How can I verify that indeed it is the case that a certain domain does not give permission to use an image in the browser in WebGL? I have a similar issue in this question: https://stackoverflow.com/questions/45088620/three-js-tainted-canvas-and-cors. – Matteo Jul 13 '17 at 21:21
  • added debugging details to answer – gman Jul 14 '17 at 04:27
  • @gman - This was super valuable information! Thanks for updating the answer! – Matteo Jul 14 '17 at 16:41
0

this is a classic crossdomain issue that happens when you're developing locally.

I use python's simple server as a quick fix for this.

navigate to your directory in the terminal, then type:

 $ python -m SimpleHTTPServer

and you'll get

Serving HTTP on 0.0.0.0 port 8000 ...

so go to 0.0.0.0:8000/ and you should see the problem resolved.

jared
  • 652
  • 9
  • 19
-2

You can base64 encode your texture.

Anon
  • 117
  • 1
  • 3