Skip to content

图片裁剪

本次将通过 canvas 实现图片裁剪的功能,上传文件后本地加载图片并渲染图片与裁剪框,裁剪框可以在图片内部中移动(记录裁剪的偏移量、大小),最终通过 canvas 的 api 实现裁剪功能,最后导出成 blob 数据。

实现步骤

  1. 通过文件域将图片上传
  2. 通过URL.createObjectURL将图片通过 img 渲染出来
  3. 在图片上铺满遮罩层,将裁剪框放置在遮罩层中,并通过事件交互设置裁剪框大小以及偏移量
  4. 设置好裁剪区域后,记录下裁剪框的偏移量、尺寸,计算出当前图片与图片原始尺寸(img.natureWidth)的缩放比,因为 canvas 渲染图片是直接渲染的图片原始尺寸,所以需要计算出缩放比例,后续 api 需要用到。
  5. 通过 ctx.drawImgae(img,图片原始 left 偏移量,图片原始 top 偏移量,图片原始裁剪宽度,图片原始裁剪高度,渲染到 canvas 的 left 偏移量,渲染到 canvas 的 top 偏移量,渲染出的宽度,渲染出的高度)渲染图片。其中图片原始 left 偏移量图片原始 top 偏移量图片原始裁剪宽度图片原始裁剪高度,需要通过计算得出的。我们知道缩放比、以及缩放后的偏移量、缩放后的尺寸,可以计算出原始的偏移量、尺寸。 计算方法为: 原始 left 偏移量:当前 left 偏移量/缩放比例 原始 top 偏移量:当前 top 偏移量/缩放比例 原始裁剪的 width:当前裁剪框 width/缩放比例 原始裁剪的 height:当前裁剪框 height/缩放比例
  6. 最后通过 canvas.toBlob 将图片导出为二进制数据,当然也可以将 Blob 包装成 File 文件

关键步骤

通过 cavansd 的 drawImage 绘制原始图片,并裁剪出对应大小、位置的图片。 剪切图像,并在画布上定位被剪切的部分:

WARNING

由于drawImage调用后,canvas 会将图片的原始尺寸渲染到画布中,而裁剪框记录的偏移量、大小都是根据实际图片来记录的,所以我们需要通过图片的原始尺寸实际尺寸计算出缩放比例,也就是实际宽度/原始宽度即可计算出缩放比例了,然后就可以context.drawImage(img, 裁剪框left偏移量/比例, 裁剪框裁剪框top宽度/比例, 裁剪框宽度/比例, 裁剪框高度/比例,画布输出left偏移量,画布输出top偏移量 , 画布输出宽度,画布输出高度);

js
context.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);
/**
 * img:要渲染哪个图片?
 * sx:从这个图片的x轴上的某个值开始裁剪
 * sy:从这个图片的y轴上的某个值开始裁剪
 * swidth:从这个图片的x轴上裁剪多宽的内容
 * sheight:从这个图片的y轴上裁剪多宽的内容
 * x:将裁剪出来的图片输出到画布的x轴上的某一处
 * y:将裁剪出来的图片输出到画布上的y轴上的某一处
 * width:输出到画布上图片的宽度
 * height:输出到画布上图片的高度
 **/
context.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);
/**
 * img:要渲染哪个图片?
 * sx:从这个图片的x轴上的某个值开始裁剪
 * sy:从这个图片的y轴上的某个值开始裁剪
 * swidth:从这个图片的x轴上裁剪多宽的内容
 * sheight:从这个图片的y轴上裁剪多宽的内容
 * x:将裁剪出来的图片输出到画布的x轴上的某一处
 * y:将裁剪出来的图片输出到画布上的y轴上的某一处
 * width:输出到画布上图片的宽度
 * height:输出到画布上图片的高度
 **/

参数值

参数描述
img规定要使用的图像、画布或视频。
sx可选。开始剪切的 x 坐标位置。
sy可选。开始剪切的 y 坐标位置。
swidth可选。被剪切图像的宽度。
sheight可选。被剪切图像的高度。
x在画布上放置图像的 x 坐标位置。
y在画布上放置图像的 y 坐标位置。
width可选。要使用的图像的宽度。(伸展或缩小图像)
height可选。要使用的图像的高度。(伸展或缩小图像)
js
// width、height裁剪框大小
// img为Image实例
// left、top为裁剪框居于原图片的偏移量
function cutter(img, width, height, left, top, cb) {
  const myCanvas = document.createElement("canvas");
  const ctx = myCanvas.getContext("2d");
  myCanvas.width = width; // 画布大小应为裁剪框大小
  myCanvas.height = height; //  画布大小应为裁剪框大小
  // 绘制图片时设置偏移量以及大小。参数6、7必须是0,可以让裁剪的内容刚好在画布中呈现。
  ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
  // 将图片导出成blob
  myCanvas.toBlob((blob) => {
    const url = URL.createObjectURL(blob);
    cb(url);
  });
}
// width、height裁剪框大小
// img为Image实例
// left、top为裁剪框居于原图片的偏移量
function cutter(img, width, height, left, top, cb) {
  const myCanvas = document.createElement("canvas");
  const ctx = myCanvas.getContext("2d");
  myCanvas.width = width; // 画布大小应为裁剪框大小
  myCanvas.height = height; //  画布大小应为裁剪框大小
  // 绘制图片时设置偏移量以及大小。参数6、7必须是0,可以让裁剪的内容刚好在画布中呈现。
  ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
  // 将图片导出成blob
  myCanvas.toBlob((blob) => {
    const url = URL.createObjectURL(blob);
    cb(url);
  });
}

实现源代码

ts
// 点击确定
const handleConfrim = () => {
  nextTick(() => {
    // 完整图片的DOM
    const imgDOM = preImgDOM.value;
    if (imgUrl && imgDOM) {
      const { naturalHeight, clientHeight } = imgDOM;
      // 裁剪框信息
      const { width, height, left, top } = cutterOffset.value;
      // 用canvans实现裁剪
      const canvas = document.createElement("canvas");
      // 画布大小就是裁剪框大小
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      if (ctx) {
        // 计算出原图片和当前图片的缩放比例
        // 这一步非常关键,因为在渲染图片时,canvas是直接将图片原尺寸渲染上去了
        // 会导致我们设置的偏移量和大小会有巨大的误差(因为我们是在被缩放过的图片调整裁剪框的位置大小)
        // 所以我们需要计算出缩放的比例
        const rate = +(clientHeight / naturalHeight).toFixed(2);
        // 根据偏移量和尺寸渲染对应位置的图片
        // 前面五个参数是渲染的图片、原图片的x坐标、原图片的y坐标、裁剪尺寸
        // 后面四个参数是canvas画布的输出位置,将图片渲染在(0,0)的位置,大小为width, height
        ctx.drawImage(
          imgDOM,
          left / rate,
          top / rate,
          width / rate,
          height / rate,
          0,
          0,
          width,
          height
        );
        // 将图片导出
        canvas.toBlob((blob) => {
          if (blob && imgFileInfo) {
            emit(
              "cutDown",
              new File([blob], imgFileInfo.name, {
                type: imgFileInfo.type,
                lastModified: imgFileInfo.lastModified,
              })
            );
          } else {
            emit("cutDown", null);
          }
          // 关闭模态框
          handleCloseBtn();
        });
      } else {
        props.otherError("画布上下文无法获取!");
        // 关闭模态框
        handleCloseBtn();
      }
    } else {
      // 请先上传图片!
      props.otherError("请先上传图片!");
    }
  });
};
// 点击确定
const handleConfrim = () => {
  nextTick(() => {
    // 完整图片的DOM
    const imgDOM = preImgDOM.value;
    if (imgUrl && imgDOM) {
      const { naturalHeight, clientHeight } = imgDOM;
      // 裁剪框信息
      const { width, height, left, top } = cutterOffset.value;
      // 用canvans实现裁剪
      const canvas = document.createElement("canvas");
      // 画布大小就是裁剪框大小
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      if (ctx) {
        // 计算出原图片和当前图片的缩放比例
        // 这一步非常关键,因为在渲染图片时,canvas是直接将图片原尺寸渲染上去了
        // 会导致我们设置的偏移量和大小会有巨大的误差(因为我们是在被缩放过的图片调整裁剪框的位置大小)
        // 所以我们需要计算出缩放的比例
        const rate = +(clientHeight / naturalHeight).toFixed(2);
        // 根据偏移量和尺寸渲染对应位置的图片
        // 前面五个参数是渲染的图片、原图片的x坐标、原图片的y坐标、裁剪尺寸
        // 后面四个参数是canvas画布的输出位置,将图片渲染在(0,0)的位置,大小为width, height
        ctx.drawImage(
          imgDOM,
          left / rate,
          top / rate,
          width / rate,
          height / rate,
          0,
          0,
          width,
          height
        );
        // 将图片导出
        canvas.toBlob((blob) => {
          if (blob && imgFileInfo) {
            emit(
              "cutDown",
              new File([blob], imgFileInfo.name, {
                type: imgFileInfo.type,
                lastModified: imgFileInfo.lastModified,
              })
            );
          } else {
            emit("cutDown", null);
          }
          // 关闭模态框
          handleCloseBtn();
        });
      } else {
        props.otherError("画布上下文无法获取!");
        // 关闭模态框
        handleCloseBtn();
      }
    } else {
      // 请先上传图片!
      props.otherError("请先上传图片!");
    }
  });
};

参考

  1. https://juejin.cn/post/7173860307574456334