修复 iOS 网页在拍照预览时图片被旋转的问题

前几天用 uni-app 写了个上传图片的小案例(编译到 H5 端),当在 iPhone 上拍照上传时,发现在页面上预览的图片是发生了旋转的。

图片的 Exif 信息

Exif 全称 Exchangeable image file format,中文叫可交换图像文件格式,Exif 中存储了图片的方向信息。

EXIF Orientation Value Row #0 is: Column #0 is:
1 Top Left side
2 Top Right side
3 Bottom Right side
4 Bottom Left side
5 Left side Top
6 Right side Top
7 Right side Bottom
8 Left side Bottom

看不懂没关系,网上找了张示例图(如有侵权,请联系删之),请对号入座:

获取图片的 Orientation 信息

网上都在推荐 Exif.js 这个库,有兴趣的可以自行研究,我没用过,就不多说了。

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
/**
* 根据File对象获取图片的方向信息
* @param {File} file
* @param {Function} callback
*/
getOrientation: function(file, callback) {
var reader = new FileReader();
reader.onload = function(e) {
var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8)
{
return callback(-2);
}
var length = view.byteLength, offset = 2;
while (offset < length)
{
if (view.getUint16(offset+2, false) <= 8) return callback(-1);
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1)
{
if (view.getUint32(offset += 2, false) != 0x45786966)
{
return callback(-1);
}

var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
{
if (view.getUint16(offset + (i * 12), little) == 0x0112)
{
return callback(view.getUint16(offset + (i * 12) + 8, little));
}
}
}
else if ((marker & 0xFF00) != 0xFF00)
{
break;
}
else
{
offset += view.getUint16(offset, false);
}
}
return callback(-1);
};
reader.readAsArrayBuffer(file);
}

如果是采用 <input type="file" capture="camera" accept="image/png, image/jpeg"> 来拍照,返回的就是一个 FileList 对象,里面储存的每一个 File 对象代表用户选择的一个图片,可以直接调用上面 getOrientation 方法获取图片的方向信息

但是 uni-app 的 uni.chooseImage 接口在 H5 端返回的是一个 blob:http://xxxxxx/xxxxx 格式的路径信息,并不是一个 File 对象,所以我们需要做一些额外处理:

1、将路径转成 Blob 对象

1
2
3
4
5
6
7
8
const xhr  = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = function() {
const blobObject = this.response;
// ...
};
xhr.send();

2、将 Blob 转成 File

1
const file = new File([blobObject], 'photo.png', {type: blobObject.type});

3、调用 getOrientation,然后做相应的旋转处理

修复预览旋转

由于我的项目是基于 uni-app 进行构建的,所以这里主要是针对 uni-app 下编译的 H5 页面进行说明。

完整示例代码:

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<template>
<view class="container">
<view class="content">
<view class="photo" @click="onTokePhotoTapped">
<image src="/static/placeholder.png"></image>
<image :src="faceImage" mode="aspectFill" :style="fixOrientationStyle"></image>
</view>
<view class="field-item">
<view class="field-item-title">工号</view>
<input class="field-item-input" placeholder="请输入工号" maxlength="30" v-model="jobNumber" />
</view>
<view class="submit-button" @click="onSubmitTapped">提交</view>
</view>
</view>
</template>

<script>
export default {
data() {
return {
jobNumber: '',
faceImage: '',
fixOrientationStyle: '', // 修正图片方向
}
},

components: { PlaceholderImage },

onLoad() {

},

methods: {

onTokePhotoTapped: function() {
uni.chooseImage({
count: 1,
success: (res) => {
// this.faceImage = res.tempFilePaths[0];
/**
1、URL => Blob => File
2、获取方向
*/
const url = res.tempFilePaths[0];
const self = this;
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = function() {
self.faceImage = url;
self.fixOrientationStyle = '';
if (this.status == 200) {
const blob = this.response;
const file = new File([blob], 'photo.png', {type: blob.type});
self.getOrientation(file, (orientation) => {
switch(orientation){
case 6://需要顺时针(向左)90度旋转
self.fixOrientationStyle = 'transform: rotate(90deg);';
break;
case 8://需要逆时针(向右)90度旋转
self.fixOrientationStyle = 'transform: rotate(-90deg);';
break;
case 3://需要180度旋转
self.fixOrientationStyle = 'transform: rotate(180deg);';
break;
}
});
}
};
xhr.send();
}
});
},

onSubmitTapped: function() {
const number = this.jobNumber.replace(/(^\s*)|(\s*$)/g, '');
if (!number.length) {
uni.showToast({ icon: 'none', title: '请输入工号' });
return;
}
if (!this.faceImage.length) {
uni.showToast({ icon: 'none', title: '请选择图片' });
return;
}
uni.showLoading({ mask: true, title: '正在保存...' });
// TODO: 这里就是图片上传的代码, 直接调用相应接口即可
// 这里上传的图片还是旋转的,需要后台将图片方向修正过来
},

/**
* 获取图片旋转方向

* @param {Object} file File对象
* @param {Object} callback 回调
*/
getOrientation: function(file, callback) {
var reader = new FileReader();
reader.onload = function(e) {

var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8)
{
return callback(-2);
}
var length = view.byteLength, offset = 2;
while (offset < length)
{
if (view.getUint16(offset+2, false) <= 8) return callback(-1);
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1)
{
if (view.getUint32(offset += 2, false) != 0x45786966)
{
return callback(-1);
}

var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
{
if (view.getUint16(offset + (i * 12), little) == 0x0112)
{
return callback(view.getUint16(offset + (i * 12) + 8, little));
}
}
}
else if ((marker & 0xFF00) != 0xFF00)
{
break;
}
else
{
offset += view.getUint16(offset, false);
}
}
return callback(-1);
};
reader.readAsArrayBuffer(file);
}
}
}
</script>

<style>
page {
background-color: #EFEFF4;
font-size: 30rpx;
}
.container {
width: 100%;
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.content {
width: 600rpx;
margin-top: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.photo {
width: 400rpx;
height: 400rpx;
border-radius: 20rpx;
background-color: white;
overflow: hidden;
position: relative;
}
.photo image {
width: 100%;
height: 100%;
}
.photo image:last-child {
position: absolute;
top: 0;
left: 0;
}
.field-item {
width: calc(100% - 40rpx);
height: 88rpx;
padding: 0 20rpx;
margin: 30rpx 0 50rpx;
border-radius: 10rpx;
overflow: hidden;
background-color: white;
display: flex;
align-items: center;
}
.field-item-title {
width: 100rpx;
}
.field-item-input {
width: calc(100% - 100rpx);
height: 100%;
}
.submit-button {
width: 100%;
height: 80rpx;
border-radius: 20rpx;
background-color: #007AFF;
color: white;
display: flex;
align-items: center;
justify-content: center;
}
</style>

参考