利用 canvas 壓縮圖片

利用 canvas 壓縮圖片

前言

在一個移動端的項目中,圖片上傳是一個比較經常使用的功能。javascript

可是,目前手機的隨便拍的照片一張都要好幾 M , 直接上傳的話特別耗費流量,並且所需時間也比較長。html

因此須要前端在上傳以前先對圖片進行壓縮。前端

原理

要使用 js 實現圖片壓縮效果, 原理其實很簡單,主要是:java

  1. 利用 canvasdrawImage 將目標圖片畫到畫布上
  2. 利用畫布調整繪製尺寸,以及導出的 quality ,肯定壓縮的程度
  3. 利用 canvastoDataURL 或者 toBlob 能夠將畫布上的內容導出成 base64 格式的數據。

注意點

IOS 下會出現圖片翻轉的問題

這個須要 import EXIF from 'exif-js';來獲取到手機的方向,而後對 canvas 的寬高進行處理git

壓縮到特定大小

let imgDataLength = dataUrl.length; 獲取到數據後,判斷壓縮後的圖片大小是否知足需求,不然就下降尺寸以及質量,再次壓縮github

quality 對 png 等無效,因此導出格式統一爲 jpeg ,透明背景填充爲白色

// 填充白色背景
ctx.fillStyle = fillBgColor;
ctx.fillRect(0, 0, size.w, size.h);

具體源碼

/**
 * 文件讀取並經過canvas壓縮轉成base64
 * @param files
 * @param callback
 */

//EXIF js 能夠讀取圖片的元信息  https://github.com/exif-js/exif-js
import EXIF from 'exif-js';

// 壓縮圖片時 質量減小的值
const COMPRESS_QUALITY_STEP = 0.03;
const COMPRESS_QUALITY_STEP_BIG = 0.06;
// 壓縮圖片時,圖片尺寸縮放的比例,eg:0.9, 等比例縮放爲0.9
const COMPRESS_SIZE_RATE = 0.9;

let defaultOptions = {
    removeBase64Header: true,
    // 壓縮後容許的最大值,默認:300kb
    maxSize: 200 * 1024,
    fillBgColor: '#ffffff'
};

/**
 *  將待上傳文件列表壓縮並轉換base64
 *  !!!! 注意 : 圖片會默認被轉爲 jpeg , 透明底會加白色背景
 *  files : 文件列表 ,必須是數組
 *  callback : 回調,每一個文件壓縮成功後都會回調,
 *  options :配置
 *  options.removeBase64Header : 是否須要刪除 'data:image/jpeg;base64,'這段前綴,默認true
 *  @return { base64Data: '',fileType: '' }, //fileType強制改成jpeg
 */
export function imageListConvert(files, callback, options = {}) {
    if (!files.length) {
        console.warn('files is null');
        return;
    }

    options = { ...defaultOptions, ...options };
    // 獲取圖片方向--iOS拍照下有值
    EXIF.getData(files[0], function() {
        let orientation = EXIF.getTag(this, 'Orientation');
        for (let i = 0, len = files.length; i < len; i++) {
            let file = files[i];
            let fileType = getFileType(file.name);

            //強制改成jpeg
            fileType = 'jpeg';

            let reader = new FileReader();
            reader.onload = (function() {
                return function(e) {
                    let image = new Image();
                    image.onload = function() {
                        let data = convertImage(
                            image,
                            orientation,
                            fileType,
                            options.maxSize,
                            options.fillBgColor
                        );
                        if (options.removeBase64Header) {
                            data = removeBase64Header(data);
                        }
                        callback({
                            base64Data: data,
                            fileType: fileType
                        });
                    };
                    image.src = e.target.result;
                };
            })(file);
            reader.readAsDataURL(file);
        }
    });
}

/**
 * 將 image 對象 畫入畫布並導出base64數據
 */
export function convertImage(
    image,
    orientation,
    fileType = 'jpeg',
    maxSize = 200 * 1024,
    fillBgColor = '#ffffff'
) {
    let maxWidth = 1280,
        maxHeight = 1280,
        cvs = document.createElement('canvas'),
        w = image.width,
        h = image.height,
        quality = 0.9;

    /**
     * 這裏用於計算畫布的寬高
     */
    if (w > 0 && h > 0) {
        if (w / h >= maxWidth / maxHeight) {
            if (w > maxWidth) {
                h = (h * maxWidth) / w;
                w = maxWidth;
            }
        } else {
            if (h > maxHeight) {
                w = (w * maxHeight) / h;
                h = maxHeight;
            }
        }
    }

    let ctx = cvs.getContext('2d');
    let size = prepareCanvas(cvs, ctx, w, h, orientation);

    // 填充白色背景
    ctx.fillStyle = fillBgColor;
    ctx.fillRect(0, 0, size.w, size.h);

    //將圖片繪製到Canvas上,從原點0,0繪製到w,h
    ctx.drawImage(image, 0, 0, size.w, size.h);

    let dataUrl = cvs.toDataURL(`image/${fileType}`, quality);

    //當圖片大小 > maxSize 時,循環壓縮,而且循環不超過5次
    let count = 0;
    while (dataUrl.length > maxSize && count < 10) {
        let imgDataLength = dataUrl.length;
        let isDoubleSize = imgDataLength / maxSize > 2;

        // 質量一次降低
        quality -= isDoubleSize
            ? COMPRESS_QUALITY_STEP_BIG
            : COMPRESS_QUALITY_STEP;
        quality = parseFloat(quality.toFixed(2));

        // 圖片還太大的狀況下,繼續壓縮 。 按比例縮放尺寸
        let scaleStrength = COMPRESS_SIZE_RATE;
        w = w * scaleStrength;
        h = h * scaleStrength;

        size = prepareCanvas(cvs, ctx, w, h, orientation);

        //將圖片繪製到Canvas上,從原點0,0繪製到w,h
        ctx.drawImage(image, 0, 0, size.w, size.h);

        console.log(`imgDataLength:${imgDataLength} , maxSize --> ${maxSize}`);
        console.log(`size.w:${size.w}, size.h:${size.h}, quality:${quality}`);
        dataUrl = cvs.toDataURL(`image/jpeg`, quality);
        count++;
    }

    console.log(`imgDataLength:${dataUrl.length} , maxSize --> ${maxSize}`);
    console.log(`size.w:${size.w}, size.h:${size.h}, quality:${quality}`);

    cvs = ctx = null;
    return dataUrl;
}

/**
 * 準備畫布
 * cvs 畫布
 * ctx 上下文
 * w : 想要畫的寬度
 * h : 想要畫的高度
 * orientation : 屏幕方向
 */
function prepareCanvas(cvs, ctx, w, h, orientation) {
    cvs.width = w;
    cvs.height = h;
    //判斷圖片方向,重置canvas大小,肯定旋轉角度,iphone默認的是home鍵在右方的橫屏拍攝方式
    let degree = 0;
    switch (orientation) {
        case 3:
            //iphone橫屏拍攝,此時home鍵在左側
            degree = 180;
            w = -w;
            h = -h;
            break;

        case 6:
            //iphone豎屏拍攝,此時home鍵在下方(正常拿手機的方向)
            cvs.width = h;
            cvs.height = w;
            degree = 90;
            // w = w;
            h = -h;
            break;
        case 8:
            //iphone豎屏拍攝,此時home鍵在上方
            cvs.width = h;
            cvs.height = w;
            degree = 270;
            w = -w;
            // h = h;
            break;
    }

    // console.log(`orientation --> ${orientation} , degree --> ${degree}`);
    // console.log(`w --> ${w} , h --> ${h}`);
    //使用canvas旋轉校訂
    ctx.rotate((degree * Math.PI) / 180);
    return { w, h };
}

/**
 * 截取 ‘data:image/jpeg;base64,’,
 * 截取到第一個逗號
 */
export function removeBase64Header(content) {
    if (content.substr(0, 10) === 'data:image') {
        let splitIndex = content.indexOf(',');
        return content.substring(splitIndex + 1);
    }
    return content;
}

export function getFileType(fileName = '') {
    return fileName.substring(fileName.lastIndexOf('.') + 1);
}

export function checkAccept(
    file,
    accept = 'image/jpeg,image/jpg,image/png,image/gif'
) {
    return accept.toLowerCase().indexOf(file.type.toLowerCase()) !== -1;
}

相關連接

我的博客
代碼片斷canvas

相關文章
相關標籤/搜索