Javascript 将图像加载到 Offscreen Canvas,执行 webp 转换
Javascript load Image into Offscreen Canvas, perform webp conversion
我最近使用 canvas 将图像转换为 webp,使用:
const dataUrl = canvas.toDataURL('image/webp');
但是对于某些图像,这会花费很多时间,例如 400 毫秒。
我收到来自 Chrome 的警告,因为它正在阻塞 UI。
我想使用 Offscreen Canvas 在后台执行该转换。
但是:
1) 我不知道应该使用哪个 Offscreen Canvas :
a] 新的 OffscreenCanvas()
b] canvas.transferControlToOffscreen()
2) 我在图像对象 (img.src = url) 中加载本地图像 url 以获取本地图像的宽度和高度。但我不明白如何将 Image 对象转移到屏幕外 Canvas,以便能够在 worker 中完成:
ctx.drawImage(img, 0, 0)
因为我不传图,工人不知道img。
您在这里遇到了一个 XY and even -Z problem,但每个人都可能有一个有用的答案,所以让我们深入研究。
X。 不要使用canvas API进行图片格式转换。
canvas API 是有损的,无论你做什么,你都会丢失原始图像中的信息,即使你传递无损图像,在 canvas 上绘制的图像也不会是和这张原图一样。
如果你传递一个已经有损的格式,比如 JPEG,它甚至会添加原始图像中没有的信息:压缩伪影现在是原始位图的一部分,导出算法会将这些视为它应该保留的信息,使你的文件可能比你给它的 JPEG 文件大。
不知道你的用例,很难给你完美的建议,但一般来说,使版本中的不同格式最接近原始图像,一旦在浏览器中绘制,你就已经至少晚了三步。
现在,如果您对此图像进行一些处理,您可能确实想要导出结果。
但是你在这里可能不需要这个 Web Worker。
Y。在您的描述中,阻塞时间最长的应该是同步 toDataURL()
调用。
而不是 API 中的这个历史错误,您应该始终使用异步但性能更高的 toBlob()
方法。在 99% 的情况下,无论如何你都不需要数据 URL,几乎所有你想用数据 URL 做的事情都应该直接用 Blob 完成。
使用这种方法,剩下的唯一繁重的同步操作就是在 canvas 上绘画,除非您正在缩小一些巨大的图像,否则这不应该花费 400 毫秒。
但是你无论如何都可以在最新的 canvas 上让它变得更好,这要归功于 createImageBitmap 方法,它允许你异步准备你的图像,以便图像的解码是完整的,所有需要的done 实际上只是一个 put pixels 操作:
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() and texImage2D()(*) 可以接受的唯一 image source 值,它在 Workers 中可用(所有其他值都是 DOM元素)。
这个 ImageBitmap 是 transferable,所以你可以从主线程生成它,然后将它发送给你的 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(例如来自 ),那么甚至不要采用 HTMLImageElement 的方式,也不要采用获取,直接发送这个 Blob 并从中生成 ImageBitmap。
*texImage2D其实接受更多的源图像格式,比如TypedArrays,和ImageData对象,但是这些TypedArrays应该代表像素数据,就像一个ImageData确实如此,并且为了获得此像素数据,您可能需要已经使用其他 image source 格式之一在某处绘制了图像。
我最近使用 canvas 将图像转换为 webp,使用:
const dataUrl = canvas.toDataURL('image/webp');
但是对于某些图像,这会花费很多时间,例如 400 毫秒。
我收到来自 Chrome 的警告,因为它正在阻塞 UI。
我想使用 Offscreen Canvas 在后台执行该转换。
但是:
1) 我不知道应该使用哪个 Offscreen Canvas : a] 新的 OffscreenCanvas() b] canvas.transferControlToOffscreen()
2) 我在图像对象 (img.src = url) 中加载本地图像 url 以获取本地图像的宽度和高度。但我不明白如何将 Image 对象转移到屏幕外 Canvas,以便能够在 worker 中完成:
ctx.drawImage(img, 0, 0)
因为我不传图,工人不知道img。
您在这里遇到了一个 XY and even -Z problem,但每个人都可能有一个有用的答案,所以让我们深入研究。
X。 不要使用canvas API进行图片格式转换。
canvas API 是有损的,无论你做什么,你都会丢失原始图像中的信息,即使你传递无损图像,在 canvas 上绘制的图像也不会是和这张原图一样。
如果你传递一个已经有损的格式,比如 JPEG,它甚至会添加原始图像中没有的信息:压缩伪影现在是原始位图的一部分,导出算法会将这些视为它应该保留的信息,使你的文件可能比你给它的 JPEG 文件大。
不知道你的用例,很难给你完美的建议,但一般来说,使版本中的不同格式最接近原始图像,一旦在浏览器中绘制,你就已经至少晚了三步。
现在,如果您对此图像进行一些处理,您可能确实想要导出结果。
但是你在这里可能不需要这个 Web Worker。
Y。在您的描述中,阻塞时间最长的应该是同步 toDataURL()
调用。
而不是 API 中的这个历史错误,您应该始终使用异步但性能更高的 toBlob()
方法。在 99% 的情况下,无论如何你都不需要数据 URL,几乎所有你想用数据 URL 做的事情都应该直接用 Blob 完成。
使用这种方法,剩下的唯一繁重的同步操作就是在 canvas 上绘画,除非您正在缩小一些巨大的图像,否则这不应该花费 400 毫秒。
但是你无论如何都可以在最新的 canvas 上让它变得更好,这要归功于 createImageBitmap 方法,它允许你异步准备你的图像,以便图像的解码是完整的,所有需要的done 实际上只是一个 put pixels 操作:
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() and texImage2D()(*) 可以接受的唯一 image source 值,它在 Workers 中可用(所有其他值都是 DOM元素)。
这个 ImageBitmap 是 transferable,所以你可以从主线程生成它,然后将它发送给你的 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(例如来自 ),那么甚至不要采用 HTMLImageElement 的方式,也不要采用获取,直接发送这个 Blob 并从中生成 ImageBitmap。
*texImage2D其实接受更多的源图像格式,比如TypedArrays,和ImageData对象,但是这些TypedArrays应该代表像素数据,就像一个ImageData确实如此,并且为了获得此像素数据,您可能需要已经使用其他 image source 格式之一在某处绘制了图像。