基于 AlloyCrop 的图片手势缩放、裁剪业务实践

file

本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。

前言

最近经常接到活动页面需要给用户定制化图片的需求,于是对之前所做过的图片裁剪业务功能、踩过的一些坑做一个总结梳理,希望对大家有参考价值。

需求描述

我们先来看一下整体的需求功能:

  • 点击固定的区域可以让用户选中拍照/图库中的照片
  • 随后用户可以对已选择的照片进行缩放、拖动来调整照片展示区域
  • 待调用户对图片整完成后还可以选择不同的活动主题封面进行合成,生成活动相关的直播封面

1

功能实现

  • 第一步调起拍照、相册功能,这一步可以直接使用 input 标签调用原生的拍照、相册功能。
<input type="file" accept="image/jpeg, image/png" @change="selectPhoto" />
  • 第二步涉及到图片裁剪、缩放、拖动等功能,这里我们引入了AlloyCrop 图片裁剪库,在 body 底部引入后即可在Vue中直接调用:
<script src="<%= BASE_URL %>alloy-crop.js"></script>
new AlloyCrop({
  image_src: file,
  width: clientWidth,
  height: clientWidth,
  output: 1,
  className: "m-clip-box",
  ok(base64) {
    that.picUrl = base64;
    that.prewObj.destroy();
  },
  ok_text: "Ok",
  cancel_text: "Cancel",
  cancel() {
    that.prewObj.destroy();
  },
});
  • 直接引入AlloyCrop后发现所展示的 UI 样式与我们设计图中的不一致,查看文档后也没发现有配置可以自定义样式,于是就直接使用 chromedom查看工具,找到对应的节点获取到类名后进行样式覆盖即可。
  • 接下来是选择不同的封面进行合成,这里可以用canvas进行绘制,再通过canvas.toDataURL导出我们需要的图片:
drawCanvas() {
  const canvas = document.getElementById('a3');
  const context = canvas.getContext('2d');
  context.clearRect(0, 0, 480, 480);

  const imageBg = new Image();
  const imageHeadBg = new Image();
  imageBg.setAttribute('crossOrigin', 'anonymous');
  imageHeadBg.setAttribute('crossOrigin', 'anonymous');
  const imgList = [];

  function allImgLoad(resolve) {
    if (imgList.length === 2) {
      // 绘制背景
      context.drawImage(imageBg, 0, 0, 480, 480);
      context.drawImage(imageHeadBg, 0, 0, 480, 480);
      resolve(canvas.toDataURL('image/jpeg'));
    }
  }

  imageBg.src = this.picUrl;
  imageHeadBg.src = this.selectCover;

  return new Promise((resolve) => {
    imageBg.onload = () => {
      console.log('image1 has loaded');
      imgList.push(1);
      allImgLoad(resolve);
    };
    imageHeadBg.onload = () => {
      console.log('image2 has loaded');
      imgList.push(1);
      allImgLoad(resolve);
    };
  });
},

遇到的问题

IOS 拍照后图片旋转 90 度问题
  • 在自测过程中,发现 IOS 设备在拍照之后图片会发生旋转 90 度的问题。经排查原因是 IOS 设备在拍照的过程中判断图片横屏/竖屏的方式存在差异,所以我们需要根据图片的参数,把图片通过canvas旋转成符合我们预期的方向。
  • 在获取图片属性上业内比较成熟的库是 exif.js 库,其原理是根据图片的二进制数据来获取拍照时图片的参数,包括拍照方向标签(Orientation)。在引入库后如下调用即可:
//获取图片方向
function getPhotoOrientation(img) {
  var orient;
  EXIF.getData(img, function () {
    orient = EXIF.getTag(this, "Orientation");
    console.log("orient2", orient);
  });
  return orient;
}
  • 考虑到我们为了获取图片的Orientation属性,就引入了一个 40K 的库,查看源码后发现其中还包含了很多我们不需要的图片处理方法,于是最后找到了一个精简版的函数,使用DataViewreadAsArrayBuffer就足以满足我们的需求:
function getOrientation(file, callback) {
  const reader = new FileReader();
  // eslint-disable-next-line func-names
  reader.onload = function (e) {
    const view = new DataView(e.target.result);
    if (view.getUint16(0, false) !== 0xffd8) {
      return callback(-2);
    }
    const length = view.byteLength;
    let offset = 2;
    while (offset < length) {
      if (view.getUint16(offset + 2, false) <= 8) return callback(-1);
      const marker = view.getUint16(offset, false);
      offset += 2;
      if (marker === 0xffe1) {
        // eslint-disable-next-line no-cond-assign
        if (view.getUint32((offset += 2), false) !== 0x45786966) {
          return callback(-1);
        }

        const little = view.getUint16((offset += 6), false) === 0x4949;
        offset += view.getUint32(offset + 4, little);
        const tags = view.getUint16(offset, little);
        offset += 2;
        for (let i = 0; i < tags; i += 1) {
          if (view.getUint16(offset + i * 12, little) === 0x0112) {
            return callback(view.getUint16(offset + i * 12 + 8, little));
          }
        }
        // eslint-disable-next-line no-bitwise
      } else if ((marker & 0xff00) !== 0xff00) {
        break;
      } else {
        offset += view.getUint16(offset, false);
      }
    }
    return callback(-1);
  };
  reader.readAsArrayBuffer(file);
}
  • 获取到的Orientation属性含义如下:
    Orientation
  • 在获取到方向后使用canvas进行旋转图片处理:
resetOrientation(srcBase64, cb) {
  const img = new Image();
  const { Orientation } = this;

  img.onload = () => {
    const { width, height } = img;
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    if (Orientation > 4 && Orientation < 9) {
      canvas.width = height;
      canvas.height = width;
    } else {
      canvas.width = width;
      canvas.height = height;
    }

    // transform context before drawing image
    switch (Orientation) {
      case 2:
        ctx.transform(-1, 0, 0, 1, width, 0);
        break;
      case 3:
        ctx.transform(-1, 0, 0, -1, width, height);
        break;
      case 4:
        ctx.transform(1, 0, 0, -1, 0, height);
        break;
      case 5:
        ctx.transform(0, 1, 1, 0, 0, 0);
        break;
      case 6:
        ctx.transform(0, 1, -1, 0, height, 0);
        break;
      case 7:
        ctx.transform(0, -1, -1, 0, height, width);
        break;
      case 8:
        ctx.transform(0, -1, 1, 0, 0, width);
        break;
      default:
        break;
    }

    ctx.drawImage(img, 0, 0);
    const rs = canvas.toDataURL('image/jpeg');

    cb(rs);
  };

  img.src = srcBase64;
},
画布导出结果为空的问题
  • 在 ios12 版本中发现截图后canvas.toDataURL 一直返回的是"data:,"导致获取图片为空的现象。
  • MDN 中的描述如下:
    mdn
  • 排查后发现是在全屏截图的时候canvas尺寸大小超出了 webview 可视区的大小。然而不同的 webview 中maximum canvas size 是不一样的,最后通过让截图区域始终小于可视区宽高,这个问题得以解决。

总结

BigoLive 营收作为一个面向全球的直播平台,拥有庞大的用户基数。和国内蒸蒸日上的互联网环境不同,很多海外用户的操作系统、网络环境仍处在中低端水平。各种终端机型、复杂的代码运行环境、网络环境都对前端开发提出了更大的挑战。我们需要在完成基础的业务功能上不断兼容更多的用户群体。

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值