移動端圖片壓縮及角度修正

我們使用uploader組件來進行測試,用ios手機去拍照,手機豎著去拍照時往史,得到的圖片會逆時針旋轉(zhuǎn)90度诱渤,橫著拍沒問題。下面圖中的左一即為橫拍梗醇,右邊的兩個豎拍就很明顯不對勁了,手機上我們拍的照片所呈現(xiàn)的都是豎屏照片,上傳后就變成橫屏的了贴捡,這無疑給用戶增加了不適感,不合適村砂,那我們需要針對這去改一下烂斋。

解決思路:獲取到照片拍攝的方向角度,然后使用canvas去進行修正


image.png

隨著這個思路础废,我們需要了解一下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
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。

image.png
// 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上的兩種限制了。

除了上面所述的限制咱台,還有兩個坑络拌,一個就是canvastoDataURL是只能壓縮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);
}

給文件處理添加了壓縮揩局,角度修正之后毫玖,上傳所得到的圖片就如下圖一樣正常了。

image.png

相關(guān)知識鏈接:

weui image處理

exif.js 在線測試

圖片的Exif信息

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凌盯,一起剝皮案震驚了整個濱河市付枫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驰怎,老刑警劉巖励背,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異砸西,居然都是意外死亡叶眉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門芹枷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衅疙,“玉大人,你說我怎么就攤上這事鸳慈”ヒ纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵走芋,是天一觀的道長绩郎。 經(jīng)常有香客問我,道長翁逞,這世上最難降的妖魔是什么肋杖? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮挖函,結(jié)果婚禮上状植,老公的妹妹穿的比我還像新娘。我一直安慰自己怨喘,他們只是感情好津畸,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著必怜,像睡著了一般肉拓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梳庆,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天暖途,我揣著相機與錄音卑惜,去河邊找鬼。 笑死丧肴,一個胖子當著我的面吹牛残揉,可吹牛的內(nèi)容都是我干的胧后。 我是一名探鬼主播芋浮,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼壳快!你這毒婦竟也來了纸巷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤眶痰,失蹤者是張志新(化名)和其女友劉穎瘤旨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖伯,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡存哲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了七婴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祟偷。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖打厘,靈堂內(nèi)的尸體忽然破棺而出修肠,到底是詐尸還是另有隱情,我是刑警寧澤户盯,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布嵌施,位于F島的核電站,受9級特大地震影響莽鸭,放射性物質(zhì)發(fā)生泄漏吗伤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一硫眨、第九天 我趴在偏房一處隱蔽的房頂上張望牲芋。 院中可真熱鬧,春花似錦捺球、人聲如沸缸浦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽裂逐。三九已至,卻和暖如春泣栈,著一層夾襖步出監(jiān)牢的瞬間卜高,已是汗流浹背弥姻。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掺涛,地道東北人庭敦。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像薪缆,于是被迫代替她去往敵國和親秧廉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容