最近在调试一个bug,项目刚接手几天,刚开始还一脸懵逼的,后来对这个bug的解决思路渐渐轻车熟路,好了,废话不说。。。
项目中使用了一个叫 localResizeIMG 前端控件进行图片的选择,加载,base64压缩,再而上传到后台服务器。
在处理bug的过程中,学习了两个知识: 1.js对图片进行base64压缩; 2.关于图片中的EXIF中的Orientation(方向)信息。
下面,分别用于记录所学到的两个知识:
1.js对图片进行base64压缩
网上查阅了一些资料,基本的思路如下:
1.收到传入的文件后,创建一个 canvas 对象 和 blob 对象(其实也就是对应的文件引用,所以能被 img src直接引用)
2.创建 img 对象,标记允许跨域处理,src 设置为 blob,接下来就是开始压缩了。
3.开始压缩,获取图片旋转的方向(EXIF中的Orientation信息),计算用户设置的尺寸,设置canvas,然后压缩成base64
以下摘取localResizeIMG的一段代码进行剖析
function Lrz (file, opts) {
var that = this;
if (!file) throw new Error('没有收到图片,可能的解决方案:https://github.com/think2011/localResizeIMG/issues/7');
opts = opts || {};
that.defaults = {
width : null,
height : null,
fieldName: 'file',
quality : 0.7
};
that.file = file;
for (var p in opts) { //配置参数
if (!opts.hasOwnProperty(p)) continue;
that.defaults[p] = opts[p];
}
return this.init(); //初始化
}
Lrz.prototype.init = function () {
var that = this,
file = that.file,
fileIsString = typeof file === 'string',
fileIsBase64 = /^data:/.test(file),
img = new Image(), //创建img对象
canvas = document.createElement('canvas'), //创建canvas,用于后续的图片加载,旋转压缩
blob = fileIsString ? file : URL.createObjectURL(file); //创建bolb用于存放图片二进制数据
that.img = img;
that.blob = blob;
that.canvas = canvas;
if (fileIsString) {
that.fileName = fileIsBase64 ? 'base64.jpg' : (file.split('/').pop());
} else {
that.fileName = file.name;
}
if (!document.createElement('canvas').getContext) {
throw new Error('浏览器不支持canvas');
}
return new Promise(function (resolve, reject) {
img.onerror = function () {
var err = new Error('加载图片文件失败');
reject(err);
throw err;
};
img.onload = function () {
that._getBase64()
.then(function (base64) {
if (base64.length < 10) {
var err = new Error('生成base64失败');
reject(err);
throw err;
}
return base64;
})
.then(function (base64) {
var formData = null;
// 压缩文件太大就采用源文件,且使用原生的FormData() @source #55
if (typeof that.file === 'object' && base64.length > that.file.size) {
formData = new FormData();
file = that.file;
} else {
formData = new BlobFormDataShim.FormData();
file = dataURItoBlob(base64);
}
formData.append(that.defaults.fieldName, file, (that.fileName.replace(/\..+/g, '.jpg')));
resolve({
formData : formData,
fileLen : +file.size,
base64 : base64,
base64Len: base64.length,
origin : that.file,
file : file
});
// 释放内存
for (var p in that) {
if (!that.hasOwnProperty(p)) continue;
that[p] = null;
}
URL.revokeObjectURL(that.blob);
});
};
// 如果传入的是base64在移动端会报错
!fileIsBase64 && (img.crossOrigin = "*");
img.src = blob;
});
};
Lrz.prototype._getBase64 = function () {
var that = this,
img = that.img,
file = that.file,
canvas = that.canvas;
return new Promise(function (resolve) {
try {
// 传入blob在android4.3以下有bug
exif.getData(typeof file === 'object' ? file : img, function () {
that.orientation = exif.getTag(this, "Orientation");
that.resize = that._getResize();
that.ctx = canvas.getContext('2d');
canvas.width = that.resize.width;
canvas.height = that.resize.height;
// 设置为白色背景,jpg是不支持透明的,所以会被默认为canvas默认的黑色背景。
that.ctx.fillStyle = '#fff';
that.ctx.fillRect(0, 0, canvas.width, canvas.height);
// 根据设备对应处理方式
if (UA.oldIOS) {
that._createBase64ForOldIOS().then(resolve);
}
else {
that._createBase64().then(resolve);
}
});
} catch (err) {
// 这样能解决低内存设备闪退的问题吗?
throw new Error(err);
}
});
};
Lrz.prototype._createBase64ForOldIOS = function () {
var that = this,
img = that.img,
canvas = that.canvas,
defaults = that.defaults,
orientation = that.orientation;
return new Promise(function (resolve) {
require(['megapix-image'], function (MegaPixImage) {
var mpImg = new MegaPixImage(img);
if ("5678".indexOf(orientation) > -1) {
mpImg.render(canvas, {
width : canvas.height,
height : canvas.width,
orientation: orientation
});
} else {
mpImg.render(canvas, {
width : canvas.width,
height : canvas.height,
orientation: orientation
});
}
resolve(canvas.toDataURL('image/jpeg', defaults.quality));
});
});
};
Lrz.prototype._createBase64 = function () {
var that = this,
resize = that.resize,
img = that.img,
canvas = that.canvas,
ctx = that.ctx,
defaults = that.defaults,
orientation = that.orientation;
// 调整为正确方向
switch (orientation) {
case 3:
ctx.rotate(180 * Math.PI / 180);
ctx.drawImage(img, -resize.width, -resize.height, resize.width, resize.height);
break;
case 6:
ctx.rotate(90 * Math.PI / 180);
ctx.drawImage(img, 0, -resize.width, resize.height, resize.width);
break;
case 8:
ctx.rotate(270 * Math.PI / 180);
ctx.drawImage(img, -resize.height, 0, resize.height, resize.width);
break;
case 2:
ctx.translate(resize.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, 0, 0, resize.width, resize.height);
break;
case 4:
ctx.translate(resize.width, 0);
ctx.scale(-1, 1);
ctx.rotate(180 * Math.PI / 180);
ctx.drawImage(img, -resize.width, -resize.height, resize.width, resize.height);
break;
case 5:
ctx.translate(resize.width, 0);
ctx.scale(-1, 1);
ctx.rotate(90 * Math.PI / 180);
ctx.drawImage(img, 0, -resize.width, resize.height, resize.width);
break;
case 7:
ctx.translate(resize.width, 0);
ctx.scale(-1, 1);
ctx.rotate(270 * Math.PI / 180);
ctx.drawImage(img, -resize.height, 0, resize.height, resize.width);
break;
default:
ctx.drawImage(img, 0, 0, resize.width, resize.height);
}
return new Promise(function (resolve) {
if (UA.oldAndroid || UA.mQQBrowser || !navigator.userAgent) {
require(['jpeg_encoder_basic'], function (JPEGEncoder) {
var encoder = new JPEGEncoder(),
img = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve(encoder.encode(img, defaults.quality * 100));
})
}
else {
resolve(canvas.toDataURL('image/jpeg', defaults.quality));
}
});
};
Lrz.prototype._getResize = function () {
var that = this,
img = that.img,
defaults = that.defaults,
width = defaults.width,
height = defaults.height,
orientation = that.orientation;
var ret = {
width : img.width,
height: img.height
};
if ("5678".indexOf(orientation) > -1) {
ret.width = img.height;
ret.height = img.width;
}
// 如果原图小于设定,采用原图
if (ret.width < width || ret.height < height) {
return ret;
}
var scale = ret.width / ret.height;
if (width && height) {
if (scale >= width / height) {
if (ret.width > width) {
ret.width = width;
ret.height = Math.ceil(width / scale);
}
} else {
if (ret.height > height) {
ret.height = height;
ret.width = Math.ceil(height * scale);
}
}
}
else if (width) {
if (width < ret.width) {
ret.width = width;
ret.height = Math.ceil(width / scale);
}
}
else if (height) {
if (height < ret.height) {
ret.width = Math.ceil(height * scale);
ret.height = height;
}
}
// 超过这个值base64无法生成,在IOS上
while (ret.width >= 3264 || ret.height >= 2448) {
ret.width *= 0.8;
ret.height *= 0.8;
}
return ret;
};
/**
* 获取当前js文件所在路径,必须得在代码顶部执行此函数
* @returns {string}
*/
function getJsDir (src) {
var script = null;
if (src) {
script = [].filter.call(document.scripts, function (v) {
return v.src.indexOf(src) !== -1;
})[0];
} else {
script = document.scripts[document.scripts.length - 1];
}
if (!script) return null;
return script.src.substr(0, script.src.lastIndexOf('/'));
}
/**
* 转换成formdata
* @param dataURI
* @returns {*}
*
* @source http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
*/
function dataURItoBlob (dataURI) {
// convert base64/URLEncoded data component to raw binary data held in a string
var byteString;
if (dataURI.split(',')[0].indexOf('base64') >= 0)
byteString = atob(dataURI.split(',')[1]);
else
byteString = unescape(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to a typed array
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new BlobFormDataShim.Blob([ia.buffer], {type: mimeString});
}
2.图片的EXIF-Orientation信息
查询了一些相关的文章,简单的了解到EXIF-Orientation信息,原来在我们拍照的时候,拍照完成了,有一些相机是加入了EXIF-Orientation信息进去图片,这样一来,在我们查看相片的时候,它那个查看器会自动帮你旋转照片,而不用像以前那样,怎么照的就怎么显示,看的时候,还得自己去转动相机。
EXIF-Orientation信息的可参阅:Exif的Orientation信息说明
我从上面这篇博文摘取一段下来作为记录吧。
其中1836是我们最常用的几个旋转,第一行的红框,就是我们相机拍摄时的旋转状态,第二行,就是我们相机摆正后,图片实际存储的状态,到第三行,我们通过相机查看图片的时候,相机根据图片存入的EXIF-Orientation信息进行旋转,比如这里的8,在说明你拍照的时候,相机是逆时针旋转90度拍照,当你相机顺时针旋转90度来查看的时候,那么相机就会把图片逆时针选择90度,这样,在摆正相机的时候,就看到的是一个天在天,地在地的照片。
除了1836,还有另外的2754的EXIF-Orientation信息,分表就是通过1的镜面翻转后再进行角度旋转,看图理解起来有点困难,可以参照以下的表格辅助理解。
参数 | 0行(未旋转上) | 0列(未旋转左) | 旋转(方法很多) |
1 | 上 | 左 | 0° |
2 | 上 | 右 | 水平翻转 |
3 | 下 | 右 | 180° |
4 | 下 | 左 | 垂直翻转 |
5 | 左 | 上 | 顺时针90°+水平翻转 |
6 | 右 | 上 | 顺时针90° |
7 | 右 | 下 | 顺时针90°+垂直翻转 |
8 | 左 | 下 | 逆时针90° |
好了,此博客记录到此,如有错误或不解之处,望各位读者留下您宝贵的评论,thanks!