js壓縮圖片 到固定像素以內(nèi),500k為例

本文旨在探究js壓縮圖片的兩種方式:改變圖片長寬被济,改變圖片質(zhì)量,和結(jié)合了以上兩者的最終方案涧团。

首先只磷,閱讀本文需要知道canvas的兩個(gè)方法

drawImage(image, x, y, width, height)
這個(gè)方法多了2個(gè)參數(shù):width 和 height,這兩個(gè)參數(shù)用來控制 當(dāng)向canvas畫入時(shí)應(yīng)該縮放的大小

canvas.toDataURL(type, encoderOptions);
type 可選
圖片格式泌绣,默認(rèn)為 image/png
encoderOptions 可選
在指定圖片格式為 image/jpeg 或 image/webp的情況下钮追,可以從 0 到 1 的區(qū)間內(nèi)選擇圖片的質(zhì)量。如果超出取值范圍阿迈,將會(huì)使用默認(rèn)值 0.92元媚。其他參數(shù)會(huì)被忽略。

這兩個(gè)方法具體的說明可以在MDN上查看仿滔,關(guān)于圖片壓縮惠毁,也有很多現(xiàn)成的博客可以直接用。但是那些博客都有個(gè)問題崎页,并沒有關(guān)心之后圖片的壓縮質(zhì)量鞠绰。
我試著用一個(gè)現(xiàn)成的例子去跑了一下,一個(gè)1.7M的圖片壓縮到了23k飒焦,堪稱像素級(jí)毀滅性破壞蜈膨。

改變圖片長寬實(shí)現(xiàn)壓縮目的

假如一張大圖可能包含著很多文字等關(guān)鍵信息屿笼,必須上傳之后使用方能清晰辨認(rèn)。所以要壓縮之后質(zhì)量盡可能接近500k的翁巍。500k像素以內(nèi)驴一,就是若一張圖寬度為1024,則高度不能超過500灶壶。因?yàn)閳D片有其他的信息肝断,也是要占大小的。即不得大于1024*500驰凛。

1024*576在不控制圖片質(zhì)量的情況下并不是576kb

所以胸懈,根據(jù)需求,上傳圖片不能超過500k的情況下盡可能保留圖片的清晰度恰响。當(dāng)然如果可以的情況下用上面提到的canvas.toDataURL設(shè)置壓縮程度為0.9趣钱,0.8試試看,圖片質(zhì)量可以接受胚宦,大小會(huì)有大幅度的縮小首有。

設(shè)為0.9之后的圖片大幅縮小了

如果不壓縮,靠調(diào)整圖片長寬去控制上傳大小呢枢劝?
原理很簡單井联,就是靠不斷地縮小限定的最大寬高,直到最終長寬的積小于規(guī)定的大小呈野。
這種方法有可能最后得出的圖片的大小會(huì)略大于規(guī)定大小低矮,原因上文也提到過了印叁,如果想使用這種方法被冒,可自行再調(diào)整一下。

// 實(shí)際的壓縮在imgResize里
export default function getUploadImgInfo(e) {
    /**
     * 獲取最終圖片上傳的所需入?yún)ⅲ簕 blob:圖片文件, fileExtension:圖片擴(kuò)展名, fileType:圖片類型, imgBase64:圖片前端展示的base64 }
     * { blob, fileExtension, fileType, imgBase64}
     */
    let fileInfo = getFileInfo(e);
    if (!fileInfo) return Promise.reject();
    let { fileExtension, fileType, file } = fileInfo;
    return imgResize({ file, fileType }).then(res => {
        return convertBlob({ imgBase64: res, fileType, fileExtension });
    });
}
// 對圖片進(jìn)行壓縮
function imgResize({ file, maxSizeNum = 500, fileType }) {
    let fileReader = new FileReader();
    return new Promise((resolve, reject) => {
        let maxSize = 1 * 1024 * 1024 * (maxSizeNum / 1024); // 最大默認(rèn)500k 即若規(guī)定最大寬度為1024轮蜕,則高度不能超過500
        let maxLength = 1024;
        if (file.size > maxSize) {
            fileReader.onload = function(e) {
                let IMG = new Image();
                IMG.onload = function() {
                    let reMaxLength = maxLength;
                    let originW = this.naturalWidth;
                    let originH = this.naturalHeight;
                    let canvas = document.createElement('canvas');
                    let ctx = canvas.getContext('2d');
                    let level = 1.0; // level是圖片質(zhì)量
                    let { resizeW, resizeH } = getImgResize(
                        originW,
                        originH,
                        maxLength
                    );
                    let count = 0;
                    while (resizeH * resizeW >= maxSize) {
                        reMaxLength = reMaxLength - 100;
                        let obj = getImgResize(originW, originH, reMaxLength);
                        resizeH = obj.resizeH;
                        resizeW = obj.resizeW;
                        console.log('rr:', resizeH, resizeW, count++);
                    }

                    if (window.navigator.userAgent.indexOf('iPhone') > 0) {
                        canvas.width = resizeH;
                        canvas.height = resizeW;
                        ctx.rotate((90 * Math.PI) / 180);
                        ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH);
                    } else {
                        canvas.width = resizeW;
                        canvas.height = resizeH;
                        ctx.drawImage(IMG, 0, 0, resizeW, resizeH);
                    }
                    let base64 = canvas.toDataURL('image/' + fileType, level);
                    resolve(base64);
                };

                IMG.src = e.target.result;
            };
        } else {
            fileReader.onload = function() {
                resolve(fileReader.result);
            };
        }
        fileReader.readAsDataURL(file);
    });
}
function getImgResize(originW, originH, maxLength = 1024) {
    // 獲得圖片調(diào)整大小之后的長寬昨悼;maxLength為長寬最大的長度
    let resizeH = 0;
    let resizeW = 0;
    if (originW > maxLength || originH > maxLength) {
        let multiple = Math.max(originW / maxLength, originH / maxLength);
        resizeW = originW / multiple;
        resizeH = originH / multiple;
    } else {
        resizeW = originW;
        resizeH = originH;
    }
    return {
        resizeH,
        resizeW
    };
}
function getFileInfo(e) {
    if (e.target.value == '') {
        return false;
    }
    let files = e.target.files || e.dataTransfer.files;
    if (!files.length) return;
    let file = files[0];
    return {
        fileExtension:
            file.name.match(/[\w]*$/) && file.name.match(/[\w]*$/)[0],
        fileType: file.type.split('/')[1],
        file: files[0]
    };
}
function convertBlob({ imgBase64, fileType, fileExtension }) {
    // 獲得blob,用于最終上傳的圖片入?yún)?    if (!imgBase64) {
        this.$toast('請選擇圖片');
        return;
    }
    let base64 = window.atob(imgBase64.split(',')[1]);
    let buffer = new ArrayBuffer(base64.length);
    let ubuffer = new Uint8Array(buffer);
    for (let i = 0; i < base64.length; i++) {
        ubuffer[i] = base64.charCodeAt(i);
    }
    let blob;
    try {
        blob = new Blob([buffer], { type: 'image/' + fileType });
    } catch (e) {
        window.BlobBuilder =
            window.BlobBuilder ||
            window.WebKitBlobBuilder ||
            window.MozBlobBuilder ||
            window.MSBlobBuilder;
        if (e.name === 'TypeError' && window.BlobBuilder) {
            let blobBuilder = new BlobBuilder();
            blobBuilder.append(buffer);
            blob = blobBuilder.getBlob('image/' + fileType);
        }
    }
    return Promise.resolve({ blob, fileExtension, fileType, imgBase64 });
}

改變圖片清晰度實(shí)現(xiàn)壓縮目的

上面的方法有個(gè)問題跃洛,就是改變了圖片的原始長寬率触。如果一個(gè)圖的長寬足夠大,壓縮圖片質(zhì)量汇竭,糊一點(diǎn)但是內(nèi)容看得清也是ok的嘛葱蝗。所以,跟上面同理细燎,我們可以不斷調(diào)整圖片的質(zhì)量設(shè)定直到大小合適两曼,那么,如何在圖片上傳之前知道圖片的大小呢玻驻?
首先悼凑,需要知道的一點(diǎn)是,壓縮之后拿到的base64字符串會(huì)轉(zhuǎn)成blob對象,然后傳給服務(wù)端户辫。
可以查閱文檔渐夸,blob對象有個(gè)屬性是size

返回一個(gè)File對象所指代的文件的大小,單位為字節(jié)渔欢。

這個(gè)size就是上傳之后實(shí)際的文件大小墓塌。
參照上面的思路,可以每次改變canvas.toDataURL('image/' + fileType, level);level的值奥额,去調(diào)整壓縮圖片質(zhì)量桃纯,然后用blob對象的size去驗(yàn)證是否滿足500k以內(nèi)的需求。
關(guān)于 canvas.toDataURL的level到底是怎么計(jì)算的披坏,MDN文檔里也沒說态坦,寫了個(gè)循環(huán)一次減少0.1的level壓縮了幾個(gè)圖片

726625字節(jié)

1738594字節(jié)
1615259 byte

用加減乘除算了一下,沒找到規(guī)律棒拂,數(shù)學(xué)不好放棄了(這個(gè)東西好像也不是能觀察出來的伞梯,看結(jié)果跟初始大小沒啥關(guān)系)。
這里要注意的是帚屉,有可能遇到超大圖片谜诫,0.1的level可能不足以壓縮到500k,所以小于0.1的時(shí)候攻旦,改變level遞減的差值繼續(xù)壓縮下去

if (level <= cutNum) {
    cutNum = cutNum / 10;
}
level = (level * 100 - cutNum * 100) / 100;

在開始接收到圖片的時(shí)候給一個(gè)loading增加用戶的耐心好了喻旷,loading萬歲~

let fileSize = null;
export default async function getUploadImgInfo(e) {
    /**
     * 獲取最終圖片上傳的所需入?yún)ⅲ簕 blob:圖片文件, fileExtension:圖片擴(kuò)展名, fileType:圖片類型, imgBase64:圖片前端展示的base64 }
     * { blob, fileExtension, fileType, imgBase64}
     */
    let fileInfo = getFileInfo(e);
    if (!fileInfo) return Promise.reject();
    let { fileExtension, fileType, file } = fileInfo;
    let level = 0.9; // 壓縮的文件質(zhì)量,如果文件大小不合適牢屋,質(zhì)量則會(huì)取這個(gè)值
    let cutNum = 0.1;
    let maxSize = 500 * 1024; // 字節(jié)且预,最大500k
    let fileRes = null;
    let finalRes = null;
    let task = async () => {
        console.log('level', level);
        fileRes = await imgResize({ file, fileType, level, maxSize });
        finalRes = await convertBlob({
            imgBase64: fileRes,
            fileType,
            fileExtension
        });
    };
    await task();
    while (finalRes.blob.size > maxSize) {
        // 如果文件大小不合適,則一直壓縮到500k以內(nèi)
        if (level <= cutNum) {
            cutNum = cutNum / 10;
        }
        level = (level * 100 - cutNum * 100) / 100;
        await task();
    }
    return finalRes;
}
function getFileInfo(e) {
    if (e.target.value == '') {
        return false;
    }
    let files = e.target.files || e.dataTransfer.files;
    if (!files.length) return;
    let file = files[0];
    return {
        fileExtension:
            file.name.match(/[\w]*$/) && file.name.match(/[\w]*$/)[0],
        fileType: file.type.split('/')[1],
        file: files[0]
    };
}
// 對圖片進(jìn)行壓縮
function imgResize({ file, maxSize = 500 * 1024, fileType, level = 1.0 }) {
    let fileReader = new FileReader();
    return new Promise((resolve, reject) => {
        fileSize = file.size;
        if (file.size > maxSize) {
            fileReader.onload = function(e) {
                let IMG = new Image();
                IMG.onload = function() {
                    let originW = this.naturalWidth;
                    let originH = this.naturalHeight;
                    let resizeH = originH;
                    let resizeW = originW;
                    let canvas = document.createElement('canvas');
                    let ctx = canvas.getContext('2d');

                    if (window.navigator.userAgent.indexOf('iPhone') > 0) {
                        canvas.width = resizeH;
                        canvas.height = resizeW;
                        ctx.rotate((90 * Math.PI) / 180);
                        ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH);
                    } else {
                        canvas.width = resizeW;
                        canvas.height = resizeH;
                        ctx.drawImage(IMG, 0, 0, resizeW, resizeH);
                    }
                    let base64 = canvas.toDataURL('image/' + fileType, level);
                    resolve(base64);
                };

                IMG.src = e.target.result;
            };
        } else {
            fileReader.onload = function() {
                resolve(fileReader.result);
            };
        }
        fileReader.readAsDataURL(file);
    });
}
function convertBlob({ imgBase64, fileType, fileExtension }) {
    // 獲得blob烙无,用于最終上傳的圖片入?yún)?    if (!imgBase64) {
        this.$toast('請選擇圖片');
        return;
    }
    let base64 = window.atob(imgBase64.split(',')[1]);
    let buffer = new ArrayBuffer(base64.length);
    let ubuffer = new Uint8Array(buffer);
    for (let i = 0; i < base64.length; i++) {
        ubuffer[i] = base64.charCodeAt(i);
    }
    let blob;
    try {
        blob = new Blob([buffer], { type: 'image/' + fileType });
    } catch (e) {
        window.BlobBuilder =
            window.BlobBuilder ||
            window.WebKitBlobBuilder ||
            window.MozBlobBuilder ||
            window.MSBlobBuilder;
        if (e.name === 'TypeError' && window.BlobBuilder) {
            let blobBuilder = new BlobBuilder();
            blobBuilder.append(buffer);
            blob = blobBuilder.getBlob('image/' + fileType);
        }
    }
    console.log(
        'count:',
        count++,
        '占比:',
        blob.size / fileSize,
        'final:',
        blob.size,
        blob.size / 1024,
        'begin',
        fileSize,
        fileSize / 1024
    );
    return Promise.resolve({ blob, fileExtension, fileType, imgBase64 });
}
let count = 1;

綜合方案

其實(shí)單純的壓縮質(zhì)量遇到稍大的圖片锋谐,會(huì)導(dǎo)致頁面高頻計(jì)算,然后頁面基本就用不了了- -截酷。有嘗試過用iphone的一個(gè)屏幕截圖(10M左右)涮拗,壓的時(shí)候稍過一會(huì),整個(gè)手機(jī)都在發(fā)燙迂苛,只能殺進(jìn)程三热。

所以,若對長度沒有特殊的限制三幻,可以做一個(gè)縮放就漾,去加快壓縮的進(jìn)度,提高能壓縮的圖片大小上限赌髓。


綜合方案壓縮10.8M圖片

頁面到了ios上還是不行- -从藤,可以看到最后圖片level為0.001催跪,最長邊為764。
問題還是循環(huán)次數(shù)還是過多夷野,計(jì)算頻率太高懊蒸。從圖中可看出,對于大圖來說悯搔,初始設(shè)定的level和圖片尺寸過于寬松骑丸,可以優(yōu)化一下初始level和尺寸。

  if (finalRes.blob.size > maxSize) {
        // 設(shè)定一個(gè)合適的初始level
        level = orginLevel(finalRes.blob.size, maxSize);
        if (level <= 0.4) maxLength = 800;
  }
function orginLevel(size, maxSize) {
    // 根據(jù)文件大小得到一開始的level
    // rate為與最終限定大小的倍數(shù)
    let rate = Number((size / maxSize).toFixed());
    let finalLevel = (1 * 100000 - (rate * 100000) / 3) / 100000;
    if (rate <= 1) {
        return 1;
    } else if (finalLevel > 0) {
        return Number(finalLevel.toFixed(1));
    } else {
        return 0.1;
    }
}
優(yōu)化大圖的初始level和最長邊之后

有的時(shí)候還會(huì)遇到一張圖片無論如何也壓不到500k妒貌,就是上一次和這次的壓縮后大小沒有變化通危,這種情況需要拋錯(cuò),不讓循環(huán)繼續(xù)灌曙。

if (finalSize === blob.size) {
    return Promise.reject(
        new Error('壓縮該文件最近兩次的大小相同菊碟,壓縮無效!')
    );
} 

大圖片的等待時(shí)間稍長在刺,可以給用戶先預(yù)覽一個(gè)base64的圖片增加等待耐心逆害,方法名為getImgBase64,這里都一并給出了

/* eslint-disable no-undef */
let fileSize = 0;
let count = 1;
let finalSize = 0; // 壓縮之后的大小,若兩次比對這個(gè)值是相同的蚣驼,則判定壓縮失敗魄幕,拋錯(cuò)
export default async function getUploadImgInfo(e) {
    /**
     * 獲取最終圖片上傳的所需入?yún)ⅲ簕 blob:圖片文件, fileExtension:圖片擴(kuò)展名, fileType:圖片類型, imgBase64:圖片前端展示的base64 }
     * { blob, fileExtension, fileType, imgBase64}
     */
    let fileInfo = getFileInfo(e);
    if (!fileInfo) return Promise.reject();
    let { fileExtension, fileType, file } = fileInfo;
    let level = 1; // 壓縮的文件質(zhì)量,如果文件大小不合適颖杏,質(zhì)量則會(huì)取這個(gè)值
    let cutNum = 0.1;
    let maxSize = 500 * 1024; // 字節(jié)纯陨,最大500k
    let fileRes = null;
    let finalRes = null;
    let maxLength = 1024; // 能接受的最大圖片長度,作為壓縮圖片的入?yún)?    const minLength = 754; // 能接受的最小圖片長度
    finalSize = 0; // 重置這個(gè)值
    let task = async () => {
        console.log('test||   ', 'level:', level, 'maxLength:', maxLength);
        fileRes = await imgResize({
            file,
            fileType,
            level,
            maxSize,
            maxLength
        });
        finalRes = await convertBlob({
            imgBase64: fileRes,
            fileType,
            fileExtension
        });
    };
    await task();
    if (finalRes.blob.size > maxSize) {
        // 設(shè)定一個(gè)合適的level留储,避免壓縮的時(shí)候太多
        level = orginLevel(finalRes.blob.size, maxSize);
        if (level <= 0.4) maxLength = 800;
        console.log('test||   ', 'originLevel', level);
    }
    while (finalRes.blob.size > maxSize) {
        // 如果文件大小不合適翼抠,則一直壓縮到500k以內(nèi)
        if (maxLength > minLength) {
            // 控制圖片最大邊的長度
            maxLength = maxLength - 10;
        }
        if (level <= cutNum) {
            // 控制level遞減的差值
            cutNum = cutNum / 10;
        }
        level = (level * 1000000 - cutNum * 1000000) / 1000000;
        await task();
    }
    return finalRes;
}
function orginLevel(size, maxSize) {
    // 根據(jù)文件大小得到一開始的level
    // rate為與最終限定大小的倍數(shù)
    let rate = Number((size / maxSize).toFixed());
    let finalLevel = (1 * 100000 - (rate * 100000) / 3) / 100000;
    if (rate <= 1) {
        return 1;
    } else if (finalLevel > 0) {
        return Number(finalLevel.toFixed(1));
    } else {
        return 0.1;
    }
}
function getImgResize(originW, originH, maxLength = 1024) {
    // 獲得圖片調(diào)整大小之后的長寬;maxLength為長寬最大的長度
    let resizeH = 0;
    let resizeW = 0;
    if (originW > maxLength || originH > maxLength) {
        let multiple = Math.max(originW / maxLength, originH / maxLength);
        resizeW = originW / multiple;
        resizeH = originH / multiple;
    } else {
        resizeW = originW;
        resizeH = originH;
    }
    return {
        resizeH,
        resizeW
    };
}
export function getImgBase64(e) {
    // 把文件轉(zhuǎn)成base64--預(yù)覽目的
    let fileInfo = getFileInfo(e);
    let { fileType, file } = fileInfo;
    return imgResize({
        file,
        maxSize: 500 * 1024,
        fileType,
        level: 0.6,
        maxLength: 500
    });
}
function getFileInfo(e) {
    if (e.target.value == '') {
        return false;
    }
    let files = e.target.files || e.dataTransfer.files;
    if (!files.length) return;
    let file = files[0];
    return {
        fileExtension:
            file.name.match(/[\w]*$/) && file.name.match(/[\w]*$/)[0],
        fileType: file.type.split('/')[1],
        file: files[0]
    };
}
// 對圖片進(jìn)行壓縮
function imgResize({
    file,
    maxSize = 500 * 1024,
    fileType,
    level = 1.0,
    maxLength = 1024
}) {
    let fileReader = new FileReader();
    return new Promise((resolve, reject) => {
        fileSize = file.size;
        if (file.size > maxSize) {
            fileReader.onload = function(e) {
                let IMG = new Image();
                IMG.onload = function() {
                    let originW = this.naturalWidth;
                    let originH = this.naturalHeight;
                    let canvas = document.createElement('canvas');
                    let ctx = canvas.getContext('2d');
                    let { resizeW, resizeH } = getImgResize(
                        originW,
                        originH,
                        maxLength
                    );
                    if (window.navigator.userAgent.indexOf('iPhone') > 0) {
                        canvas.width = resizeH;
                        canvas.height = resizeW;
                        ctx.rotate((90 * Math.PI) / 180);
                        ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH);
                    } else {
                        canvas.width = resizeW;
                        canvas.height = resizeH;
                        ctx.drawImage(IMG, 0, 0, resizeW, resizeH);
                    }
                    let base64 = canvas.toDataURL('image/' + fileType, level);
                    resolve(base64);
                };

                IMG.src = e.target.result;
            };
        } else {
            fileReader.onload = function() {
                resolve(fileReader.result);
            };
        }
        fileReader.readAsDataURL(file);
    });
}
function convertBlob({ imgBase64, fileType, fileExtension }) {
    // 獲得blob欲鹏,用于最終上傳的圖片入?yún)?    if (!imgBase64) {
        this.$toast('請選擇圖片');
        return;
    }
    let base64 = window.atob(imgBase64.split(',')[1]);
    let buffer = new ArrayBuffer(base64.length);
    let ubuffer = new Uint8Array(buffer);
    for (let i = 0; i < base64.length; i++) {
        ubuffer[i] = base64.charCodeAt(i);
    }
    let blob;
    try {
        blob = new Blob([buffer], { type: 'image/' + fileType });
    } catch (e) {
        window.BlobBuilder =
            window.BlobBuilder ||
            window.WebKitBlobBuilder ||
            window.MozBlobBuilder ||
            window.MSBlobBuilder;
        if (e.name === 'TypeError' && window.BlobBuilder) {
            let blobBuilder = new BlobBuilder();
            blobBuilder.append(buffer);
            blob = blobBuilder.getBlob('image/' + fileType);
        }
    }
    if (finalSize === blob.size) {
        return Promise.reject(
            new Error('壓縮該文件兩次的結(jié)果相同机久,壓縮無效臭墨!')
        );
    } else {
        finalSize = blob.size;
    }
    console.log(
        'test||   ',
        'count:',
        count++,
        '占比:',
        blob.size / fileSize,
        'final:',
        blob.size,
        blob.size / 1024,
        'begin',
        fileSize,
        fileSize / 1024
    );
    return Promise.resolve({ blob, fileExtension, fileType, imgBase64 });
}

優(yōu)化方案

解決的隱患:上面這個(gè)方案會(huì)出現(xiàn)我需要一個(gè)500k的照片赔嚎,壓到了520k之后,再壓了一次胧弛。有時(shí)候這最后的一次會(huì)特別夸張尤误,直接將圖片弄到了幾十k。
參考了:https://github.com/WangYuLue/image-conversion
這個(gè)庫里面有個(gè)方法compressAccurately,這個(gè)方法可以比較精準(zhǔn)地壓縮结缚。偷偷翻了一下源碼损晤。

async function compressAccurately(file: Blob, config: compressAccuratelyConfig = {}): Promise<Blob> {
  if (!(file instanceof Blob)) {
    throw new Error('compressAccurately(): First arg must be a Blob object or a File object.');
  }
  if (typeof config !== 'object') {
    config = Object.assign({
      size: config,
    });
  }
  // 如果指定體積不是數(shù)字或者數(shù)字字符串,則不做處理
  config.size = Number(config.size);
  if (Number.isNaN(config.size)) {
    return file;
  }
  // 如果指定體積大于原文件體積红竭,則不做處理尤勋;
  if (config.size * 1024 > file.size) {
    return file;
  }
  config.accuracy = Number(config.accuracy);
  if (!config.accuracy
    || config.accuracy < 0.8
    || config.accuracy > 0.99) {
    config.accuracy = 0.95; // 默認(rèn)精度0.95
  }
  const resultSize = {
    max: config.size * (2 - config.accuracy) * 1024,
    accurate: config.size * 1024,
    min: config.size * config.accuracy * 1024,
  };
  const dataURL = await filetoDataURL(file);
  let originalMime = dataURL.split(',')[0].match(/:(.*?);/)[1] as EImageType; // 原始圖像圖片類型
  let mime = EImageType.JPEG;
  if (checkImageType(config.type)) {
    mime = config.type;
    originalMime = config.type;
  }
  const image = await dataURLtoImage(dataURL);
  const canvas = await imagetoCanvas(image, Object.assign({}, config));
  /**
   * 經(jīng)過測試發(fā)現(xiàn)喘落,blob.size與dataURL.length的比值約等于0.75
   * 這個(gè)比值可以同過dataURLtoFile這個(gè)方法來測試驗(yàn)證
   * 這里為了提高性能,直接通過這個(gè)比值來計(jì)算出blob.size
   */
  const proportion = 0.75;
  let imageQuality = 0.5;
  let compressDataURL;
  const tempDataURLs: string[] = [null, null];
  /**
   * HTMLCanvasElement.toBlob()以及HTMLCanvasElement.toDataURL()壓縮參數(shù)
   * 的最小細(xì)粒度為0.01最冰,而2的7次方為128瘦棋,即只要循環(huán)7次,則會(huì)覆蓋所有可能性
   */
  for (let x = 1; x <= 7; x++) {
    compressDataURL = await canvastoDataURL(canvas, imageQuality, mime);
    const CalculationSize = compressDataURL.length * proportion;
    // 如果到循環(huán)第七次還沒有達(dá)到精確度的值暖哨,那說明該圖片不能達(dá)到到此精確度要求
    // 這時(shí)候最后一次循環(huán)出來的dataURL可能不是最精確的赌朋,需要取其周邊兩個(gè)dataURL三者比較來選出最精確的;
    if (x === 7) {
      if (resultSize.max < CalculationSize || resultSize.min > CalculationSize) {
        compressDataURL = [compressDataURL, ...tempDataURLs]
          .filter(i => i) // 去除null
          .sort((a, b) => Math.abs(a.length * proportion - resultSize.accurate)
            - Math.abs(b.length * proportion - resultSize.accurate))[0];
      }
      break;
    }
    if (resultSize.max < CalculationSize) {
      tempDataURLs[1] = compressDataURL;
      imageQuality -= 0.5 ** (x + 1);
    } else if (resultSize.min > CalculationSize) {
      tempDataURLs[0] = compressDataURL;
      imageQuality += 0.5 ** (x + 1);
    } else {
      break;
    }
  }
  const compressFile = await dataURLtoFile(compressDataURL, originalMime);
  // 如果壓縮后體積大于原文件體積篇裁,則返回源文件沛慢;
  if (compressFile.size > file.size) {
    return file;
  }
  return compressFile;
};

其實(shí)上一個(gè)方案的痛點(diǎn)就在于,如何在每一個(gè)壓縮循環(huán)里處理尺寸和壓縮比例达布。

  • 尺寸
    在imagetoCanvas里团甲,修改了圖片的寬和高如下
width = myConfig.width || myConfig.height * image.width / image.height || image.width;
height = myConfig.height || myConfig.width * image.height / image.width || image.height;
  • 壓縮比例
    然后看這個(gè)循環(huán)
for (let x = 1; x <= 7; x++) {
    compressDataURL = await canvastoDataURL(canvas, imageQuality, mime);
    const CalculationSize = compressDataURL.length * proportion;
    // 如果到循環(huán)第七次還沒有達(dá)到精確度的值,那說明該圖片不能達(dá)到到此精確度要求
    // 這時(shí)候最后一次循環(huán)出來的dataURL可能不是最精確的黍聂,需要取其周邊兩個(gè)dataURL三者比較來選出最精確的伐庭;
    if (x === 7) {
      if (resultSize.max < CalculationSize || resultSize.min > CalculationSize) {
        compressDataURL = [compressDataURL, ...tempDataURLs]
          .filter(i => i) // 去除null
          .sort((a, b) => Math.abs(a.length * proportion - resultSize.accurate)
            - Math.abs(b.length * proportion - resultSize.accurate))[0];
      }
      break;
    }
    if (resultSize.max < CalculationSize) {
      tempDataURLs[1] = compressDataURL;
      imageQuality -= 0.5 ** (x + 1);
    } else if (resultSize.min > CalculationSize) {
      tempDataURLs[0] = compressDataURL;
      imageQuality += 0.5 ** (x + 1);
    } else {
      break;
    }
  }

總結(jié)

  1. 根據(jù)所要求的最終圖片大小,得出誤差范圍分冈;
  2. 在誤差范圍內(nèi)比對壓縮過后的圖片大小圾另,調(diào)整下一次壓縮的質(zhì)量;
  3. 初始化壓縮比例為0.5雕沉,單次調(diào)整的質(zhì)量尺度為±(1/2) ^ (x + 1) 集乔。總循環(huán)次數(shù)為7次坡椒;
    pass:總循環(huán)為7次的原因是壓縮比例最小細(xì)粒度為0.01扰路,因?yàn)?/(2^8) 約等于0.0078,當(dāng)壓縮比例從1/2開始對(1/2^2) 0.25 這種值進(jìn)行相加減將會(huì)趨近于最接近的壓縮比例倔叼。
  4. 若循環(huán)的確到了第七次汗唱,則不一定是最準(zhǔn)的,可以找前面的最接近的結(jié)果來作為最終計(jì)算結(jié)果丈攒。

如有紕漏哩罪,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市巡验,隨后出現(xiàn)的幾起案子际插,更是在濱河造成了極大的恐慌,老刑警劉巖显设,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件框弛,死亡現(xiàn)場離奇詭異,居然都是意外死亡捕捂,警方通過查閱死者的電腦和手機(jī)瑟枫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門斗搞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慷妙,你說我怎么就攤上這事榜旦。” “怎么了景殷?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵溅呢,是天一觀的道長。 經(jīng)常有香客問我猿挚,道長咐旧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任绩蜻,我火速辦了婚禮铣墨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘办绝。我一直安慰自己伊约,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布孕蝉。 她就那樣靜靜地躺著屡律,像睡著了一般。 火紅的嫁衣襯著肌膚如雪降淮。 梳的紋絲不亂的頭發(fā)上超埋,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機(jī)與錄音佳鳖,去河邊找鬼霍殴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛系吩,可吹牛的內(nèi)容都是我干的来庭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼穿挨,長吁一口氣:“原來是場噩夢啊……” “哼月弛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起絮蒿,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤尊搬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后土涝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幌墓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年但壮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜡饵。...
    茶點(diǎn)故事閱讀 39,764評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肢专,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情焦辅,我是刑警寧澤博杖,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布筷登,位于F島的核電站,受9級(jí)特大地震影響前方,放射性物質(zhì)發(fā)生泄漏狈醉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一惠险、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧班巩,春花似錦金吗、人聲如沸趣竣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单匣。三九已至,卻和暖如春码秉,著一層夾襖步出監(jiān)牢的瞬間鸡号,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工府蔗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姓赤。 一個(gè)月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓不铆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親誓斥。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評論 2 354