题意:将图像加载到离屏画布中,执行 WebP 转换的 JavaScript
问题背景:
I recently used canvas to convert images to webp, using:
我最近使用画布将图像转换为 WebP,使用了:
const dataUrl = canvas.toDataURL('image/webp');
But this takes a lots of time for certain images, like 400ms.
但对于某些图像,这需要很长时间,比如 400 毫秒。
I got a warning from Chrome, since it is blocking UI.
我收到了来自 Chrome 的警告,因为这阻塞了用户界面。
I would like to use an Offscreen Canvas to perform that conversion in background.
我想使用离屏画布在后台执行该转换。
But:
-
I don't know which Offscreen Canvas I should use: 我不知道应该使用哪个离屏画布:
new OffscreenCanvas()
canvas.transferControlToOffscreen()
-
I load a local image url in an Image object (
img.src = url
) to get width and height of the local image. But I don't understand how to transfer the Image object to the offscreen Canvas, to be able to do in the worker : 我在一个 Image 对象中加载本地图像的 URL(img.src = url),以获取本地图像的宽度和高度。但我不明白如何将 Image 对象传递给离屏画布,以便在工作线程中进行操作:ctx.drawImage(img, 0, 0)
Because If I don't transfer the image, worker doesn't know img.
因为如果我不传递图像,工作线程就不知道 img。
问题解决:
You are facing an XY and even -Z problem here, but each may have an useful answer, so let's dig in.
你在这里面临一个 XY 甚至 -Z 问题,但每个问题可能都有有用的答案,所以我们来深入探讨一下。
X. Do not use the canvas API to perform image format conversion. The canvas API is lossy, whatever you do, you will loose information from your original image, even if you do pass it lossless images, the image drawn on the canvas will not be the same as this original image.
X. 不要使用画布 API 进行图像格式转换。画布 API 是有损的,无论你怎么做,都会丢失原始图像的信息,即使你传递的是无损图像,绘制在画布上的图像也不会与原始图像相同。
If you pass an already lossy format like JPEG, it will even add information that were not in the original image: the compression artifacts are now part of the raw bitmap, and export algo will treat these as information it should keep, making your file probably bigger than the JPEG file you fed it with.
如果你传递一个已经压缩过的格式,比如JPEG,它甚至会添加原始图像中不存在的信息:压缩伪影现在成为了原始位图的一部分,导出算法会将这些视为应保留的信息,这可能会导致你的文件比你输入的JPEG文件还要大。
Not knowing your use case, it's a bit hard to give you the perfect advice, but generally, make the different formats from the version the closest to the raw image, and once it's painted in a browser, you are already at least three steps too late.
不知道你的使用场景,很难给出完美的建议,但一般来说,应该从最接近原始图像的版本生成不同的格式,而一旦它在浏览器中显示,你就至少已经晚了三步。
Now, if you do some processing on this image, you may indeed want to export the results.
现在,如果你对这张图像进行了一些处理,你可能确实想要导出结果。
But you probably don't need this Web Worker here.
但你可能不需要在这里使用这个Web Worker。
Y. What takes the biggest blocking time in your description should be the synchronous toDataURL() call.
在你的描述中,造成最大阻塞时间的应该是同步的 `toDataURL()` 调用。
Instead of this historical error in the API, you should always be using the asynchronous and nonetheless more performant toBlob() method. In 99% of the cases, you don't need a data URL anyway, almost all you want to do with a data URL should be done with a Blob directly.
在你的描述中,造成最大阻塞时间的应该是同步的 `toDataURL()` 调用。
Using this method, the only heavy synchronous operation remaining would be the painting on canvas, and unless you are downsizing some huge images, this should not take the 400ms.
使用这种方法,唯一剩下的重型同步操作就是在画布上绘制,除非你在缩小一些非常大的图像,否则这不应该耗时400毫秒。
But you can anyway make it even better on newest canvas thanks to createImageBitmap method, which allows you to prepare asynchronously your image so that the image's decoding be complete and all that needs to be done is really just a put pixels operation:
不过,得益于最新的画布和 `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. To draw an image on an OffscreenCanvas from a Web Worker, you will need the createImageBitmap
mentioned above. Indeed, the ImageBitmap object produced by this method is the only image source value that drawImage() and texImage2D()(*) can accept which is available in Workers (all other being DOM Elements).
要从 Web Worker 在 OffscreenCanvas 上绘制图像,你需要上述提到的 `createImageBitmap`。实际上,由该方法生成的 ImageBitmap 对象是 `drawImage()` 和 `texImage2D()` (*) 可以接受的唯一图像源值,而这种值在 Worker 中可用(所有其他的都是 DOM 元素)。
This ImageBitmap is transferable, so you could generate it from the main thread and then send it to you Worker with no memory cost:
这个 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;
An other solution is to fetch your image's data from the Worker directly, and to generate the ImageBitmap object from the fetched Blob:
另一种解决方案是直接从 Worker 获取图像数据,并从获取的 Blob 生成 ImageBitmap 对象:
worker.js
const blob = await fetch(url).then(r => r.blob());
const img = await createImageBitmap(blob);
ctx.drawImage(img,0,0);
And note if you got the original image in your main's page as a Blob (e.g from an <input type="file">), then don't even go the way of the HTMLImageElement, nor of the fetching, directly send this Blob and generate the ImageBitmap from it.
请注意,如果你在主页面中将原始图像作为 Blob(例如来自 `<input type="file">`),那么就不需要使用 HTMLImageElement 或进行获取,直接发送这个 Blob 并从中生成 ImageBitmap。