前端-Compressor.js递归压缩图片至目标体积

72 阅读3分钟

一.需求描述

将不同体积的图片压缩至目标大小,比如5M。

二.需求分析

1.较新的 iPhone 默认使用 HEIC 格式,而大多数 Android 设备仍然使用 JPEG 格式

2.压缩考虑从两个角度,一个是像素点的数量(分辨率),一个是像素点的质量。可根据实际需求进行取舍。

比如压缩后的图片是用于导出到pdf,pdf文件内嵌入的图片,照片尺寸较小,且用户没有点击图片看大图的操作可能性(如果是word,那用户可以调整图片的大小),所以压缩后图片尺寸可以比较小。相对于主流手机2k 4k的照片质量,在像素密度方面有比较大的操作空间。

比如压缩后的照片没有特定使用场景,那就不能把像素压得太低,因为用户可能要在pc端大图查看,甚至放大。此时就需要综合像素点和像素质量来调优。

注意点,对于png格式的图片,压缩往往不理想,此时可以先把格式转为jpeg,转格式的过程本身就会减小一部分体积。

三.核心方案

使用Compressor.js

压缩并不是线性的(比如压缩比0.3和0.6得到的体积并不是2倍的关系),quality的最佳取值可能需要通过测试来寻找

四.详细示例

// html
<input type="file" id="upload" accept="image/*">

// 监听上传
document.getElementById('upload').addEventListener('change', (event) => {
  const file = event.target.files[0];
  handleCompress(file)
});

// 理想的目标体积byte
const limitSize = 200000 // 200k

// 执行一次压缩
function compressFile(file, options) {
  return new Promise((resolve, reject) => {
    new Compressor(file, {
      ...options,
      success(result) {
        resolve({
          isSuccess: true,
          result
        })
      },
      error(err) {
        reject({
          isSuccess: false,
          err
        })
      },
    });
  })
}

// 整个压缩过程,分为3步
async function handleCompress (file) {
  // 设置一定的误差空间(20%),如果某次压缩后,仅比目标大一点,若再多压一次,就太小了
  if (file.size <= limitSize * 1.2) { // 不需要压缩
    return {
      isSuccess: true,
      result: file,
    }
  }
  
  const {type} = file;
  const isPng = type === 'image/png';

  console.log('原图片', file.size / 1000);

  let result
  let resultImg = file
  // 步骤1,如果是png格式,先使用convertTypes参数转一下格式,起到减小体积的效果,同时方便后续继续压缩
  if (isPng) {
    console.log('png转格式');
    try {
      result = await compressFile(file, {
        quality: 1,
        convertSize: limitSize, // 超过convertSize的图片会转换格式以进一步压缩
      })
    } catch (error) {
      result = error
    }
    if (result.isSuccess) {
      resultImg = result.result
    } else {
      console.log(result.err);
    }
  }

  console.log(isPng ? `png转格式后:${resultImg.size / 1000}` : '不是png格式,不需要转格式');
  if (!resultImg) return '压缩失败'

  // 步骤2,如果resultImg大于limitSize * 1.2,采用压缩分辨率(不超过2k)的方式压缩
  if (resultImg.size > limitSize * 1.2) {
    try {
      result = await compressFile(resultImg, {
        quality: 1, // 不降低像素点质量
        maxWidth: 1920 / 2, // 设置最大宽度,宽高根据需求,一般来说pc端可以压到2k,移动端就可以更小
        maxHeight: 1080 / 2, // 设置最大高度,插件会根据图片尺寸和设置的宽高进行计算,宽高比保持不变
      })
    } catch (error) {
      result = error
    }
    if (result.isSuccess) {
      resultImg = result.result
    } else {
      console.log(result.err);
    }
  }

  console.log('采用压缩分辨率后:', resultImg.size / 1000);
  if (!resultImg) return '压缩失败'

  // 步骤3,如果resultImg大于limitSize * 1.2,采用压缩像素点质量的方式压缩
  let compresseQualityCount = 0
  // 多次压缩,直到达到理想体积或者无法明显压缩为止
  while(resultImg.size > limitSize * 1.2) {
    try {
      result = await compressFile(resultImg, {
        quality: 0.92 // 这个数很难确认,姑且使用默认。因为压缩比和得到的体积不是线性的
      })
    } catch (error) {
      result = error
    }
    if (result.isSuccess) {
      resultImg = result.result
    } else {
      console.log(result.err);
      break
    }
    if (++compresseQualityCount >= 3) break // 当limitSize比较小时,可能到某次之后已经无法再压了,避免跳不出循环,数字3可根据情况调整
    console.log('采用压缩像素点质量后:', resultImg.size / 1000);
  }

  if (!resultImg) return '压缩失败'

  return result
  // 下载
  const blobUrl = URL.createObjectURL(resultImg);
  const a = document.createElement('a');
  a.href = blobUrl;
  a.download = file.name || 'file';
  document.body.appendChild(a);
  a.click();
  URL.revokeObjectURL(a.href);
  document.body.removeChild(a);
}