背景:
在使用某些支持webgl的图形库(eg:PIXI.js,fabric.js)场景中,如果加载的纹理超过webgl可处理的最大纹理限制,会导致渲染的纹理缺失,甚至无法显示。
方案
实现图片自动压缩算法,自动获取 webgl 支持的最大纹理大小,设置一个压缩比率,循环压缩图片的像素,直到小于最大纹理限制。
返回 canvas,方便第三方库继续处理,如果需要 image,可自行调用canvas方法转换成image。
注意:如果不需要像素处理,删除处理像素相关的代码即可。
vim imageHelp.ts
/**
*
* @param imgStr image base64 | url
* @param ratio 压缩比率
* @returns 压缩后的canvas对象,获取image 使用 canvas.convertToBlob()
*/
export async function compressImage(options: { imgStr: string; ratio?: number; negate?: 0 | 1 }) {
const { imgStr, ratio = 0.5, negate = 0 } = options
const isInverted = negate == 1 // 底色是否反黑色
// 2. 添加错误处理
if (!imgStr) throw new Error('Invalid image source')
if (ratio <= 0 || ratio > 1) throw new Error('Invalid compression ratio')
try {
const img = await loadImage(imgStr)
const maxTextureSize = getMaxTextureSize()
// 5. 优化尺寸计算逻辑
const { width, height } = calculateTargetSize(img, ratio, maxTextureSize)
// 6. 使用 OffscreenCanvas 提升性能
const { canvas, ctx } = createCanvasContext(width, height)
ctx.drawImage(img, 0, 0, width, height)
// 7. 添加 Worker 终止逻辑防止内存泄漏
const worker = new CanvasWorker()
const cleanup = () => worker.terminate()
return await new Promise<{ canvas: OffscreenCanvas; width: number; height: number }>(
(resolve, reject) => {
worker.onmessage = (e) => {
try {
// imageData.data.buffer 所有权已转移,无法更新数据 imageData.data.buffer
// 重新构建 ImageData 对象
const updatedImageData = new ImageData(
new Uint8ClampedArray(e.data.buffer),
canvas.width,
canvas.height
)
// 将修改后的图像数据放回画布
ctx.putImageData(updatedImageData, 0, 0)
cleanup()
if (width > maxTextureSize || height > maxTextureSize) {
// 压缩后的图像需要缩放,保持原图像的视觉大小
ctx.scale(1 / ratio, 1 / ratio)
}
resolve({
canvas,
width,
height,
})
} catch (error) {
cleanup()
reject(error)
}
}
worker.onerror = (error) => {
cleanup()
reject(error)
}
// 8. 优化数据传输
const imageData = ctx.getImageData(0, 0, width, height)
// 传递图像数据给worker,第二个参数是一个Transferable对象,可以将数据从一个线程传递到另一个线程,而不是复制
worker.postMessage(
{
buffer: imageData.data.buffer,
targetColor: isInverted ? [0, 0, 0, 255] : [255, 255, 255, 255],
tolerance: 50, // 添加颜色容差参数
},
[imageData.data.buffer]
)
}
)
} catch (error) {
throw new Error(`Image processing failed: ${error?.message}`)
}
}
function getMaxTextureSize(): number {
const gl = document.createElement('canvas').getContext('webgl')
return gl ? gl.getParameter(gl.MAX_TEXTURE_SIZE) : 4096 // 默认值
}
function calculateTargetSize(
img: { width: number; height: number },
ratio: number,
maxSize: number
) {
let width = img.width
let height = img.height
// 压缩图像像素
while (width > maxSize || height > maxSize) {
width *= ratio
height *= ratio
}
return {
width,
height,
}
}
function createCanvasContext(width: number, height: number) {
const canvas = new OffscreenCanvas(width, height)
canvas.width = width
canvas.height = height
return {
canvas,
ctx: canvas.getContext('2d')!,
}
}
vim canvas.worker.ts
self.onmessage = (event) => {
const { buffer, targetColor, isInverted } = event.data
// 转换为 Uint8ClampedArray 进行像素级别的处理
const data = new Uint8ClampedArray(buffer);
// 遍历每个像素
for(let i = 0; i < data.length; i += 4) {
const r = data[i]; // 红色通道
const g = data[i + 1]; // 绿色通道
const b = data[i + 2]; // 蓝色通道
// 检查该像素是否为需要删除的颜色
if(r === targetColor[0] && g === targetColor[1] && b === targetColor[2]) {
// 将黑色像素设置为透明
data[i + 3] = 0; // Alpha通道设置为0
}
// 反转颜色
if(isInverted) {
data[i] = 255 - data[i]
data[i + 1] = 255 - data[i + 1]
data[i + 2] = 255 - data[i + 2]
}
}
self.postMessage(data)
}