哇塞,有好吃的~

前端图片压缩,不借助任何第三方api

前端图片压缩

  • 需求:最近做项目,要求900k-5M的图片,需要压缩到900K以下
  • 实现:利用canvas.toDataURL(type, encoderOptions)toBlob(callback, type, encoderOptions)实现

前置知识点

  • canvas.toDataURL(type, encoderOptions),直接照搬MDN的文档描述。总的来说,想要用这个方法压缩图片,调用toDataURL和toBlob的时候,type只能jpeg或者webp的格式来处理,不然压缩是无效的。最后输出的时候是可以换成任意格式的图片的,比如我就改成了png格式的图片输出。
    1. 如果画布的高度或宽度是0,那么会返回字符串“data:,”。
    2. 如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。
    3. Chrome支持“image/webp”类型。
    4. 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
  • canvas.toBlob(callback, type, encoderOptions),这个方法用起来比上面简单一点,因为上面的方法需要处理base64的URL转换成bufferArray,这个直接使用blob对象去初始化File就可以。
    1. callback,回调函数,可获得一个单独的Blob对象参数。
    2. type,DOMString类型,指定图片格式,默认格式为image/png。
    3. encoderOptions,Number类型,值在0与1之间,当请求图片格式为image/jpeg或者image/webp时用来指定图片展示质量。如果这个参数的值不在指定类型与范围之内,则使用默认值,其余参数将被忽略。
  • atob(base64Str),将base64的字符串解码,与btoa(str)功能相反,因为toDataURL返回的是一个base64编码的地址,需要将其解析并转化成文件。
  • String.prototype.charCodeAt(),返回 0 到 65535 之间的整数,表示给定索引处的 UTF-16 代码单元。

代码实现

// 使用toBlob
// file,需要压缩的file对象
// quality,压缩的初始质量
// targetSize,需要压缩的临界值,当压缩一次后比这个临界值要大,就自动递归压缩
function compress(file, quality = 0.9, targetSize = 900 * 1024) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = (e) => {
            const src = e.target.result;
            const image = new Image();
            image.src = src;
            image.onload = () => {
                const canvas = document.createElement('canvas');
                const width = image.width;
                const height = image.height;
                canvas.width = width;
                canvas.height = height;

                const context = canvas.getContext('2d');
                context.drawImage(image, 0, 0, width, height);

                canvas.toBlob((blob) => { 
                    // 将文件名后缀改成png,并修改文件格式为png格式
                    const miniFile = new File([blob], file.name.replace(/\.[a-zA-Z]+$/g, '.png'), { type: 'image/png' })
                    if(miniFile.size > targetSize) {
                        compress(file, quality * 0.9, targetSize).then(mini => resolve(mini));
                    } else {
                        resolve(miniFile);
                    }
                }, 'image/jpeg', quality);
            }
            image.onerror = (err) => {
                reject(err);
            }
        }
        reader.onerror = (err) => {
            reject(err);
        }
    })
}
// 使用toDateURL
function compress(file, quality = 0.9, targetSize = 900 * 1024) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = (e) => {
            const src = e.target.result;
            const image = new Image();
            image.src = src;
            image.onload = () => {
                const canvas = document.createElement('canvas');
                const width = image.width;
                const height = image.height;
                canvas.width = width;
                canvas.height = height;

                const context = canvas.getContext('2d');
                context.drawImage(image, 0, 0, width, height);

                const canvasURL = canvas.toDataURL('image/jpeg', quality);
                const buffer = atob(canvasURL.split(',')[1]) // 获取实际的文件内容
                let length = buffer.length
                // 将每一块的内容映射成对应的unicode编码
                const bufferArray = new Uint8Array(new ArrayBuffer(length)).map((item, index) => buffer.charCodeAt(index))
                // 将文件名后缀改成png,并修改文件格式为png格式
                const miniFile = new File([bufferArray], file.name.replace(/\.[a-zA-Z]+$/g, '.png'), { type: 'image/png' })
                if(miniFile.size > targetSize) {
                    compress(file, quality * 0.9, targetSize).then(mini => resolve(mini));
                } else {
                    resolve(miniFile);
                }
            }
            image.onerror = (err) => {
                reject(err);
            }
        }
        reader.onerror = (err) => {
            reject(err);
        }
    })
}
// 最近又发现可以通过尺寸压缩,实现起来更简单
// ratio尺寸压缩比,需要压缩多少倍
function compress(file, ratio) {
    return new Promise((resolve, reject) => {
        const size = file.size;
        const suffix = file.name.match(/\.\w+$/g)[0];
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function(e) {
            const src = e.target.result;
            const img = new Image();
            img.src = src;
            img.onload = function() {
                const w = img.width,
                    h = img.height;
                const canvas = document.createElement('canvas');
                canvas.width = w / ratio;
                canvas.height = h / ratio;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0, w / ratio, h / ratio);
                // 这里依然可以使用toBlob和toDataURL两种方式
                const url = canvas.toDataURL(`image/${suffix}`);
                const binary = btoa(url.split(',')[1]);
                const bufferArray = new Uint8Array(new ArrayBuffer(binary.length)).map((item, index) => binary.charCodeAt(index));
                const miniFile = new File([bufferArray], file.name, { type: `image/${suffix}` });
                resolve(miniFile)
            }
            img.onerror = function() {
                reject('Img load fail.')
            }
        }
        reader.onerror = function() {
            reject('Reader fail.')
        }
    })
}
// 调用
compress(file).then(miniFile=> {
  console.log(miniFile);
}).catch(err => {
  console.log(err)
})

注意点

  • 压缩很难精确到固定的尺寸,如果对于临界尺寸有偏差值的要求,需要在递归条件上自己拓展,目前只要小于临界值就可以,如果需要更精确,可以改成一个尺寸的范围判断,如果太小了,就把压缩比例再上调,直到满足条件为止。
  • 目前压缩过后的文件类型我自己改成了png,如果有其它要求,或者想动态的传递格式,可以自行拓展该方法,将该参数提炼出来。
  • 当递归的次数比较多的时候,可能就会比较耗时,所以建议采用二分法去获取最佳尺寸,但是依然会比较耗时,如果考虑到界面交互的话,可以先展示原图加一个filter的blur,然后等待压缩完成后替换成压缩后的图片。
posted @ 2021-04-12 10:30  风行者夜色  阅读(476)  评论(0编辑  收藏  举报