移動端圖片壓縮上傳實現(xiàn)

移動端圖片壓縮上傳實現(xiàn)

移動端上傳的圖片一般都是手機(jī)照片体箕,現(xiàn)在的手機(jī)都是高清像素专钉,一張圖片都在三四兆,直接上傳不僅傳輸速度慢累铅,而且如果用戶使用的是流量跃须,勢必會耗費(fèi)大量流量。

H5的各種API在移動端的主流瀏覽器都得到了很好的支持娃兽,比如案例中用到的FileReader菇民、Blob、FormData投储、canvas等API第练,所以壓縮上傳圖片在前端已經(jīng)是必備的操作。

壓縮上傳基本操作流程:
  1. 圖片上傳后使用FileReader將文件讀取成base64
  2. 創(chuàng)建Image玛荞,設(shè)置src屬性為圖片base64
  3. 創(chuàng)建canvas娇掏,繪制Image
  4. 調(diào)用canvas的toDataURL方法壓縮,返回壓縮后的base64
  5. 將base64轉(zhuǎn)成Blob對象
  6. 創(chuàng)建FormData對象冲泥,append Blob對象驹碍,提交給服務(wù)端

下面是每一步的具體實現(xiàn)以及一些坑(比如:API的兼容性、IOS圖片旋轉(zhuǎn)凡恍、底色等)志秃,并貼上全部代碼。

<input type="file" id="upload" accept='image/*'>

<h3>調(diào)用系統(tǒng)錄制功能</h3>
<input name='video' type='file' id='video-input' accept='video/*' capture='camcorder' />

<h3>調(diào)用系統(tǒng)相機(jī)</h3>
<input name='video' type='file' id='video-input' accept='image/*' capture='camcorder' />
import EXIF from 'exif';
一嚼酝、監(jiān)聽input的change事件浮还,讀取成base64。如果照片是豎著拍的闽巩,在IOS手機(jī)上傳后圖片會被旋轉(zhuǎn)钧舌。這里需要用到一個庫EXIF),可以獲取相片的屬性涎跨,比如曝光度洼冻、拍照方向、GPS等隅很。圖片加載完成后撞牢,在壓縮前需要解決IOS圖片是否被旋轉(zhuǎn)的問題和圖片壓縮格式的問題。
  • 圖片旋轉(zhuǎn)

    1. 問題: IOS豎著拍的照片會旋轉(zhuǎn)叔营。
    2. 解決: 首先創(chuàng)建臨時canvas屋彪,繪制圖片,旋轉(zhuǎn)成正確方向
  • canvas的toDataURL() 參數(shù)type的默認(rèn)值是 “image/png”绒尊,如果傳入的類型非“image/png”畜挥,但是返回的值以“data:image/png”開頭,那么該傳入的類型是不支持的婴谱。把類型統(tǒng)一設(shè)成jpeg蟹但,也就是統(tǒng)一用canvas.toDataURL('image/jpeg', 0.3) 躯泰,壓縮默認(rèn)值 0.92,這里我設(shè)的0.3矮湘。

let inp = document.getElementById('upload');

inp.onchange = function (event) {
    let file = event.target.files[0];
    let reader = new FileReader();
    let Orientation;

    // 讀取文件轉(zhuǎn)base64 
    reader.readAsDataURL(file);

    // 讀取完成
    reader.onload = function () {
        let result = this.result;

        /**
          *  result.length 的單位是字節(jié)
          *  如果圖片小于100K直接上傳斟冕,反之壓縮圖片
         */
        if (result.length <= (100 * 1024)) {
            // 直接上傳 調(diào)用API

        }
        else {
            // 創(chuàng)建image
            let image = new Image();
            image.src = result;

            // 圖片加載完成
            image.onload = function () {
                //獲取拍照的信息,解決IOS拍出來的照片旋轉(zhuǎn)問題
                EXIF.getData(image, function () {
                    Orientation = EXIF.getTag(this, 'Orientation');
                });

                // 首先旋轉(zhuǎn)成正確位置 再根據(jù)大小壓縮 然后根據(jù)像素判斷是否需要通過瓦片繪制
                let canvas;

                // 修復(fù)ios拍照上傳圖片的時被旋轉(zhuǎn)的問題
                if (Orientation !== '' && Orientation !== 1) {
                    // 創(chuàng)建臨時canvas  用來調(diào)整正確方位
                    canvas = document.createElement('canvas');

                    switch (Orientation) {
                        case 6://需要順時針(向左)90度旋轉(zhuǎn)
                            console.log(image.width, image.height);
                            rotateImg(image, 'left', canvas);
                            break;
                        case 8://需要逆時針(向右)90度旋轉(zhuǎn)
                            rotateImg(image, 'right', canvas);
                            break;
                        case 3://需要180度旋轉(zhuǎn)
                            rotateImg(image, 'right', canvas);//轉(zhuǎn)兩次
                            rotateImg(image, 'right', canvas);
                            break;
                    }
                }
                else {
                    canvas = compress(image);
                }

                // 對縮小比例后的canvas再進(jìn)行壓縮
                let compressData = canvas.toDataURL("image/jpeg", 0.3);  // 默認(rèn)MIME image/png
                let blob = convertBase64UrlToBlob(compressData);

                // 提交數(shù)據(jù)
                submitFormData(blob);
            }
        }
    }
}
二缅阳、 解決了圖片旋轉(zhuǎn)和圖片格式問題在壓縮前需要解決canvas繪制圖片的兩個限制和圖片格式轉(zhuǎn)換的問題磕蛇。
  1. 兩個限制

    • 問題
    1. 第一是圖片的大小:如果圖片的大小超過兩百萬像素十办,圖片也是無法繪制到canvas上的秀撇,調(diào)用drawImage的時候不會報錯,但是你用toDataURL獲取圖片數(shù)據(jù)的時候獲取到的是空的圖片數(shù)據(jù)向族。

    2. 第二是canvas的大小有限制呵燕,如果canvas的大小大于大概五百萬像素(寬 * 高)時,不僅圖片畫不出來件相,其他什么東西也都是畫不出來的再扭。

    • 解決方法
    1. 第一種限制,處理辦法就是瓦片繪制夜矗。瓦片繪制泛范,也就是將圖片分割成多塊繪制到canvas上,代碼里的實現(xiàn)是把圖片分割成100萬像素一塊的大小紊撕,再繪制到canvas上罢荡。

    2. 第二種限制,對圖片的寬高進(jìn)行適當(dāng)壓縮对扶。具體實現(xiàn)以上限四百萬像素為基準(zhǔn)区赵,如果圖片大于四百萬像素就壓縮到小于四百萬像素。

  2. 如果是png轉(zhuǎn)jpg浪南,繪制到canvas上的時候笼才,canvas存在透明區(qū)域的話,當(dāng)轉(zhuǎn)成jpg的時候透明區(qū)域會變成黑色络凿,因為canvas的透明像素默認(rèn)為rgba(0,0,0,0)患整,所以轉(zhuǎn)成jpg就變成rgba(0, 0, 0 ,1)了,也就是透明背景會變成了黑色喷众。解決辦法就是繪制之前在canvas上鋪一層白色的底色。

function compress(image) {
    let {width, height} = image;

    // 創(chuàng)建canvas 獲取上下文
    let canvas = document.createElement('canvas');
    let ctx = canvas.getContext('2d');

    /**
    * 判斷像素大小
    * 像素 = 寬 * 高 
    */

    // 如果像素大于400萬 則需計算壓縮比 壓縮至400萬以下
    let ratio = (width * height) / 4000000;

    if (ratio > 1) {
        // 倍數(shù)開方 (相當(dāng)于面積為多少倍紧憾,則寬高對應(yīng)的倍數(shù)需對面積倍數(shù)開方)
        ratio = Math.sqrt(ratio);

        // 寬高對應(yīng)的值
        width /= ratio;
        height /= ratio;
    }
    else {
        ratio = 1;
    }

    // 畫布寬高
    canvas.width = width;
    canvas.height = height;

    // 鋪底色
    ctx.fillStyle = '#fff';

    // 繪制矩形
    ctx.fillRect(0, 0, width, height);

    // 如果縮放比例后畫布像素仍大于100萬像素 則使用瓦片繪制到千, 反之直接繪制
    let count = width * height / 1000000;

    if (count > 1) {
        // 創(chuàng)建瓦片 獲取2d上下文
        let tcanvas = document.createElement('canvas');
        let tctx = tcanvas.getContext('2d');

        /**
         * 瓦片數(shù)量 = count的平方 + 1
         * +1不是必須得,是為了瓦片更小赴穗,數(shù)量更多一些
         */
        count = ~~(Math.sqrt(count) + 1);  // 比如count為2.3 則轉(zhuǎn)成3

        let tWidth = ~~(width / count);
        let tHeight = ~~(height / count);

        // 瓦片的寬高
        tcanvas.width = tWidth;
        tcanvas.height = tHeight;

        for (let i = 0; i < count; i++) {
            for (let j = 0; j < count; j++) {
                tctx.drawImage(image, i * tWidth * ratio, j * tHeight * ratio, tWidth * ratio, tHeight * ratio, 0, 0, tWidth, tHeight);
                console.log(tcanvas.width, tcanvas.height, tcanvas.width * tcanvas.height);
                ctx.drawImage(tcanvas, i * tWidth, j * tHeight, tWidth, tHeight);
            }
        }
    }
    else {
        // 直接繪制
        ctx.drawImage(image, 0, 0, width, height);
    }

    return canvas;
}
  1. 完成圖片壓縮后憔四,先將base64提取出來膀息,再實例化一個ArrayBuffer,然后將字符串以8位整型的格式傳入ArrayBuffer了赵,再通過Blob對象(可能需要兼容Blob)潜支,將8位整型的ArrayBuffer轉(zhuǎn)成二進(jìn)制對象blob,然后把blob對象append到formdata里柿汛,再提交給后臺冗酿。
function convertBase64UrlToBlob(urlData) {
    let bytes = window.atob(urlData.split(',')[1]);

    // 處理異常,將ascii碼小于0的轉(zhuǎn)換為大于0
    let ab = new ArrayBuffer(bytes.length);
    let ia = new Uint8Array(ab);
    for (let i = 0; i < bytes.length; i++) {
        ia[i] = bytes.charCodeAt(i);
    }

    // 二進(jìn)制對象
    return getBlob([ab], "image/jpeg");
}
  1. 兼容Blob對象
/**
* Blob對象的兼容性寫法
* @param buffer 數(shù)據(jù)流
* @param format 表示將會被放入到blob中的數(shù)組內(nèi)容的MIME類型。類型默認(rèn) '' 
*/
function getBlob(buffer, format = 'image/jpeg') {
    try {
        return new Blob(buffer, {
            type: format
        });
    }
    catch (e) {
        let blob = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder)();

        buffer.forEach(function (buf) {
            blob.append(buf);
        });
        return blob.getBlob(format);
    }
}
  1. 低版本的Android機(jī)不支持FormData络断,需要做兼容處理裁替。首先判斷是否需要兼容
function needsFormDataShim() {
    return  ~navigator.userAgent.indexOf('Android')
            && ~navigator.vendor.indexOf('Google')
            && !~navigator.userAgent.indexOf('Chrome')
            && navigator.userAgent.match(/AppleWebKit\/(\d+)/).pop() <= 534;
}
  1. 給不支持FormData上傳Blob的android機(jī)打補(bǔ)丁,定義boundary分隔符貌笨,設(shè)置請求體弱判。重寫XMLHttpRequest原型的send方法。
function FormDataShim() {
    let o = this,
        // 請求體 
        parts = [],
        // 分隔符
        boundary = Array(5).join('-') + (+new Date() * (1e16 * Math.random())).toString(36),
        oldSend = XMLHttpRequest.prototype.send;

    this.append = function (name, value, filename) {
        parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"`);

        if (value instanceof Blob) {
            parts.push(`; filename="${filename || 'blob'}"\r\nContent-Type: ${value.type}\r\n\r\n`);
            parts.push(value);
        }
        else {
            parts.push('\r\n\r\n' + value);
        }
        parts.push('\r\n');
    };

    // override XHR send()
    XMLHttpRequest.prototype.send = function (val) {
        let fr,
            data,
            oXHR = this;

        if (val === o) {
            // 不能漏最后的\r\n ,否則服務(wù)器有可能解析不到參數(shù).
            parts.push(`--${boundary}--\r\n`);

            // 創(chuàng)建Blob對象
            data = getBlob(parts);

            // Set up and read the blob into an array to be sent
            fr = new FileReader();
            fr.onload = function () {
                oldSend.call(oXHR, fr.result);
            };
            fr.onerror = function (err) {
                throw err;
            };
            fr.readAsArrayBuffer(data);

            // 設(shè)置請求頭Content-Type的類型和分隔符 服務(wù)端是根據(jù)Content-Type來解析請求體中
            this.setRequestHeader(
                'Content-Type',
                `multipart/form-data; boundary=${boundary}`
            );

            XMLHttpRequest.prototype.send = oldSend;
        }
        else {
            oldSend.call(this, val);
        }
    };
}
  1. 提交數(shù)據(jù)锥惋。判斷是否支持FormData
function submitFormData(blob) {
    let isNeedShim = needsFormDataShim();
    let formdata = isNeedShim ? new FormDataShim() : new FormData();

    formdata.append('imagefile', blob);
    
    if (isNeedShim) {
        let ajax = new XMLHttpRequest();

        ajax.open('POST', '/');
        ajax.onreadystatechange = function() {
            if (ajax.status === 200 && ajax.readyState === 4) {

            }
        }
        ajax.send(formdata);
    }
    else {
        // 調(diào)用API
        axios.post('/upload', formdata)
            .then(response => {
                console.log(response);
            })
            .catch(error => {
                console.log(error);
            });

        // axios 會根據(jù)提交的文件類型昌腰,設(shè)置相應(yīng)的Content-Type類型
    }
}
  1. 旋轉(zhuǎn)圖片
/**
* @param 旋轉(zhuǎn)的圖片
* @param 方向
* @param 繪制的canvas
*/
function rotateImg(img, direction, canvas) {
    //最小與最大旋轉(zhuǎn)方向,圖片旋轉(zhuǎn)4次后回到原方向
    const min_step = 0;
    const max_step = 3;

    if (img == null) return;

    // 縮小比例后的canvas
    let lessCnavas = compress(img);
    let {width, height} = lessCnavas;
    let step = 2;

    if (step == null) {
        step = min_step;
    }

    if (direction == 'right') {
        step++;

        //旋轉(zhuǎn)到原位置膀跌,即超過最大值
        step > max_step && (step = min_step);
    } else {
        step--;
        step < min_step && (step = max_step);
    }

    //旋轉(zhuǎn)角度以弧度值為參數(shù)
    let degree = (step * 90 * Math.PI) / 180;
    let ctx = canvas.getContext('2d');

    switch (step) {
        case 0:
            canvas.width = width;
            canvas.height = height;
            ctx.drawImage(lessCnavas, 0, 0);
            break;
        case 1:
            canvas.width = height;
            canvas.height = width;
            ctx.rotate(degree);
            ctx.drawImage(lessCnavas, 0, -height);
            break;
        case 2:
            canvas.width = width;
            canvas.height = height;
            ctx.rotate(degree);
            ctx.drawImage(lessCnavas, -width, -height);
            break;
        case 3:
            canvas.width = height;
            canvas.height = width;
            ctx.rotate(degree);
            ctx.drawImage(lessCnavas, -width, 0);
            break;
    }
}

以上就是壓縮上傳的全部實現(xiàn)遭商。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市淹父,隨后出現(xiàn)的幾起案子株婴,更是在濱河造成了極大的恐慌,老刑警劉巖暑认,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件困介,死亡現(xiàn)場離奇詭異,居然都是意外死亡蘸际,警方通過查閱死者的電腦和手機(jī)座哩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粮彤,“玉大人根穷,你說我怎么就攤上這事〉挤兀” “怎么了屿良?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惫周。 經(jīng)常有香客問我尘惧,道長,這世上最難降的妖魔是什么递递? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任喷橙,我火速辦了婚禮啥么,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贰逾。我一直安慰自己悬荣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布疙剑。 她就那樣靜靜地躺著氯迂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪核芽。 梳的紋絲不亂的頭發(fā)上囚戚,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機(jī)與錄音轧简,去河邊找鬼驰坊。 笑死,一個胖子當(dāng)著我的面吹牛哮独,可吹牛的內(nèi)容都是我干的拳芙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼皮璧,長吁一口氣:“原來是場噩夢啊……” “哼舟扎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悴务,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤睹限,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后讯檐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羡疗,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贡未,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年波丰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搭盾。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡挖垛,死狀恐怖痒钝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痢毒,我是刑警寧澤送矩,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站哪替,受9級特大地震影響益愈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一蒸其、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧库快,春花似錦摸袁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至闽铐,卻和暖如春蝶怔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兄墅。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工踢星, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隙咸。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓沐悦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親五督。 傳聞我的和親對象是個殘疾皇子藏否,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

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