JavaScript将图像加载到离屏画布中,执行WebP转换

4
我最近使用canvas将图像转换为webp格式,使用如下代码:
const dataUrl = canvas.toDataURL('image/webp');
但是对于某些图像,例如400毫秒,这需要很长时间。
我收到了来自Chrome的警告,因为它阻塞了UI。
我想使用Offscreen Canvas在后台执行该转换。
但是:
1)我不知道应该使用哪个Offscreen Canvas: a] new OffscreenCanvas() b] canvas.transferControlToOffscreen()
2)我通过在Image对象中加载本地图像url(img.src = url)来获取本地图像的宽度和高度。但我不明白如何将Image对象传输到离屏Canvas中,以便在worker中进行以下操作:
ctx.drawImage(img, 0, 0)
因为如果我不传输图像,worker将不知道img。

使用JavaScript,在img之前使用占位符img,然后设置img.src = url。同时,在img上附加一个onload函数,当img加载完成时进行渲染。 - Murtaza Hussain
1个回答

10

你在这里面临一个XY 甚至 -Z 问题,但每个问题都可能有一个有用的答案,所以让我们深入探讨。


不要使用canvas API进行图像格式转换。Canvas API是有损的,无论你做什么,你都会失去原始图像的信息,即使你传递的是无损图像,画在画布上的图像也不会与原始图像相同。
如果你传递的是已经有损的格式(比如JPEG),它甚至会添加不在原始图像中的信息:压缩伪像现在成为原始位图的一部分,输出算法将把这些伪像作为应保留的信息处理,使得生成的文件可能比你输入的JPEG文件还要大。
不了解你的用例,很难给出完美的建议,但一般来说,从版本中制作不同的格式最接近原始图像,并且一旦在浏览器中绘制,你已经晚了至少三步。
现在,如果你对这个图像进行一些处理,你可能确实想要导出结果。但是你可能不需要在这里使用Web Worker。Y。根据你的描述,最耗时的阻塞时间应该是同步的toDataURL()调用。而不是使用API中的历史性错误,你应该始终使用异步且更高效的toBlob()方法。在99%的情况下,你不需要数据URL,几乎所有你想要使用数据URL做的事情都可以直接使用Blob完成。
使用这种方法,唯一剩下的重量级同步操作将是在画布上绘制,除非你缩小了一些巨大的图像,否则这不应该花费400毫秒。

但是在最新的画布上,您仍然可以通过createImageBitmap方法使其变得更好,该方法允许您异步准备图像,以便图像的解码完成,真正需要做的只是一个放置像素操作:

large.onclick = e => process('https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg');
medium.onclick = e => process('https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Black_hole_-_Messier_87.jpg/1280px-Black_hole_-_Messier_87.jpg');

function process(url) {
  convertToWebp(url)
    .then(prepareDownload)
    .catch(console.error);
}

async function convertToWebp(url) {
  if(!supportWebpExport())
  console.warn("your browser doesn't support webp export, will default to png");

  let img = await loadImage(url);
  if(typeof window.createImageBitmap === 'function') {
    img = await createImageBitmap(img);
  }
  const ctx = get2DContext(img.width, img.height);

  console.time('only sync part');
  ctx.drawImage(img, 0,0);
  console.timeEnd('only sync part');
  
  return new Promise((res, rej) => {
    ctx.canvas.toBlob( blob => {
      if(!blob) rej(ctx.canvas);
      res(blob);
    }, 'image/webp');
  });
}

// some helpers

function loadImage(url) {
  return new Promise((res, rej) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = url;
    img.onload = e => res(img);
    img.onerror = rej;
  });
}

function get2DContext(width = 300, height=150) {
  return Object.assign(
    document.createElement('canvas'),
    {width, height}
  ).getContext('2d');
}

function prepareDownload(blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'image.' + blob.type.replace('image/', '');
  a.textContent = 'download';
  document.body.append(a);
}

function supportWebpExport() {
  return get2DContext(1,1).canvas
    .toDataURL('image/webp')
    .indexOf('image/webp') > -1;
}
<button id="large">convert large image (7,416 × 4,320 pixels)</button>
<button id="medium">convert medium image (1,280 × 746 pixels)</button>


Z. 要从 Web Worker 上绘制 OffscreenCanvas 上的图像,您需要使用上面提到的 createImageBitmap 方法。确实,此方法生成的 ImageBitmap 对象是 drawImage()texImage2D()(*) 可以接受的唯一可在 Workers 中使用的 图像源 值(所有其他值均为 DOM 元素)。

此 ImageBitmap 是 可转移 的,因此您可以从主线程生成它,然后将其发送到 Worker,而无需付出任何内存成本:

main.js

const img = new Image();
img.onload = e => {
  createImageBitmap(img).then(bmp => {
    // transfer it to your worker
    worker.postMessage({
      image: bmp // the key to retrieve it in `event.data`
    },
   [bmp] // transfer it
  );
};
img.src = url;

另一种解决方案是直接从Worker中获取图像数据,并从获取的Blob生成ImageBitmap对象:

worker.js

const blob = await fetch(url).then(r => r.blob());
const img = await createImageBitmap(blob);
ctx.drawImage(img,0,0);

请注意,如果您在主页面中得到了原始图像作为Blob(例如来自<input type="file">),那么不要使用HTMLImageElement或获取方式,直接发送此Blob并从中生成ImageBitmap。

*texImage2D实际上接受更多的源图像格式,如TypedArrays和ImageData对象,但这些TypedArrays应该表示像素数据,就像ImageData一样,为了拥有这些像素数据,您可能需要在使用其他图像来源格式之一绘制图像时已经拥有了这些像素数据。


有了你的见解,我能够制作出我期望的解决方案。谢谢! - Denis TRUFFAUT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接