实现图片自动压缩算法,canvas压缩图片方法

背景:

在使用某些支持webgl的图形库(eg:PIXI.js,fabric.js)场景中,如果加载的纹理超过webgl可处理的最大纹理限制,会导致渲染的纹理缺失,甚至无法显示。

方案

实现图片自动压缩算法,自动获取 webgl 支持的最大纹理大小,设置一个压缩比率,循环压缩图片的像素,直到小于最大纹理限制。

返回 canvas,方便第三方库继续处理,如果需要 image,可自行调用canvas方法转换成image。

注意:如果不需要像素处理,删除处理像素相关的代码即可。

vim imageHelp.ts

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

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)
}
相关推荐
Clownseven2 小时前
[Web服务器对决] Nginx vs. Apache vs. LiteSpeed:2025年性能、功能与适用场景深度对比
服务器·前端·nginx
TE-茶叶蛋3 小时前
React的合成事件(SyntheticEventt)
前端·javascript·react.js
GISer_Jing5 小时前
CSS-in-JS:现代前端样式管理的革新
前端·javascript·css
咖啡の猫7 小时前
JavaScript基础-作用域链
开发语言·javascript
2501_914286497 小时前
Web技术与Nginx网站环境部署
前端·nginx·php
啊啊啊~~7 小时前
css实现不确定内容的高度过渡
前端·javascript·css
tongjiwenzhang7 小时前
APPtrace 智能参数系统:重构 App 用户增长与运营逻辑
大数据·前端·重构
亲爱的马哥7 小时前
TDuckX 2.6 正式发布|API 能力开放,核心表单逻辑重构,多项实用功能上线。
java·服务器·前端
Raink老师8 小时前
制作大风车动画
前端·harmonyos·鸿蒙·案例实战