Canvas 进阶(六)实现图片压缩功能

1,746 阅读5分钟

功能体验

先看demo:图片压缩

项目源码:github

效果如下:

实现功能

  1. 能够获取压缩后的 base64 图片
  2. 能够获取压缩后的图片节点,并能替换文档中的图片节点
  3. 能够获取压缩后的 canvas 进行二次绘制,并能替换文档中的 canvas
  4. 能过获取压缩后的 blob 文件
  5. 能够下载压缩后的图片

具体实现

前端实现压缩功能,从图片大小和质量两方面着手:缩小图片和降低图片质量

因此我们设计一个 imageCompress 类,传入一个 option, 其参数有:

file: url 或 file
width: 输出图片的宽度
height: 输出图片的高度
mineType: 输出图片的格式,默认为image/png
quality: 输出图片的画质。值为0~1,默认为1 

因为图片的加载是一个异步的过程,因此我们需要借助 promise, 以new ImageCompress(option).then(instance => {})创建并在 then 中调用实例的方法。因为 file 可以是 url 也可以是 file 对象,因此在构建函数中需对这两种情况分别判断,并在结束时返回 promise

1. 创建此类

class imageCompress {
    constructor(options) {
        this._img = null; // 原始图片
        this._compressedImg = null; // 压缩后的图片
        this._canvas = null; // 创建的canvas
        this._blob = null; // 创建的blob
        
        // 默认设置
        this.options = {
            mimeType: 'image/png',
            quality: 1,
        };
        // 合并默认参数与用户参数
        extend(this.options, options, true);

        // 文件为传入抛出错误
        if (!options.file) {
            throw new Error('图片未传入');
        }

        let that = this;
        if (typeof options.file === 'string') {
            let img = (this._img = new Image());
            // 解决跨域报错问题
            img.setAttribute('crossOrigin', 'anonymous');
            img.src = options.file;

            return new Promise((resolve, reject) => {
                img.onload = function() {
                    resolve(that);
                };
                img.onerror = function(...args) {
                    reject(new Error('图片加载失败'));
                };
            });
        } else if (typeof options.file === 'object' && options.file.name) {
            // 判断文件的后缀是否为图片
            if (!isAssetTypeAnImage(options.file.name)) {
                throw new Error('该文件不是图片');
            }
            let image = (this._img = new Image());
            return new Promise((resolve, reject) => {
                if (window.URL) {
                    image.src = window.URL.createObjectURL(options.file);
                } else {
                    const reader = new FileReader();
                    reader.onload = e => {
                        image.src = e.target.result;
                    };

                    reader.onabort = () => {
                        reject(
                            new Error(
                                'Aborted to load the image with FileReader.'
                            )
                        );
                    };
                    reader.onerror = () => {
                        reject(
                            new Error(
                                'Failed to load the image with FileReader.'
                            )
                        );
                    };

                    reader.readAsDataURL(options.file);
                }
                image.onload = function() {
                    resolve(that);
                };
                image.onerror = function(...args) {
                    reject(new Error('图片加载失败'));
                };
            });
        }
    }
}

2. 获取 canvas 和与页面 canvas 进行替换功能

我们在构建函数中已经将传入的图片加载并赋值给了 this._img, 接下来创建一个 canvas, 并将此图片按设置的大小画出来,便得到目标 canvas; 替换一个节点,先找出其父节点,并用一个新节点替换 oldNode.parentNode.replaceChild(newNode, oldNode);

// 获取canvas,可用于二次加工绘制
getCanvas() {
    if (!this._canvas) this._imagetoCanvas();
    return this._canvas;
}
// 私有方法,图片转canvas
_imagetoCanvas() {
    let image = this._img;
    var cvs = (this._canvas = document.createElement('canvas'));
    var ctx = cvs.getContext('2d');
    cvs.width = this.options.width || image.width;
    // 高度默认等比例压缩
    cvs.height = this.options.width
        ? (this.options.width * image.height) / image.width
        : image.height;
    ctx.drawImage(image, 0, 0, cvs.width, cvs.height);
}
// 替换文档canvas节点
replaceCanvasNode(oldNode) {
    let newNode = this.getCanvas();
    // 使新节点具有旧节点的id,类名,样式
    newNode.style.cssText = oldNode.style.cssText;
    newNode.id = oldNode.id;
    newNode.className = oldNode.className;
    // 用新节点替换旧节点
    oldNode.parentNode.replaceChild(this.getCanvas(), oldNode);
}

3. 获取压缩后的图片base64

前一步我们已经能够获取 canvas,将 canvas 调用 canvas.toDataURL(this.options.mimeType, this.options.quality)即可获取 base64

getImageBase64() {
    let canvas = this.getCanvas()
    return canvas.toDataURL(this.options.mimeType, this.options.quality);
}

4. 获取压缩后的文件

获取blob调用 canvas.toBlob(callback,mimeType,quality), 由于此过程也是异步,因此返回 promise

// 获取压缩后的文件,return promise.resolve(blob)
getCompressFile() {
    if (!this._canvas) this._imagetoCanvas();
    let that = this;
    return new Promise((resolve, reject) => {
        that._canvas.toBlob(
            blob => {
                that._blob = blob;
                resolve(blob);
            },
            that.options.mimeType, // 图片类型
            that.options.quality // 图片质量
        );
    });
}

5. 获取压缩后的 img 节点与页面 img 进行替换功能

this._compressedImg 指向压缩后的图片,我们的目标是找到 image 的 src 属性,有两种方法 URL.createObjectURL(blob)new FileReader().readAsDataURL(blob), 因此我们需调用第 4 步实现的方法 getCompressFile 获取 blob

// 获取压缩后的图片节点
getCompressImageNode() {
    // 如果压缩后的图片已经创建,则不需要重复创建,返回即可
    if (this._compressedImg && this._compressedImg.src)
        return Promise.resolve(this._compressedImg);
    let image = (this._compressedImg = new Image());
    return this.getCompressFile().then(blob => {
        if (window.URL) {
            image.src = window.URL.createObjectURL(blob);
        } else {
            const reader = new FileReader();

            reader.onload = e => {
                image.src = e.target.result;
            };
            // 终止事件
            reader.onabort = () => {
                return Promise.reject(
                    new Error('Aborted to load the image with FileReader.')
                );
            };
            reader.onerror = () => {
                return Promise.reject(
                    new Error('Failed to load the image with FileReader.')
                );
            };

            reader.readAsDataURL(blob);
        }
        return Promise.resolve(image);
    });
}
// 替换页面图片
replaceImageNode(oldNode) {
    this.getCompressImageNode().then(image => {
        image.style.cssText = oldNode.style.cssText;
        image.id = oldNode.id;
        image.className = oldNode.className;
        oldNode.parentNode.replaceChild(image, oldNode);
    });
}

6. 下载压缩后的文件

this._compressedImg 被赋值且其 src 属性存在时,可以直接创建 a 标签下载;若没有创建压缩后的 img, 则调用上一步创建的 getCompressImageNode() 方法获取压缩后的 img, 再进行下载

// 下载压缩后的文件
downloadCompressFile(name = 'compress-file') {
    if (this.blob && this._compressedImg) {
        const dataURL = this._compressedImg.src;
        const link = document.createElement('a');
        link.download = name;
        link.href = dataURL;
        link.dispatchEvent(new MouseEvent('click'));
    } else {
        this.getCompressImageNode().then(image => {
            const dataURL = image.src;
            const link = document.createElement('a');
            link.download = name;
            link.href = dataURL;
            link.dispatchEvent(new MouseEvent('click'));
        });
    }
}

demo 测试

对上述7个功能进行测试,效果及代码如下:

new imageCompress({
    file:
        'https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1477436123,3577936229&fm=26&gp=0.jpg',
    width: 500,
    quality: 1,
    mimeType: 'image/jpeg',
})
    .then(instance => {
        // 获取canvas,可用于自行加工绘制
        let canvas = instance.getCanvas();
        let context = canvas.getContext('2d');
        context.moveTo(100, 100);
        context.lineTo(50, 50);
        context.stroke();

        // 替换文档中存在图片节点
        instance.replaceImageNode(document.getElementById('img'));
        // 替换文档中存在的canvas节点
        instance.replaceCanvasNode(document.getElementById('canvas'));
        // 获取压缩后生成的image节点
        instance.getCompressImageNode().then(image => {
           console.log(image)
        });

        // // 获取压缩后的blob文件,可用于上传
        instance.getCompressFile().then(blob => {
           
        });
        // 获取图片base64
        let base64 = instance.getImageBase64();
        // 下载压缩后文件
        // instance.downloadCompressFile();
    })
    .catch(err => {
        console.log(err);
    });

以上是我能想到的图片压缩的7个功能,如果你有想到其他的需求,欢迎在评论区留言。如果文中有错漏,也欢迎指出!

更多推荐

前端进阶小书(advanced_front_end)

前端每日一题(daily-question)

webpack4 搭建 Vue 应用(createVue)

Canvas 进阶(一)二维码的生成与扫码识别

Canvas 进阶(二)写一个生成带logo的二维码npm插件

Canvas 进阶(三)ts + canvas 重写”辨色“小游戏

Canvas 进阶(四)实现一个“刮刮乐”游戏

Canvas 进阶(五)实现图片滤镜效果

VUI创建日志(一)——图片懒加载指令的实现

VUI创建日志(二)——防抖节流组件的实现

前端算法题目解析(一)

前端算法题目解析(二)

简易路由实现——(hash路由)

简易路由实现——(history路由)