2

I'm passing an ImageBitmap into a webworker, to render on canvas and then generate a URL for loading into THREE.js on the main thread.

in the main thread

 this.canvas = this.canvasEl.transferControlToOffscreen() 
this.workerInstance.postMessage({canvas: this.canvas}, [this.canvas]);
...

createImageBitmap(this.img).then(imageData =>  {
        this.imageBitmap = imageData
        this.workerInstance.postMessage({image:imageData}, [imageData])

In the worker

_ctx.drawImage(image, 0, 0)
    _canvas.convertToBlob({type: "image/png"}).then(blob => console.log(blob))

DOMException: Failed to execute 'convertToBlob' on 'OffscreenCanvas': Tainted "OffscreenCanvas" may not be exported.

The image on the main thread has crossorigin="anonymous". It also does have another image copied to it but this is same domain.

The image is created dynamically:

docString = '<svg width="' + this.width + '" height="' + this.height + '" xmlns="http://www.w3.org/2000/svg"><defs><style type="text/css"><![CDATA[a[href]{color:#0000EE;text-decoration:underline;}' + this.cssembed.join('') + ']]></style></defs><foreignObject x="0" y="0" width="' + this.width + '" height="' + this.height + '">' + parent[0] + docString + parent[1] + '</foreignObject></svg>';
this.img.src = "data:image/svg+xml;utf8," + encodeURIComponent(docString);

THe code is here https://github.com/supereggbert/aframe-htmlembed-component/blob/master/src/htmlcanvas.js

I'm updating it to use Offscreen canvas and web workers for faster renderering

beek
  • 3,522
  • 8
  • 33
  • 86
  • Did you ever load that image without cross-origin? Might be a cache issue, see https://stackoverflow.com/questions/49503171/the-image-tag-with-crossorigin-anonymous-cant-load-success-from-s3/49503414#49503414 Anyway we'll need to see how you did load that image, because an image loaded as cross-origin should not taint your canvas, and an image with the `crossorigin` attribute but which is not served correctly by the server should not be loaded at all (an thus would make createImageBitmap throw). Also, what if you just try on the 2D context of an *inscreen* canvas? – Kaiido Feb 25 '20 at 03:36
  • Ah... an svg with foreignObject... That's a very important information, and it has nothing to do with cross-origin data, as you said, you do generate that image. [There was this](https://stackoverflow.com/questions/40897039/problems-with-getting-canvas-datauri-from-svg-with-foreignobject) I wrote a while back, but it seems you are using the workaround already, maybe they did something bad with ImageBitmap, I'll check. – Kaiido Feb 25 '20 at 04:01

1 Answers1

2

This is an extension of this known bug on which I already gave an explanation here.

For your case it seems that even using the "data:URL hack-around" won't work, probably because of some other checks performed in the createImageBitmap internal steps.

I will add a comment about this in the bug report, but for the time being, you are stuck with rasterizing that svg image on a <canvas> first and use that <canvas> as source for the ImageBitmap, which is I guess quite suboptimal for your case.

if( !window.OffscreenCanvasRenderingContext2D ) {
  console.warn("Your browser doesn't support the 2D context of the OffscreenCanvas API");
}

const worker = initWorker();

const data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
  '<foreignObject width="100%" height="100%">' +
  '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
  '<em>I</em> like ' +
  '<span style="color:white; text-shadow:0 0 2px blue;">' +
  'beer</span>' +
  '</div>' +
  '</foreignObject>' +
'</svg>';
const img = new Image();
var url = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(data);

img.onload = function() {
  // create a new canvas just to rasterize that svg
  const canvas = document.createElement( 'canvas' );
  const ctx = canvas.getContext('2d');
  canvas.width = img.width;
  canvas.height = img.height;
  ctx.drawImage(img, 0, 0);
  // use that canvas as the source for the ImageBitmap
  createImageBitmap(canvas).then(bmp => {
    worker.postMessage( bmp, [bmp] );
  });
};

img.src = url;

function initWorker() {
  const script = `
    let canvas, ctx;
    onmessage = e => {
      if(e.data instanceof ImageBitmap) {
        ctx.drawImage( e.data, 0, 0);
        canvas.convertToBlob().then( blob => {
          self.postMessage( blob );
        });
      }
      else {
        canvas = e.data;
        ctx = canvas.getContext('2d');
      }
    };
`
  const url = URL.createObjectURL(new Blob([script], { type: "application/javascript" }));
  const worker = new Worker(url);
  worker.onmessage = e => console.log('from worker', e.data);
  const offscreen = document.getElementById('canvas').transferControlToOffscreen();
  worker.postMessage(offscreen, [offscreen]);
  return worker;
}
<canvas id="canvas"></canvas>

However there is one thing I still quite don't like in that you should have gotten an error before it even gets to the worker thread stating

Failed to execute 'postMessage' on 'Worker': Non-origin-clean ImageBitmap cannot be transferred.

I don't know how come your Chrome did bypass that step...

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • That is an amazing answer. So the benefit I'm trying to get of processing these images in and offscreenCanvas won't be achieved this way? i.e. to stop the UI freezing during this processing? – beek Feb 25 '20 at 04:37
  • Indeed but anyway, all the hard work is done by createImageBitmap, so even with raster images, your setup wouldn't have made you win anything. – Kaiido Feb 25 '20 at 05:41