背景

在使用uni-app开发的过程中,有这样一个需求:考勤打卡,需要拍照后添加水印上传,起初没对照片进行压缩,后面上线使用后实施开始反馈,说有的地方网络不好,上传照片特别特别慢,让我进行优化,想到的办法肯定是压缩后再上传啦,但是呢,项目是基于uni-app开发的,不过是h5因此会有不少东西不能使用,于是通过Canvas画布重新绘制图片的方法来进行图片压缩,经过开发测试没问题后上线使用了,但是使用没多久,实施开始反馈,说是拍照打卡有问题了,拍照的时候会出现空白照片,有时候要重拍很多次才能拍到正常图片上传,于是,就有了这个问题。

原因分析

因为添加图片压缩之前是没有任何问题的,当添加图片压缩功能后就开始反馈这个情况,于是就猜测是图片压缩过程中出现的问题,朝着这个思路,开始检查代码,使用uni.chooseImage调用相机拍摄图片,获取到图片信息后进行判断,当图片文件大小超过1M就进行压缩,压缩的步骤为:

  1. 先计算出原图片的长款像素值,然后对图片进行最长边设置为1080像素的等比缩放,计算出缩放后图片的长款像素值;
  2. 使用let ctx = uni.createCanvasContext('myCanvas')获取绘图上下文;
  3. 使用ctx.drawImage(src, 0, 0, imageInfo.width, imageInfo.height)绘制缩放后的图片到canvas
  4. 使用ctx.draw(true, setTimeout(async ()=>{...},200))执行绘制,并且将绘制后的图片转为临时路径进行上传。
    这让我想到之前使用微信小程序绘制二维码出现的一个情况,iOS测试生成二维码没有任何问题,Android生成二维码的时候就会出现错乱的情况,那个原因是因为执行draw()方法后立马就去执行了获取临时路径导致,因为图片可能还没完整的绘制到canvas画布上面,就去获取图片临时路径,因此得到的是一张不完整的图片数据。

基于这个情况,我猜测,是不是延时获取图片临时路径的时间太短了,尝试把200毫秒改为3000毫秒,问题确实几乎是没有重现了,于是基本确认是绘制图片到canvas这个情节出了问题。

解决方案

我不满足于上面的解决方式。这种不能根本上解决问题,如果图片过大或者别的原因导致3秒还没完整的将图片绘制到canvas画布上,那这个问题还会重现,而且,对于绘制只需要几百甚至几十毫秒的,还需要等待几秒钟,对用户不用好。于是查阅了很多文档,在微信小程序的文档中有这样一句话:

把当前画布指定区域的内容导出生成指定大小的图片。在draw()回调里调用该方法才能保证图片导出成功。

我看我的获取图片路径也是在draw()的回调里面啊,为什么就会不行呢,猜想,难道是我加了setTimeout延时执行导致的,可是这当初不就是为了防止这种情况吗,于是怀着尝试的态度将setTimeout去掉,这个问题消失了,没有再复现,原来这个方法已经是完成后的回调了,不需要再手动延迟执行,但是我不理解的是,为什么加了延时还会出问题,不应该是更保险么。

部分代码记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* 打开相机拍照
*/
async openCamera() {
let that = this;
this.markBase64File = '';
this.orignFilePath = '';
uni.chooseImage({
sourceType: ['camera'],
count: 1,
async success(res) {
that.orignFilePath = res.tempFilePaths[0];
// 图片超过1M就进行压缩
if ((res.tempFiles[0].size / 1024) > 1024) {
let compressParam = {
src: that.orignFilePath,
canvasId: 'myCanvas'
}
await that.drawImage(compressParam);
}
that.addWatermark(that.orignFilePath);
}
})
},
/**
* base64图片转blob临时路径
*/
base64ToBlob({b64data = '', contentType = 'image/jpeg', sliceSize = 512} = {}) {
return new Promise((resolve, reject) => {
// 使用 atob() 方法将数据解码
let byteCharacters = atob(b64data);
let byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize);
let byteNumbers = [];
for (let i = 0; i < slice.length; i++) {
byteNumbers.push(slice.charCodeAt(i));
}
// 8 位无符号整数值的类型化数组。内容将初始化为 0。
// 如果无法分配请求数目的字节,则将引发异常。
byteArrays.push(new Uint8Array(byteNumbers));
}
let result = new Blob(byteArrays, {
type: contentType
})
result = Object.assign(result, {
// jartto: 这里一定要处理一下 URL.createObjectURL
preview: URL.createObjectURL(result)
});
resolve(result)
})
},
/**
* 绘制原始图片到canvas
* @param {Object} param
*/
async drawImage(param) {
const that = this;
const ctx = uni.createCanvasContext(param.canvasId);
uni.showLoading({
mask: true,
title: '加载中...'
})
let imageInfo = await that.calcCompressSize(param.src);
this.canvasStyle.width = imageInfo.width + 'px';
this.canvasStyle.height = imageInfo.height + 'px';

return new Promise((resolve, reject) => {
ctx.drawImage(param.src, 0, 0, imageInfo.width, imageInfo.height)
ctx.draw(true, async ()=>{
// draw异步是假象,
// 即使改成同步也可能获取不到正确的文件路径,因为canvas可能还没绘制完成就开始获取文件路径了
that.canvas2FilePath({
orignPath: param.src,
canvasId: 'myCanvas'
}).then(path=>{
that.orignFilePath = path;
uni.hideLoading();
resolve(true);
}).catch(err=>{
uni.hideLoading();
reject(err);
})
})
})
},
/**
* 从canvas获取图片临时路径(此过程压缩图片)
* @param {Object} param
*/
async canvas2FilePath(param) {
const that = this;
let targetImageInfo = await that.calcCompressSize(param.orignPath);
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
destWidth: targetImageInfo.width,
destHeight: targetImageInfo.height,
canvasId: param.canvasId,
fileType: 'jpeg',
success: function(res) {
resolve(res.tempFilePath)
},
fail(err) {
reject(err)
}
})
})
},
/**
* 计算压缩后图片宽高,最大边不超过1080像素
* @param {Object} imageInfo
*/
async calcCompressSize(src) {
const MAX_WIDTH = 1080;
let imageInfo = await this.getImageInfo(src);
let sourceWidth = imageInfo.width;
let sourceHeight = imageInfo.height;
let targetInfo = {};
if (sourceHeight > sourceWidth) {
targetInfo['height'] = MAX_WIDTH;
targetInfo['width'] = MAX_WIDTH * sourceWidth / sourceHeight;
} else {
targetInfo['width'] = MAX_WIDTH;
targetInfo['height'] = MAX_WIDTH * sourceHeight / sourceWidth;
}
return targetInfo;
},