Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【bigo】基于 AlloyCrop 的图片手势缩放、裁剪业务实践 #27

Open
Rhan2020 opened this issue Mar 18, 2021 · 0 comments
Open

Comments

@Rhan2020
Copy link

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

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

需求描述

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant