11

HTMLCanvasElement has toDataURL(), but OffscreenCanvas does not have. What a surprise.

Ok, so how can i get this toDataURL() to work with Worker-s? I have a ready canvas (fully drawn), and can send it to a Worker. But then what can i do from there?

The only solution i have, is to manually do all operations to create an image/png. So i found this page from 2010. (I am not sure if that is what i need though.) And further it provides this code, from where it generates a PNG and makes it to base64.

And my final question:

1 - Is there some reasonable way to get toDataURL() from Worker, OR

2 - Is there any library or something designed to for this job, OR

3 - Using all functionalities of HTMLCanvasElement and OffscreenCanvas, how should the following code be adapted to replace toDataURL()?

Here are the two functions from the code im linking to. (They are really complicated for me, and i understand almost nothing from getDump())

 // output a PNG string
 this.getDump = function() {
 // compute adler32 of output pixels + row filter bytes
 var BASE = 65521; /* largest prime smaller than 65536 */
 var NMAX = 5552; /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */
 var s1 = 1;
 var s2 = 0;
 var n = NMAX;
 for (var y = 0; y < this.height; y++) {
 for (var x = -1; x < this.width; x++) {
 s1+= this.buffer[this.index(x, y)].charCodeAt(0);
 s2+= s1;
 if ((n-= 1) == 0) {
 s1%= BASE;
 s2%= BASE;
 n = NMAX;
 }
 }
 }
 s1%= BASE;
 s2%= BASE;
 write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1));
 // compute crc32 of the PNG chunks
 function crc32(png, offs, size) {
 var crc = -1;
 for (var i = 4; i < size-4; i += 1) {
 crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff);
 }
 write(png, offs+size-4, byte4(crc ^ -1));
 }
 crc32(this.buffer, this.ihdr_offs, this.ihdr_size);
 crc32(this.buffer, this.plte_offs, this.plte_size);
 crc32(this.buffer, this.trns_offs, this.trns_size);
 crc32(this.buffer, this.idat_offs, this.idat_size);
 crc32(this.buffer, this.iend_offs, this.iend_size);
 // convert PNG to string
 return "211円PNG\r\n032円\n"+this.buffer.join('');
 }

Here it is quite clear what is going on:

 // output a PNG string, Base64 encoded
 this.getBase64 = function() {
 var s = this.getDump();
 // If the current browser supports the Base64 encoding
 // function, then offload the that to the browser as it
 // will be done in native code.
 if ((typeof window.btoa !== 'undefined') && (window.btoa !== null)) {
 return window.btoa(s);
 }
 var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
 var c1, c2, c3, e1, e2, e3, e4;
 var l = s.length;
 var i = 0;
 var r = "";
 do {
 c1 = s.charCodeAt(i);
 e1 = c1 >> 2;
 c2 = s.charCodeAt(i+1);
 e2 = ((c1 & 3) << 4) | (c2 >> 4);
 c3 = s.charCodeAt(i+2);
 if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); }
 if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; }
 r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4);
 } while ((i+= 3) < l);
 return r;
 }

Thanks

asked Jun 7, 2019 at 12:26

2 Answers 2

15

First, I'll note you probably don't want a data URL of your image file, data URLs are really a less performant way to deal with files than their binary equivalent Blob, and almost you can do with a data URL can actually and should generally be done with a Blob and a Blob URI instead.

Now that's been said, you can very well still generate a data URL from an OffscreenCanvas.

This is a two step process:

const worker = new Worker(getWorkerURL());
worker.onmessage = e => console.log(e.data);
function getWorkerURL() {
 return URL.createObjectURL(
 new Blob([worker_script.textContent])
 );
}
<script id="worker_script" type="ws">
 const canvas = new OffscreenCanvas(150,150);
 const ctx = canvas.getContext('webgl');
 canvas[
 canvas.convertToBlob 
 ? 'convertToBlob' // specs
 : 'toBlob' // current Firefox
 ]()
 .then(blob => {
 const dataURL = new FileReaderSync().readAsDataURL(blob);
 postMessage(dataURL);
 });
</script>


Since what you want is actually to render what this OffscreenCanvas did produce, you'd be better to generate your OffscreenCanvas by transferring the control of a visible one.
This way you can send the ImageBitmap directly to the UI without any memory overhead.

const offcanvas = document.getElementById('canvas')
 .transferControlToOffscreen();
const worker = new Worker(getWorkerURL());
worker.postMessage({canvas: offcanvas}, [offcanvas]);
function getWorkerURL() {
 return URL.createObjectURL(
 new Blob([worker_script.textContent])
 );
}
<canvas id="canvas"></canvas>
<script id="worker_script" type="ws">
 onmessage = e => {
 const canvas = e.data.canvas;
 const gl = canvas.getContext('webgl');
 gl.viewport(0, 0,
 gl.drawingBufferWidth, gl.drawingBufferHeight);
 gl.enable(gl.SCISSOR_TEST);
 // make some slow noise (we're in a Worker)
 for(let y=0; y<gl.drawingBufferHeight; y++) {
 for(let x=0; x<gl.drawingBufferWidth; x++) {
 gl.scissor(x, y, 1, 1);
 gl.clearColor(Math.random(), Math.random(), Math.random(), 1.0);
 gl.clear(gl.COLOR_BUFFER_BIT);
 }
 }
 // draw to visible <canvas> in FF
 if(gl.commit) gl.commit();
 };
</script>

If you really absolutely need an <img>, then create a BlobURI from the generated Blob. But note that doing so, you do keep the image in memory once (which is still far better than the thrice induced by data URL, but still, don't do this with animated content).

const worker = new Worker(getWorkerURL());
worker.onmessage = e => {
 document.getElementById('img').src = e.data;
}
function getWorkerURL() {
 return URL.createObjectURL(
 new Blob([worker_script.textContent])
 );
}
img.onerror = e => {
 document.body.textContent = '';
 const a = document.createElement('a');
 a.href = "https://jsfiddle.net/5yhg2c9L/";
 a.textContent = "Your browser doesn't like StackSnippet's null origined iframe, please try again from this jsfiddle";
 document.body.append(a);
};
<img id="img">
<script id="worker_script" type="ws">
 const canvas = new OffscreenCanvas(150,150);
 const gl = canvas.getContext('webgl');
 gl.viewport(0, 0,
 gl.drawingBufferWidth, gl.drawingBufferHeight);
 gl.enable(gl.SCISSOR_TEST);
 // make some slow noise (we're in a Worker)
 for(let y=0; y<gl.drawingBufferHeight; y++) {
 for(let x=0; x<gl.drawingBufferWidth; x++) {
 gl.scissor(x, y, 1, 1);
 gl.clearColor(Math.random(), Math.random(), Math.random(), 1.0);
 gl.clear(gl.COLOR_BUFFER_BIT);
 }
 }
canvas[
 canvas.convertToBlob 
 ? 'convertToBlob' // specs
 : 'toBlob' // current Firefox
 ]()
 .then(blob => {
 const blobURL = URL.createObjectURL(blob);
 postMessage(blobURL);
 });
</script>

(Note that you could also transfer an ImageBitmap from the Worker to the main thread and then draw it on a visible canvas, but in this case, using a tranferred context is even better.)

answered Jun 7, 2019 at 13:15
Sign up to request clarification or add additional context in comments.

3 Comments

Great. Thanks. I need to assign the Worker-result to the source of a simple <img>. Can you please hint me to how i can do that more performant without Data URL? Thank you
@ituy sorry I didn't got time before now, I did edit my answer with two ways of doing this.
Thank you. Only thing to note is that over toDataURL, the toBlob + createObjectURL takes 4 times more memory in WK, and 2 times in FF. Thats when it is left idle.
4

Blob is great, but actually toBlob && convertToBlob are much much slower compared to toDataUrl on Chrome, I really dont understand why....

On chrome, a 1920*1080 canvas, toDataUrl toke me 20ms, toBlob toke 300-500ms, convertToBlob toke 800ms.

So if the performance is an issue, I would rather use toDataUrl in main thread.

answered Sep 17, 2019 at 3:09

2 Comments

If I remember correctly, convertToBlob is a very low prio async process, at least in Chrome. If the browser is busy with anything else (like running an animation that isn't 100% gpu accelerated) it ends up taking its sweet time creating the blob. So if you want consistent speed, use toDataURL (it blocks the main thread, that's why it's always 'high' prio). The downside: it blocks the main thread and working with the resulting data url is slower than working with blobs :(
Ah, I was thinking about HTMLCanvasElement.toBlob(), not OffscreenCanvas.convertToBlob() - yeah they are named differently, which is strange until you realize the former uses callbacks, the second a promise. Anyway, toBlob() runs in idle time on the main thread in Chrome, thus other things in the main thread will keep it from completing: groups.google.com/a/chromium.org/g/scheduler-dev/c/CFuzg2g-obI/…

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.