本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。
前言
最近经常接到活动页面需要给用户定制化图片的需求,于是对之前所做过的图片裁剪业务功能、踩过的一些坑做一个总结梳理,希望对大家有参考价值。
需求描述
我们先来看一下整体的需求功能:
- 点击固定的区域可以让用户选中拍照/图库中的照片
- 随后用户可以对已选择的照片进行缩放、拖动来调整照片展示区域
- 待调用户对图片整完成后还可以选择不同的活动主题封面进行合成,生成活动相关的直播封面
功能实现
- 第一步调起拍照、相册功能,这一步可以直接使用
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 样式与我们设计图中的不一致,查看文档后也没发现有配置可以自定义样式,于是就直接使用chrome
的dom
查看工具,找到对应的节点获取到类名后进行样式覆盖即可。 - 接下来是选择不同的封面进行合成,这里可以用
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 的库,查看源码后发现其中还包含了很多我们不需要的图片处理方法,于是最后找到了一个精简版的函数,使用DataView
和readAsArrayBuffer
就足以满足我们的需求:
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
属性含义如下:
- 在获取到方向后使用
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 中的描述如下:
- 排查后发现是在全屏截图的时候
canvas
尺寸大小超出了 webview 可视区的大小。然而不同的 webview 中maximum canvas size
是不一样的,最后通过让截图区域始终小于可视区宽高,这个问题得以解决。
总结
BigoLive 营收作为一个面向全球的直播平台,拥有庞大的用户基数。和国内蒸蒸日上的互联网环境不同,很多海外用户的操作系统、网络环境仍处在中低端水平。各种终端机型、复杂的代码运行环境、网络环境都对前端开发提出了更大的挑战。我们需要在完成基础的业务功能上不断兼容更多的用户群体。
欢迎大家留言讨论,祝工作顺利、生活愉快!
我是bigo前端,下期见。