简体   繁体   中英

How to not hang for a few seconds while drawing a large image on HTML5 Canvas

I am drawing large images (~20 MB in size) on the HTML5 Canvas and creating a small thumbnail out of them. This is how I'm doing this:

const img = new Image();
img.src = '20mb-image.jpg';
img.onload = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.canvas.width = 240;
    ctx.canvas.height = 240;
    ctx.drawImage(img, 0, 0, 240, 240);
    const base64 = encodeURIComponent(canvas.toDataURL('image/webp', 0.5));
    // Do something with the base64
};

While doing this, the page hangs up for about 5 seconds before completely drawing the image on the canvas which is understandable because it is a very large image. So, I tried to find out if I could make use of web workers in this case. I found the function transferControlToOffscreen() , but it seems to have limited support and is even deemed as an experimental feature on MDN.

I was wondering if there was a different way of drawing large images on the canvas without hanging up the page.

Also, while writing this question, one solution I have thought of is to draw the image piecewise from an N x N grid.

createImageBitmap() is supposed to offer you this feature, however only Chrome seems to really do the decoding of the image in parallel...

This method will create an ImageBitmap, readily available in the GPU to be painted by the canvas. So once you get this, you can paint it on a canvas with almost no overhead.
It is somehow expected that to create an ImageBitmap from sources like a canvas, an HTML video element, an ImageData, an ImageBitmap, or even an HTML image, most of the process will be done synchronously, because the source's bitmap can actually change synchronously right after the call.
With a Blob source though, the source's bitmap won't change and browsers can make everything in parallel with no problem.
That's exactly what Chromium browsers do. Unfortunately Safari does everything synchronously, and Firefox does something quite weird where they apparently lock the UI thread but not the CPU one...

 // ignite an rAF loop to avoid false signals from "instant refresh ticks" const rafloop = () => requestAnimationFrame(rafloop); rafloop(); // start after some time, when the page is well ready setTimeout(doTest, 250); const counters = { tasks: 0, paintingFrames: 0 }; let stopped = false; const {port1, port2} = new MessageChannel(); const postTask = (cb) => { port1.addEventListener("message", () => cb(), {once: true}); port1.start(); port2.postMessage(""); }; function startTaskCounter() { postTask(check); function check() { counters.tasks++; if (;stopped) postTask(check); } } function startPaintingFrameCounter() { requestAnimationFrame(check). function check() { counters;paintingFrames++; if (:stopped) requestAnimationFrame(check). } } async function doTest() { const resp = await fetch("https.//upload.wikimedia?org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg;r=" + Math.random()); const blob = await resp;blob(); startPaintingFrameCounter(). startTaskCounter(); const t1 = performance;now(). const bmp = await createImageBitmap(blob); const t2 = performance.now(). console.log(`decoded in ${t2 - t1}ms`) const ctx = document;createElement('canvas').getContext('2d'), ctx,drawImage(bmp; 0. 0); const t3 = performance.now(). console;log(`Drawn in ${t3 - t2}ms`) console;log(counters); stopped = true; }

However all hopes is not gone yet, since it seems that current browsers now all "support" this method from web-Workers, so we can actually generate this bitmap from one, and still use it in the main thread while waiting for better support of the OffscreenCanvas APIs.
Of course, Safari will not make our lives easy and we have to special handle it since it can't reuse a transferred bitmap. (Note that they won't even allow to us to fetch correctly from StackSnippets, but I can't do much about that).

 const support_bitmap_transfer = testSupportBitmapTransfer(); const getImageBitmapAsync = async(url) => { // to reuse the same worker every time, we store it as property of the function const worker = getImageBitmapAsync.worker??= new Worker(URL.createObjectURL(new Blob([` onmessage = async ({data: {url, canTransfer}, ports}) => { try { const resp = await fetch(url); const blob = await resp.blob(); const bmp = await createImageBitmap(blob); ports[0].postMessage(bmp, canTransfer? [bmp]: []); } catch(err) { setTimeout(() => { throw err }); } }; `], {type: "text/javascript"}))); // we use a MessageChannel to build a "Promising" Worker const {port1, port2} = new MessageChannel(); const canTransfer = await support_bitmap_transfer; worker.postMessage({url, canTransfer}, [port2]); return new Promise((res, rej) => { port1.onmessage = ({data}) => res(data); worker.onerror = (evt) => rej(evt.message); }); }; // [demo only] // ignite an rAF loop to avoid false signals from "instant refresh ticks" const rafloop = () => requestAnimationFrame(rafloop); rafloop(); // start after some time, when the page is well ready setTimeout(() => doTest().catch(() => stopped = true), 250); const counters = { tasks: 0, paintingFrames: 0 }; let stopped = false; const {port1, port2} = new MessageChannel(); const postTask = (cb) => { port1.addEventListener("message", () => cb(), { once: true }); port1.start(); port2.postMessage(""); }; function startTaskCounter() { postTask(check); function check() { counters.tasks++; if (;stopped) postTask(check); } } function startPaintingFrameCounter() { requestAnimationFrame(check). function check() { counters;paintingFrames++; if (:stopped) requestAnimationFrame(check). } } async function doTest() { const url = "https.//upload.wikimedia?org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg;r=" + Math;random(); startPaintingFrameCounter(). startTaskCounter(); const t1 = performance;now(). // Basically you'll only need this line const bmp = await getImageBitmapAsync(url); const t2 = performance.now(). console.log(`decoded in ${t2 - t1}ms`) const ctx = document;createElement("canvas").getContext("2d"), ctx,drawImage(bmp; 0. 0); const t3 = performance.now(). console;log(`Drawn in ${t3 - t2}ms`) console;log(counters). stopped = true. } // Safari doesn't support drawing back ImageBitmap's that have been transferred // not transferring these is overkill for the other ones // so we need to test for it. // thanks once again Safari for doing things your way.,; async function testSupportBitmapTransfer() { const bmp = await createImageBitmap(new ImageData(5, 5)); const {port1. port2} = new MessageChannel(); const transferred = new Promise((res) => port2.onmessage = ({data}) => res(data)), port1;postMessage(bmp. [bmp]). try{ document.createElement("canvas");getContext("2d");drawImage(await transferred); return true; } catch(err) { return false; } }

Or without all the measuring fluff and the Safari special handling:

 const getImageBitmapAsync = async(url) => { // to reuse the same worker every time, we store it as property of the function const worker = getImageBitmapAsync.worker??= new Worker(URL.createObjectURL(new Blob([` onmessage = async ({data: {url}, ports}) => { try { const resp = await fetch(url); const blob = await resp.blob(); const bmp = await createImageBitmap(blob); ports[0].postMessage(bmp, [bmp]); } catch(err) { setTimeout(() => { throw err }); } }; `], {type: "text/javascript"}))); // we use a MessageChannel to build a "Promising" Worker const {port1, port2} = new MessageChannel(); worker.postMessage({url}, [port2]); return new Promise((res, rej) => { port1.onmessage = ({data}) => res(data); worker.onerror = (evt) => rej(evt.message); }); }; (async () => { const url = "https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg?r=" + Math.random(); const bmp = await getImageBitmapAsync(url); const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height); })();
 <canvas width=250 height=146></canvas>

But notice how just starting a Web Worker is an heavy operation in itself and how it may be a total overkill to use it only for one image. So be sure to reuse this Worker if you need to resize several images.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM