likes
comments
collection

前端指定大小缩放比例压缩图片

作者站长头像
站长
· 阅读数 18

核心内容

  • 图片压缩,包括大小和缩放
  • 瓦片绘制,针对 ios 图片的大小超过两百万像素,图片无法绘制到canvas上的问题
  • canvas的大小限制,如果canvas的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。【解决:对图片的宽高进行适当压缩】
  • canvas的toDataURLtoBlob是只能压缩图片格式为image/jpeg或者image/webp的图片,需要将图片类型设置为image/jepg
  • png类型图片,写入canvas后背景会变成黑色,手动为图片铺白色底
  • 由于无法直接设置图片大小,所以通过二分法尽可能找到最接近设置大小的值

效果预览,如下是图片设置最大10KB,缩放比例0.5

compress(file, 10, 0.5);

前端指定大小缩放比例压缩图片

您可以点击此处进行测试测试链接

准备知识

在移动端压缩图片并且上传主要涉及到 filereader 、canvas 以及 formdata 这三个h5的api。同时还包括一些blob、base64、blobURL等相关转换的知识。

通过本文你将会了解:

  • blob => dataURL(base64) FileReader.toBlob()
  • blob => blobURL URL.createObjectURLURL.revokeObjectURL
  • canvas 绘制图片 ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
  • canvas 瓦片绘制
  • 二分法求最接近值
  • 循环的异步执行 async await (此方法并非压缩方法需要,只在demo中用到)
  • canvas 压缩图片 canvas.toDataURL(type, encoderOptions)canvas.toBlob(callback, type, encoderOptions)

相关内容下文代码中会体现,如果希望详细了解也可以参考blob、base64、file相互转换;

实现

canvas为本例的核心部分,图片质量,压缩比例均需要通过canvas实现.

canvas 具有图像操作能力,支持将一个已有的图片作为图片源,来操作图像。

设计

方法接收 file 类型文件,压缩大小,压缩比例,返回压缩后的文件

/**
 * @param: (file: blob, maxSize: number, size: number[0-1])
 * @return: {blob, base64}
 */
function compress(file, maxSize, scale): Promise<> {
  ...
}

1. 接收file,并转为url(base64/objectURL)传入img, 在回调函数中处理压缩逻辑

// 利用 FileReader 将blob转base64
// function blob2base64(blob) {
//   return new Promise(function(resolve, reject) {
//     const reader = new FileReader();
//     reader.onload = (e) => {
//       resolve(e.target.result)
//     }
//     reader.readAsDataURL(blob)
//   })
// }
// const dataUrl = blob2base64(file);

// 利用URL.createObjectURL 将blob转为objectURL
const blobUrl = URL.createObjectURL(file);

const tmpImg = new Image();
tmpImg.onLoad = function(e) {
  const img = e.target;
  URL.revokeObjectURL(file); // 清理图片缓存
  // ... 压缩代码
}
tmpImg.src = blobUrl;

2. 绘制canvas

/**
 * @description 将图片绘制到画布上,并根据缩放比例进行缩放,若图片大于2000000像素则进行瓦片处理
 * @param {context: canvasContext, img: imgObj, scale: 0-1}
 */
function drawImage(context, img, scale) {
  const {
    width: sourceWidth,
    height: sourceHeight,
  } = img;
  const pSize = sourceWidth * sourceHeight;
  const targetWidth = sourceWidth * scale,
  targetHeight = sourceHeight * scale;
  const defSize = 1000; // 设置瓦片默认宽高
  const cols = pSize > 2e6 ? ~~(targetWidth / defSize) + 1 : 1; // 列数
  const rows = pSize > 2e6 ? ~~(targetHeight / defSize) + 1 : 1; // 行数
  // 瓦片绘图
  for (let i = 0; i < rows; i ++) {   // 遍历列
    for (let j = 0; j < cols; j ++) { // 遍历行
      const canvasWidth = j === (cols - 1) ? targetWidth % defSize : defSize;
      const canvasHeight = i === (rows - 1) ? targetHeight % defSize : defSize;
      const canvasLeft = j * defSize;
      const canvasTop = i * defSize;
      const imgWidth = canvasWidth / scale;
      const imgHeight = canvasHeight / scale;
      const imgLeft = canvasLeft / scale;
      const imgTop = canvasTop / scale;
      context.drawImage(img, imgLeft, imgTop, imgWidth, imgHeight, canvasLeft, canvasTop, canvasWidth, canvasHeight);
    }
  }
}

注意:demo中添加了定时器,延迟每次绘制150ms执行,并且defSize设置为50,用来展示瓦片绘制的效果,实际上并不需要

demo 代码

function drawImage(context, img, scale) {
  const {
    width: sourceWidth,
    height: sourceHeight,
  } = img;
  const targetWidth = sourceWidth * scale,
  targetHeight = sourceHeight * scale;
  // 定义sleep方法用于延迟执行
  function sleep(time) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
      }, time * 120)
    })
  }
  return new Promise(async (resolve, reject) => {
    const defSize = 50;
    const cols = ~~(targetWidth / defSize) + 1; // 列数
    const rows = ~~(targetHeight / defSize) + 1; // 行数
    // 这里注意,for循环若想要异步执行,则方法需要加上 async 关键字,并且异步方法通过 await 调用
    for (let i = 0; i < rows; i ++) {   // 遍历列
      for (let j = 0; j < cols; j ++) { // 遍历行
        const canvasWidth = j === (cols - 1) ? targetWidth % defSize : defSize;
        const canvasHeight = i === (rows - 1) ? targetHeight % defSize : defSize;
        const canvasLeft = j * defSize;
        const canvasTop = i * defSize;
        const imgWidth = canvasWidth / scale;
        const imgHeight = canvasHeight / scale;
        const imgLeft = canvasLeft / scale;
        const imgTop = canvasTop / scale;
        await sleep(1);
        context.drawImage(img, imgLeft, imgTop, imgWidth, imgHeight, canvasLeft, canvasTop, canvasWidth, canvasHeight);
      }
    }
    resolve('success');
  })
}

上述代码涉及到循环的异步执行,参考注释,另外,这种方式在 forEach 方法中无效,因为 forEach 本身为同步设计的,不支持异步

3. 压缩方法

/**
 * @description 该步骤为关键步骤,通过canvas的toDataURL和toBlob对图片进行压缩
 * @param {canvas: Canvas, quality: 0-1 图片质量}
 */
function compressBlob(canvas, quality) {
  const type = "image/jpeg"; // 只有jpeg格式图片支持图片质量压缩
  const dataURL = canvas.toDataURL(type, quality);
  return new Promise((resolve, reject) => {
    canvas.toBlob(function(blob) {
      resolve({
        dataURL,
        blob,
      });
    }, type, quality);
  })
}

4. 压缩算法,计算最接近maxSize的文件大小

这一步利用二分法获取合适的 quality , 重复调用 compressBlob(), 使得文件大小尽量接近设定值

/**
 * @param {canvas: Canvas, maxSize: number}
 */
function compressCaculation(canvas, maxSize) {
  let count = 1; // 记录循环次数,防止死循环
  let quality = 1; // 图片质量
  let maxQuality = 1, minQuality = 0; // 定义图片质量范围
  return new Promise(function(resolve, reject) {
    async function fn() {
      quality = (maxQuality + minQuality) / 2;
      // 获取压缩后结果
      let res = await compressBlob(canvas, quality);
      const { blob, dataURL } = res;
      const { size } = blob;
      console.log(`第${count}次压缩:`, quality, size);
      // 二分法获取最接近值
      function partition() {
        count ++;
        if (size > maxSize) {
          maxQuality = quality;
        } else {
          minQuality = quality;
        }
        fn(); 
      }
      if(count < 10 && size !== maxSize) { // 默认循环不大于20次,认为10次内的结果已足够接近
        partition();
      } else if(size > maxSize) { // 若限制次数之后size > maxSize,则继续执行,至结果小于maxSize
        partition();
      } else {
        resolve(res);
      }
    }
    fn();
  })
}

5. 整合方法

串联整个压缩流程

/**
 * 
 * @param {file: File|Blob, maxSize: number, scale: 0-1}
 * @return {name: string, blob: Blob, dataURL: base64}
 */
function compress(file, maxSize = 13, scale = 0.5) {
  maxSize *= 1024; // maxSize 单位为kb,这里转为b
  const {
    size: sourceSize,
    name,
  } = file;
  const fileType = name.replace(/^.*\.(\w*)$/, '$1').toLowerCase();
  if (!['jpg', 'jpeg', 'png', 'webp'].includes(fileType)) return Promise.reject('文件格式不支持!');
  const blobUrl = URL.createObjectURL(file);
  const tmpImg = new Image();
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');
  return new Promise((resolve, reject) => {
    tmpImg.onload = async function(e) {
      const img = e.target;
      URL.revokeObjectURL(file); // 清理图片缓存
      let { width, height } = img;
      width *= scale;
      height *= scale;
      //如果图片大于四百万像素,重写缩放比例比并将大小压至400万以下
      if ((width * height / 4e6) > 1) {
        scale = 4e6 / (width * height);
        width *= scale;
        height *= scale;
      }
      // 设置画布大小
      canvas.width = width;
      canvas.height = height;
      context.fillStyle = "#fff"; // 填充底色,处理png背景为黑色的问题
      context.fillRect(0, 0, width, height);
      // canvas 绘图,这一步将图片按比例缩放,并处理瓦片问题
      drawImage(context, img, scale);
      compressCaculation(canvas, maxSize).then(res => {
        console.log('压缩完成,压缩前:', sourceSize, ' => 压缩后:', res.blob.size);
        resolve({
          name,
          ...res,
        })
      })
    }
    tmpImg.src = blobUrl;
  })
}

测试

<input type="file" onchange="upload(this)" />
function upload(e) {
  [file] = e.files;
  // 最大值10M, 缩放比例0.5
  compress(file, 10, 0.5).then(res => {
    console.log(res);
  });
}

效果如下

前端指定大小缩放比例压缩图片

参考链接

MDN WEB Docs Blob-File-Base64转换