利用canvas+Image()+File()进行图片压缩

946 阅读4分钟

图片上传时偶尔会遇到图片过大的问题,我们可以利用canvas+Image()+File()进行图片的压缩

注:本文中代码仅为在理想状态下实现功能,没有做任何设备兼容或者接口检查

// 文件流文件转base64
       fileToBase64(file) {
            return new Promise(resolve => {
                // 创建文件对象
                let fileReader = new FileReader();
                // 读取file文件,得到的结果为base64位
                fileReader.readAsDataURL(file);
                fileReader.onload = function () {
                    // 把读取到的base64
                    resolve(this.result);
                }
            })
        };

// base64转文件流

       base64ToFile(base64, name = 'test') {
            return new Promise(resolve => {
                let arr = base64.split(','), // base64拆分数据 头部为图片格式信息
                    mime = arr[0].match(/:(.*?);/)[1], // 图片格式
                    type = mime.split('/')[1],
                    bstr = atob(arr[1]), // 将base64解码
                    n = bstr.length, // 图片真实大小
                    u8arr = new Uint8Array(n); 
                    // Uint8Array 数组类型表示一个8位无符号整型数组,
                    // 创建时内容被初始化为0。
                    // 创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。
                while (n--) {
                    u8arr[n] = bstr.charCodeAt(n); // 获取图片每一位字符的Unicode
                }
                // 生成新文件
                let newFile = new File([u8arr], `${name}.${type}`, {
                    type: mime
                });
                resolve(newFile);
            })
        }

通过这两个方法我们可以将图片在文件和base46之间互相转换

为了方便使用我们封装一个HandleImage类,包含上面两个方法和图片压缩方法

class HandleImage {

    // 文件流文件转base64
    fileToBase64(file){ //... };

    // base64转二进制文件流
    base64ToFile(base64, name = 'test') { //... }

    /** 
    * 压缩图片 
    * @param {参数obj} param 
    * @param {二进制文件流文件} param.file 必传 
    * @param {文件名称} param.name 选传 文件名称 默认test 
    * @param {图片质量} param.quality 选传 压缩比率 0~1 默认值0.92 
    * @param {type} param.type 选传 文件类型/格式 默认'image/jpeg' 
    * @param {输出} param.out 选传 希望方法输出数据类型:base64/file  默认file 
    * */ 
    compressImg(param){
        let _this = this;   
        return new Promise(resolve => {
            const {
                file, 
                quality = 0.92, 
                name = 'test', 
                type = 'image/jpeg', 
                out = 'file'} = param;        
            let image = new Image();        
            _this.fileToBase64(file).then(res => {                
                image.src = res;
            })
            image.onload = function () {
                //创建一个canvas
                let canvas = document.createElement('canvas');
                //获取上下文
                let context = canvas.getContext('2d');            
                // 默认原图宽度
                canvas.width = image.width;
                // 默认原图高度
                canvas.height = image.height;
                //把图片绘制到canvas上面
                context.drawImage(image, 0, 0, canvas.width, canvas.height);
                //压缩图片,获取到新的base64Url             
                // todo 注:quality为图片质量 并非图片大小            
                let compressBase64 = canvas.toDataURL(type, quality);   
                canvas = null; // 销毁canvas         
                if (out === 'file') {    
                    let newFile = _this.base64ToFile(compressBase64, name)    
                    resolve(newFile)
                } else {    
                    resolve(compressBase64)
                }        
            }
       })
    };
}

可以看到生成的新图片在降质50%后大小发生了明显的变化而小图的情况下和原图在视觉上没有特别大的区别

有了压缩图片方法我们可以递归调用多次直到压缩图片到期望的大小,

所以当压缩比率 quality 越接近1,最后输出的结果越接近期望的大小。

但是发现在实际操作中会产生“边界效应”。

我目前的知识无法从专业的角度讲解“边界效应”的原理,不过可以描述一下现象:

拿我的demo图来说,原图大小8.25M当我们希望压缩到5M,以0.92默认的质量压缩比率进行处理时一次就会拿到结果图片,但大小只有2.5M;

这时我们以0.99质量压缩比率进行处理递归了10次花费20秒的时间才将图片压缩到了7.7M,且按比例压缩每次被压缩调的大小会越来越小,甚至可能重复无数次,这显然也是不合理的。

所以我们在HandelImage类中增加一个计数器来记录压缩次数,同时设置一个阀值,达到这个阀值后直接输出结果,此时我们可以通过灵活的控制压缩比率 quality 和阀值 count 来进行指定大小的压缩。

class HandleImage {

    // 文件流文件转base64
    fileToBase64(file){ // ... };

    // base64转二进制文件流
    base64ToFile(base64, name = 'test') { // ... }

    // 压缩图片至指定质量比率   compressImg(param){ // ... }/** 
* 压缩图片到指定大小 
* @param {参数obj} param 
* @param {二进制文件流文件} param.file 必传 
* @param {希望输出文件大小} param.size 必传 
* @param {最大压缩次数} param.count 选传 默认5 
* 由于压缩至指定大小时如果过于追求大小精确会发生边界效应导致压缩次数增加,所以增加一个最大压缩次数 
* @param {名称} param.name 选传 文件名称 默认test 
* @param {每次压缩使用图片质量} param.quality 选传 压缩比率 0~1 默认值0.92,
* 越接近1最终大小越接近希望大小,同时约占用资源越多,持续时间越长
* @param {type} param.type 选传 文件类型/格式 默认'image/jpeg' 
* @param {输出} param.out 选传 希望方法输出数据类型:base64/file  默认file 
* */async compressImgToSize(param){
    let _this = this;
    const {size,count = 5} = param;
    let newFile = await _this.compressImg(param);    
    if(newFile.size > size && _this.count < count){
        param.file = newFile        
        _this.count++;
        return await _this.compressImgToSize(param);        
    } else {
        _this.count = 0;        
        return newFile;
    }
}; /** 
* 图片压缩计数 
* 由于压缩至指定大小时如果过于追求大小精确会发生边界效应导致压缩次数增加 
* 边界效应:如果每次压图的质量比率为0.9可能一次就会输出结果但比期望值要小很多,
* 如果以0.95以上压缩图片可能会持续压图无数次才会得到一个精确的结果,显然也是不合理的 
* 所以增加一个最大压缩次数 
* */   
count = 0;
}