我們使用uploader
組件來進行測試,用ios
手機去拍照,手機豎著去拍照時往史,得到的圖片會逆時針旋轉(zhuǎn)90度
诱渤,橫著拍沒問題。下面圖中的左一即為橫拍梗醇,右邊的兩個豎拍就很明顯不對勁了,手機上我們拍的照片所呈現(xiàn)的都是豎屏照片,上傳后就變成橫屏的了贴捡,這無疑給用戶增加了不適感,不合適村砂,那我們需要針對這去改一下烂斋。
解決思路:獲取到照片拍攝的方向角度,然后使用canvas去進行修正
隨著這個思路础废,我們需要了解一下EXIF
這個概念源祈。EXIF
,可交換圖像文件格式(英語:Exchangeable image file format色迂,官方簡稱Exif)香缺,是專門為數(shù)碼相機的照片設(shè)定的,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)歇僧。
google
了一下图张,有個exif.js
可以讓我們輕松的取到圖片的Orientation
,即為照片的拍攝方向诈悍,它的值為1-8祸轮,默認豎拍為1。
//獲取照片方向角屬性侥钳,用戶旋轉(zhuǎn)控制
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值 | 旋轉(zhuǎn)角度 |
---|---|
1 | 0° |
3 | 180° |
6 | 順時針90° |
8 | 逆時針90° |
為了解決一個獲取Orientation
值問題去引入一個js
庫适袜,不太值得。weui.js
中也有個upload
組件舷夺,并且它單獨處理了image
的這種旋轉(zhuǎn)苦酱,提供了方法。
function getOrientation(buffer){
var view = new DataView(buffer); // buffer是圖片字節(jié)碼流
// 每一個JPEG文件的內(nèi)容都開始于一個二進制的值 '0xFFD8', 并結(jié)束于二進制值'0xFFD9', 是個標記
// 標記的格式 0xFF+標記號(1個字節(jié))+數(shù)據(jù)大小描述符(2個字節(jié))+數(shù)據(jù)內(nèi)容(n個字節(jié))
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 文件格式都開始于它
// 圖片的數(shù)據(jù)域就存在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內(nèi)容的話疫萤。Exif 信息就是由數(shù)碼相機在拍攝過程中采集一系列的信息,然后把信息放置在我們熟知的 JPEG/TIFF 文件的頭部敢伸,也就是說 Exif信息是鑲嵌在 JPEG/TIFF 圖像文件格式內(nèi)的一組拍攝參數(shù)扯饶,它就好像是傻瓜相機的日期打印功能一樣,只不過 Exif信息所記錄的資訊更為詳盡和完備。Exif 所記錄的元數(shù)據(jù)信息非常豐富尾序,主要包含了以下幾類信息:
- 拍攝日期
- 攝器材(機身钓丰、鏡頭、閃光燈等)
- 拍攝參數(shù)(快門速度每币、光圈F值斑粱、ISO速度、焦距脯爪、測光模式等
- 圖像處理參數(shù)(銳化则北、對比度、飽和度痕慢、白平衡等)
- 圖像描述及版權(quán)信息
- GPS定位數(shù)據(jù)
- 縮略圖
這里面就包含了圖片的角度信息尚揣,就是說你用手機拍照時是不是倒著拍還是側(cè)著拍,這些都是有記錄的掖举。下圖就是exif所攜帶的照片信息快骗,而我們所關(guān)注的角度就在0x0112。
// base64轉(zhuǎn)arrayBuffer字節(jié)碼
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;
}
最重要的圖片旋轉(zhuǎn)在這里塔次,結(jié)合上面我們所了解的exif信息方篮,對canvas繪制的image進行相應(yīng)的旋轉(zhuǎn)調(diào)整。
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;
}
}
到這励负,圖片角度修正是完成了藕溅。我們還需要對圖片進行壓縮,因為現(xiàn)在移動端手機相機像素高继榆,隨手一拍動輒4-5m
巾表,可能會對服務(wù)器造成極大壓力。不僅如此略吨,移動端input
選取文件然后渲染成圖片集币,通常這種都是將獲取到的文件流轉(zhuǎn)成base64
,可能一個文件是1m
翠忠,轉(zhuǎn)成base64
就變成了4m
甚至更多鞠苟,這對移動端渲染也是個性能消耗。
因此前端還需要對圖片進行一些壓縮處理秽之,壓縮圖片也并不是直接把圖片繪制到canvas
再調(diào)用一下toDataURL
就行的当娱,我們需要考慮一些方面的因素。
在IOS
中政溃,canvas
繪制圖片是有兩個限制的:
首先是圖片的大小趾访,如果圖片的大小超過兩百萬像素态秧,圖片也是無法繪制到canvas
上的董虱,調(diào)用drawImage
的時候不會報錯,但是你用toDataURL
獲取圖片數(shù)據(jù)的時候獲取到的是空的圖片數(shù)據(jù)。
再者就是canvas
的大小有限制愤诱,如果canvas
的大小大于大概五百萬像素(即寬高乘積)的時候云头,不僅圖片畫不出來,其他什么東西也都是畫不出來的淫半。
應(yīng)對第一種限制溃槐,處理辦法就是瓦片繪制了。瓦片繪制科吭,也就是將圖片分割成多塊繪制到canvas
上昏滴,我代碼里的做法是把圖片分割成100萬像素
一塊的大小,再繪制到canvas
上对人。
而應(yīng)對第二種限制谣殊,我的處理辦法是對圖片的寬高進行適當壓縮,我代碼里為了保險起見牺弄,設(shè)的上限是四百萬像素姻几,如果圖片大于四百萬像素就壓縮到小于四百萬像素。四百萬像素的圖片應(yīng)該夠了势告,算起來寬高都有2000X2000了蛇捌。
如此一來就解決了IOS
上的兩種限制了。
除了上面所述的限制咱台,還有兩個坑络拌,一個就是canvas
的toDataURL
是只能壓縮jpg
的,當用戶上傳的圖片是png
的話回溺,就需要轉(zhuǎn)成jpg
盒音,也就是統(tǒng)一用canvas.toDataURL('image/jpeg', 0.1)
, 類型統(tǒng)一設(shè)成jpeg
馅而,而壓縮比就自己控制了祥诽。
另一個就是如果是png
轉(zhuǎn)jpg
,繪制到canvas
上的時候瓮恭,canvas
存在透明區(qū)域的話雄坪,當轉(zhuǎn)成jpg
的時候透明區(qū)域會變成黑色,因為canvas
的透明像素默認為rgba(0,0,0,0)
屯蹦,所以轉(zhuǎn)成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
采用了另外的處理方式,看起來更加簡潔優(yōu)雅脑蠕。
/**
* 壓縮圖片
*/
function compress(file, options, callback) {
const reader = new FileReader();
reader.onload = function (evt) {
// 啟用壓縮
const img = new Image();
img.onload = function () {
// 拍照在IOS7或以下的機型會出現(xiàn)照片被壓扁的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);
// 源碼只轉(zhuǎn)jpeg购撼,jpg跪削,我加了png的轉(zhuǎn)化
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);
}
給文件處理添加了壓縮揩局,角度修正之后毫玖,上傳所得到的圖片就如下圖一樣正常了。
相關(guān)知識鏈接: