js图片裁剪和透明处理封装

记录下前端对于图片文件的方法封装,包括 File对象转base64、base64转File对象、去除图片背景色、清除图片周围空白区域

起因是项目中遇到需要上传印章图片并自动截取有效印章区域并背景透明化处理。

思路

  • 首先通过antd组件的 transformFile方法获取到用户上传的file对象。
  • 对图片进行去除图片背景的操作,对应方法为removeImgBg
  • 清除图片周围透明色区域,截取图片主体内容,方法clearImageEdgeBlank
  • 将返回的base64转为file对象,返回一个file对象和base64。方便业务取用。

具体代码如下:

index.jsx

import { Upload, Button, Icon } from 'antd';
import {removeImgBg} from 'utils'
const props = {
  action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76',
  transformFile(file) {
    return new Promise((resolve) => {
      removeImgBg(file, [255, 255, 255, 255], 40).then(res=>{
        resolve(res.file)
      })
    })
  },
};
ReactDOM.render(
  <div>
    <Upload {...props}>
      <Button>
        <Icon type="upload" /> Upload
      </Button>
    </Upload>
  </div>,
  mountNode,
);

utils.js

/**
 * File对象转base64
 * @param file File对象
 * @returns Promise
 */
export function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    // 创建一个新的 FileReader 对象
    const reader = new FileReader();
    // 读取 File 对象
    reader.readAsDataURL(file);
    // 加载完成后
    reader.onload = function () {
      // 将读取的数据转换为 base64 编码的字符串
      const base64String = reader.result.split(",")[1];
      // 解析为 Promise 对象,并返回 base64 编码的字符串
      resolve(base64String);
    };
    // 加载失败时
    reader.onerror = function () {
      reject(new Error("Failed to load file"));
    };
  });
}

/**
 * base64转File对象
 * @param base64 base64编码
 * @param fileName string 文件名
 * @returns File
 */
export function base64ToFile(base64, fileName) {
  let arr = base64.split(",");
  let mime = arr[0].match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*/)[1];
  let bstr = atob(arr[1]);
  let n = bstr.length;
  let u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], fileName, { type: mime });
}
/**
 * 获取File图片信息
 * @param fileObj Filee图片
 * @returns imageObj imageObj对象
 */
export function fileToImgObj(fileObj) {
  return new Promise((resolve, reject) => {
    var reader = new FileReader();
    reader.readAsDataURL(fileObj);
    reader.onload = function (evt) {
        var replaceSrc = evt.target.result;
        var imageObj = new Image();
        imageObj.src = replaceSrc;
        imageObj.onload = function () {
            resolve(imageObj);
        };
    };
  })
}

/**
 * 去除图片背景
 * @param fileObj File文件
 * @param rgba 去除色
 * @param tolerance 容差值
 * @returns File
 */
export async function removeImgBg(fileObj, rgba, tolerance) {
  var imgData = null;
  const [r0, g0, b0, a0] = rgba;
  var r, g, b, a;
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');
  const imageObj = await fileToImgObj(fileObj)
  const w = imageObj.width;
  const h = imageObj.height;
  canvas.width = w;
  canvas.height = h;
  context.drawImage(imageObj, 0, 0);
  imgData = context.getImageData(0, 0, w, h);
  for (let i = 0; i < imgData.data.length; i += 4) {
      r = imgData.data[i];
      g = imgData.data[i + 1];
      b = imgData.data[i + 2];
      a = imgData.data[i + 3];
      const t = Math.sqrt((r - r0) ** 2 + (g - g0) ** 2 + (b - b0) ** 2 + (a - a0) ** 2);
      if (t <= tolerance) {
          imgData.data[i] = 0;
          imgData.data[i + 1] = 0;
          imgData.data[i + 2] = 0;
          imgData.data[i + 3] = 0;
      }
  }
  context.putImageData(imgData, 0, 0);
  const newBase64 = canvas.toDataURL('image/png');
  const clearedBase64 = await clearImageEdgeBlank(newBase64, 10)
  const newFileObj = base64ToFile(clearedBase64, fileObj.name)
  const transformedFile = Object.assign(newFileObj, {
    uid: fileObj.uid,
  });
  return {
    file: transformedFile,
    base:  clearedBase64
  }
}
/**
 * 清楚图片周围空白区域
 * @param {string} url - 图片地址或base64
 * @param {number} [padding=0] - 内边距
 * @return {string} base64 - 裁剪后的图片字符串
 */
function clearImageEdgeBlank(url, padding = 0) {
  return new Promise((resolve, reject) => {
    // create canvas
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    // create image
    const image = new Image();
    image.onload = draw;
    image.src = url;
    image.crossOrigin = "Anonymous";
    function draw() {
      canvas.width = image.width;
      canvas.height = image.height;
      ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const { data, width, height } = imageData;
      // 裁剪需要的起点和终点,初始值为画布左上和右下点互换设置成极限值。
      let startX = width,
        startY = height,
        endX = 0,
        endY = 0;
      /*
      col为列,row为行,两层循环构造每一个网格,
      便利所有网格的像素,如果有色彩则设置裁剪的起点和终点
      */
      console.log('开始')
      const startTime = Date.now();
      for (let col = 0; col < width; col++) {
        for (let row = 0; row < height; row++) {
          // 网格索引
          const pxStartIndex = (row * width + col) * 4;
          // 网格的实际像素RGBA
          const pxData = {
            r: data[pxStartIndex],
            g: data[pxStartIndex + 1],
            b: data[pxStartIndex + 2],
            a: data[pxStartIndex + 3]
          };
          // 存在色彩:不透明
          const colorExist = pxData.a !== 0;
          /*
          如果当前像素点有色彩
          startX坐标取当前col和startX的最小值
          endX坐标取当前col和endX的最大值
          startY坐标取当前row和startY的最小值
          endY坐标取当前row和endY的最大值
          */
          if (colorExist) {
            startX = Math.min(col, startX);
            endX = Math.max(col, startX);
            startY = Math.min(row, startY);
            endY = Math.max(row, endY);
          }
        }
      }
      const interval = Date.now() - startTime;
      console.log('总共耗时', interval);
      // 右下坐标需要扩展1px,才能完整的截取到图像
      endX += 1;
      endY += 1;
      // 加上padding
      startX -= padding;
      startY -= padding;
      endX += padding;
      endY += padding;
      // 根据计算的起点终点进行裁剪
      const cropCanvas = document.createElement("canvas");
      const cropCtx = cropCanvas.getContext("2d");
      cropCanvas.width = endX - startX;
      cropCanvas.height = endY - startY;
      cropCtx.drawImage(
        image,
        startX,
        startY,
        cropCanvas.width,
        cropCanvas.height,
        0,
        0,
        cropCanvas.width,
        cropCanvas.height
      );
      // rosolve裁剪后的图像字符串
      resolve(cropCanvas.toDataURL());
    }
  });
}

效果预览

befor
after
before after