我们使用uploader
组件来进行测试,用ios
手机去拍照,手机竖着去拍照时,得到的图片会逆时针旋转90度
,横着拍没问题。下面图中的左一即为横拍,右边的两个竖拍就很明显不对劲了,手机上我们拍的照片所呈现的都是竖屏照片,上传后就变成横屏的了,这无疑给用户增加了不适感,不合适,那我们需要针对这去改一下。
解决思路:获取到照片拍摄的方向角度,然后使用canvas去进行修正
随着这个思路,我们需要了解一下EXIF
这个概念。EXIF
,可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。
google
了一下,有个exif.js
可以让我们轻松的取到图片的Orientation
,即为照片的拍摄方向,它的值为1-8,默认竖拍为1。
//获取照片方向角属性,用户旋转控制
EXIF.getData(file, function() {
// alert(EXIF.pretty(this));
EXIF.getAllTags(this);
//alert(EXIF.getTag(this, 'Orientation'));
Orientation = EXIF.getTag(this, 'Orientation');
console.log(Orientation, '===')
});
orientation值 | 旋转角度 |
---|---|
1 | 0° |
3 | 180° |
6 | 顺时针90° |
8 | 逆时针90° |
为了解决一个获取Orientation
值问题去引入一个js
库,不太值得。weui.js
中也有个upload
组件,并且它单独处理了image
的这种旋转,提供了方法。
function getOrientation(buffer){
var view = new DataView(buffer); // buffer是图片字节码流
// 每一个JPEG文件的内容都开始于一个二进制的值 '0xFFD8', 并结束于二进制值'0xFFD9', 是个标记
// 标记的格式 0xFF+标记号(1个字节)+数据大小描述符(2个字节)+数据内容(n个字节)
if (view.getUint16(0, false) != 0xFFD8) return -2;
var length = view.byteLength, offset = 2;
while (offset < length) {
var marker = view.getUint16(offset, false);
offset += 2;
// Exif 使用 APP1(0xFFE1)标记来避免与JFIF格式的 冲突. 且每一个 Exif 文件格式都开始于它
// 图片的数据域就存在APP1中
if (marker == 0xFFE1) {
if (view.getUint32(offset += 2, false) != 0x45786966) return -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++)
// Orientation 存在 0x0112 中
if (view.getUint16(offset + (i * 12), little) == 0x0112)
return view.getUint16(offset + (i * 12) + 8, little);
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
return -1;
}
看懂这段代码是很难的,如果不知道exif内容的话。Exif 信息就是由数码相机在拍摄过程中采集一系列的信息,然后把信息放置在我们熟知的 JPEG/TIFF 文件的头部,也就是说 Exif信息是镶嵌在 JPEG/TIFF 图像文件格式内的一组拍摄参数,它就好像是傻瓜相机的日期打印功能一样,只不过 Exif信息所记录的资讯更为详尽和完备。Exif 所记录的元数据信息非常丰富,主要包含了以下几类信息:
- 拍摄日期
- 摄器材(机身、镜头、闪光灯等)
- 拍摄参数(快门速度、光圈F值、ISO速度、焦距、测光模式等
- 图像处理参数(锐化、对比度、饱和度、白平衡等)
- 图像描述及版权信息
- GPS定位数据
- 缩略图
这里面就包含了图片的角度信息,就是说你用手机拍照时是不是倒着拍还是侧着拍,这些都是有记录的。下图就是exif所携带的照片信息,而我们所关注的角度就在0x0112。
// base64转arrayBuffer字节码
function dataURItoBuffer(dataURI){
var byteString = atob(dataURI.split(',')[1]);
var buffer = new ArrayBuffer(byteString.length);
var view = new Uint8Array(buffer);
for (var i = 0; i < byteString.length; i++) {
view[i] = byteString.charCodeAt(i);
}
return buffer;
}
最重要的图片旋转在这里,结合上面我们所了解的exif信息,对canvas绘制的image进行相应的旋转调整。
function orientationHelper(canvas, ctx, orientation) {
const w = canvas.width, h = canvas.height;
if(orientation > 4){
canvas.width = h;
canvas.height = w;
}
switch (orientation) {
case 2:
ctx.translate(w, 0);
ctx.scale(-1, 1);
break;
case 3:
ctx.translate(w, h);
ctx.rotate(Math.PI);
break;
case 4:
ctx.translate(0, h);
ctx.scale(1, -1);
break;
case 5:
ctx.rotate(0.5 * Math.PI);
ctx.scale(1, -1);
break;
case 6:
ctx.rotate(0.5 * Math.PI);
ctx.translate(0, -h);
break;
case 7:
ctx.rotate(0.5 * Math.PI);
ctx.translate(w, -h);
ctx.scale(-1, 1);
break;
case 8:
ctx.rotate(-0.5 * Math.PI);
ctx.translate(-w, 0);
break;
}
}
到这,图片角度修正是完成了。我们还需要对图片进行压缩,因为现在移动端手机相机像素高,随手一拍动辄4-5m
,可能会对服务器造成极大压力。不仅如此,移动端input
选取文件然后渲染成图片,通常这种都是将获取到的文件流转成base64
,可能一个文件是1m
,转成base64
就变成了4m
甚至更多,这对移动端渲染也是个性能消耗。
因此前端还需要对图片进行一些压缩处理,压缩图片也并不是直接把图片绘制到canvas
再调用一下toDataURL
就行的,我们需要考虑一些方面的因素。
在IOS
中,canvas
绘制图片是有两个限制的:
首先是图片的大小,如果图片的大小超过两百万像素,图片也是无法绘制到canvas
上的,调用drawImage
的时候不会报错,但是你用toDataURL
获取图片数据的时候获取到的是空的图片数据。
再者就是canvas
的大小有限制,如果canvas
的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。
应对第一种限制,处理办法就是瓦片绘制了。瓦片绘制,也就是将图片分割成多块绘制到canvas
上,我代码里的做法是把图片分割成100万像素
一块的大小,再绘制到canvas
上。
而应对第二种限制,我的处理办法是对图片的宽高进行适当压缩,我代码里为了保险起见,设的上限是四百万像素,如果图片大于四百万像素就压缩到小于四百万像素。四百万像素的图片应该够了,算起来宽高都有2000X2000了。
如此一来就解决了IOS
上的两种限制了。
除了上面所述的限制,还有两个坑,一个就是canvas
的toDataURL
是只能压缩jpg
的,当用户上传的图片是png
的话,就需要转成jpg
,也就是统一用canvas.toDataURL('image/jpeg', 0.1)
, 类型统一设成jpeg
,而压缩比就自己控制了。
另一个就是如果是png
转jpg
,绘制到canvas
上的时候,canvas
存在透明区域的话,当转成jpg
的时候透明区域会变成黑色,因为canvas
的透明像素默认为rgba(0,0,0,0)
,所以转成jpg
就变成rgba(0,0,0,1)
了,也就是透明背景会变成了黑色。解决办法就是绘制之前在canvas
上铺一层白色的底色。
function compressImage (img, quatity) {
var initSize = img.src.length;
var width = img.width;
var height = img.height;
// 如果图片大于四百万像素,计算压缩比并将大小压至400万以下
var ratio;
if ((ratio = width * height / 4000000) > 1) {
ratio = Math.sqrt(ratio);
width /= ratio;
height /= ratio;
} else {
ratio = 1;
}
// 用于压缩图片的canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext('2d');
// 瓦片canvas
var tCanvas = document.createElement("canvas");
var tctx = tCanvas.getContext("2d");
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
//如果图片像素大于100万则使用瓦片绘制
var count;
if ((count = width * height / 1000000) > 1) {
count = ~~(Math.sqrt(count) + 1); //计算要分成多少块瓦片
// 计算每块瓦片的宽和高
var nw = ~~(width / count);
var nh = ~~(height / count);
tCanvas.width = nw;
tCanvas.height = nh;
for (var i = 0; i < count; i++) {
for (var j = 0; j < count; j++) {
tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);
ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);
}
}
} else {
ctx.drawImage(img, 0, 0, width, height);
}
//进行最小压缩
var ndata = canvas.toDataURL("image/jpeg", quatity);
console.log("压缩前:" + initSize);
console.log("压缩后:" + ndata.length);
console.log("压缩率:" + ~~(100 * (initSize - ndata.length) / initSize) + "%");
tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;
return ndata;
}
weui
采用了另外的处理方式,看起来更加简洁优雅。
/**
* 压缩图片
*/
function compress(file, options, callback) {
const reader = new FileReader();
reader.onload = function (evt) {
// 启用压缩
const img = new Image();
img.onload = function () {
// 拍照在IOS7或以下的机型会出现照片被压扁的bug
const ratio = detectVerticalSquash(img);
// 获取拍摄角度
const orientation = getOrientation(dataURItoBuffer(img.src));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 可配置宽高压缩
const maxW = options.compress.width;
const maxH = options.compress.height;
let w = img.width;
let h = img.height;
let dataURL;
if(w < h && h > maxH){
w = parseInt(maxH * img.width / img.height);
h = maxH;
}else if(w >= h && w > maxW){
h = parseInt(maxW * img.height / img.width);
w = maxW;
}
canvas.width = w;
canvas.height = h;
if(orientation > 0){
// 对图片进行角度修正
orientationHelper(canvas, ctx, orientation);
}
ctx.drawImage(img, 0, 0, w, h / ratio);
// 源码只转jpeg,jpg,我加了png的转化
if(/image\/(jpeg|jpg|png)/i.test(file.type)){
dataURL = canvas.toDataURL('image/jpeg', options.compress.quality);
}else{
dataURL = canvas.toDataURL(file.type);
}
if(options.type == 'file'){
if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
// 压缩出错,以文件方式上传的,采用原文件上传
console.warn('Compress fail, dataURL is ' + dataURL + '. Next will use origin file to upload.');
callback(file);
}else{
let blob = dataURItoBlob(dataURL);
blob.id = file.id;
blob.name = file.name;
blob.lastModified = file.lastModified;
blob.lastModifiedDate = file.lastModifiedDate;
callback(blob);
}
}else{
if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
// 压缩失败,以base64上传的,直接报错不上传
options.onError(file, new Error('Compress fail, dataURL is ' + dataURL + '.'));
callback();
}else{
file.base64 = dataURL;
callback(file);
}
}
};
img.src = evt.target.result;
};
reader.readAsDataURL(file);
}
给文件处理添加了压缩,角度修正之后,上传所得到的图片就如下图一样正常了。
相关知识链接: