利用canvas压缩图片,同步的解决方案

问题背景

uni-app应用图片存储在七牛云上,后端只存储图片地址,一开始没有限制图片大小,导致手机拍的照片,大约6、7M直接存到七牛云中,虽然应用浏览人数不多,但仍有加载缓慢、浪费流量等问题。

解决办法

  1. 可以对上传的图片做大小限制,超过的限制的提示上传小点的图片,简单粗暴,但太不友好,大多人使用此应用都应该是直接拍照上传,此方案不可行。
  2. 进行压缩,七牛云提供png、jpg的图片自动压缩功能,但由手机拍照的图片为jpeg,还得转格式,不如直接压缩。

压缩方式

  • 使用canvas进行压缩
  • 原理:使用canvas进行重绘,将大的图片画在小的画布上,再将画出来的图片上传,实现压缩

具体过程

uni-appcanvas文档

  1. 在页面中声明一个canvas
    <canvas canvas-id="canvas1" class="canvas"/>

  2. 创建canvas, 参数为上方的canvas-id
    const canvas = uni.createCanvasContext('canvas');

  3. 画图

    uni.getImageInfo({
    src,
    success: async function (res) {
        let canvasWidth = res.width //图片原始长宽
        let canvasHeight = res.height;
        let base = canvasWidth/canvasHeight;
        if(canvasWidth>500){
            canvasWidth = 500;
            canvasHeight = Math.floor(canvasWidth/base);
        }
        let img = new Image();
        img.src = src;
        const canvas = uni.createCanvasContext('canvas');
        canvas.width = canvasWidth;
        canvas.height = canvasHeight;
        // 核心JS就这个
        canvas.drawImage(src,0,0,res.width, res.height,0,0,canvasWidth,canvasHeight);
        canvas.draw();
    }
    })
  4. 转成base64文件路径

    setTimeout(() => {
    // 延时执行的目的,需要等上方的canvas.draw()将片画完,才能转码,否则会得到的链接会没有图片
    uni.canvasToTempFilePath({
    width: canvasWidth,
    height: canvasHeight,
    destWidth: canvasWidth,
    destHeight: canvasHeight,
    canvasId: `canvas${i + 1}`,
    success: (res) => {
    // 在H5平台下,tempFilePath 为 base64
    // console.log(res.tempFilePath);
        imageList.push(res.tempFilePath.split(',')[1]);
        // 原路径为data:image/png;base64,iVBORw0KGgoAAA....
        // 但图片的数据只为逗号后边的,所以这只保留逗号后面的
    }
    }
    })
    }, 300)
  5. 图片上传至七牛云

    uni.request({
    header: {
    'Content-Type': 'application/octet-stream',
    'Authorization': `UpToken ${this.uploadToken}`
    },
    url: 'https://up-z1.qiniup.com/putb64/-1',// 华北区
    method: 'POST',
    data: imageList[i],  // 图片的base64数据,我是传多张,所以这里是数组
    complete: (res) => {
    if (res.statusCode === 200) {
    // 将图片地址保存,等图片上传完毕后存到后端,如果为一张图片,这里可以直接调用后台接口保存
    that.images.push('https://picture.wrpxcx.com/' + res.data.key);
    }
    }
    })

遇到的问题

  1. 画图是异步执行的,需要等图片画完后才能调用canvasToTempFilePath,否则会没有图片信息,采用setTimeout延时执行
  2. 调用后端接口需要等上传完七牛云后执行,如果是一张图片,可以直接在七牛云返回后进行调用,但应用需要多张图片的时候,就需要先保存,在上传。解决方法,使用async await 同步化
  3. 由于是多张图片,如果用一个画布的话,在setTimeout后在转码,会造成图片的覆盖,在调用canvasToTempFilePath时,多张图片都已经画好了,后面的会覆盖前面的,应用中最多为3张图片,所以使用3个画布
    完整代码

            async uploadToQiNiu() {
                const imageList = [];
                // 先将图片压缩,将压缩后生成的地址放到imageList中
                await this.compress(imageList);
                const that = this;
                return new Promise((resolve, reject) => {
                    let len = 0;
                    for (let i = 0; i < imageList.length; i++) {
                        uni.request({
                            header: { 
                                'Content-Type': 'application/octet-stream',
                                'Authorization': `UpToken ${this.uploadToken}`
                            },
                            // url: 'https://wrpxcx.com/api/v1/goods/goods',
                            url: 'https://up-z1.qiniup.com/putb64/-1',
                            method: 'POST',
                            data: imageList[i],
                            complete: (res) => {
                                len++;
                                if (res.statusCode === 200) {
                                    that.images.push('https://picture.wrpxcx.com/' + res.data.key);
                                }
                                if (len === imageList.length) {
                                    resolve('complete');
                                }
                            }
                        })
                    }
                })
            },

    图片压缩

            async compress(imageList) {
                const that = this;
                return new Promise((resolve, reject) => {
                    // 获取需要上传的图片的path列表
                    const imageListBeforeCompress = this.$refs.uUpload.lists;
                        for (let i = 0; i < imageListBeforeCompress.length; i++) {
                        // 利用提前生成好的画布画图
                            that.myDrawImage(i, imageListBeforeCompress[i].file.path);
                            setTimeout(() => {
                                let canvasWidth;
                                let canvasHeight;
                                uni.getImageInfo({
                                    src: imageListBeforeCompress[i].file.path,
                                    success: async (res) => {
                                        canvasWidth = res.width //图片原始长宽
                                        canvasHeight = res.height;
                                        let base = canvasWidth/canvasHeight;
                                        if(canvasWidth>500){
                                                canvasWidth = 500;
                                                canvasHeight = Math.floor(canvasWidth/base);
                                        }
                                        uni.canvasToTempFilePath({
                                            width: canvasWidth,
                                            height: canvasHeight,
                                            destWidth: canvasWidth,
                                            destHeight: canvasHeight,
                                            canvasId: `canvas${i + 1}`,
                                            success: (res) => {
                                                // 在H5平台下,tempFilePath 为 base64
                                                // console.log(res.tempFilePath);
                                                imageList.push(res.tempFilePath.split(',')[1]);
                                                if (imageList.length === imageListBeforeCompress.length) {
                                                    resolve('complete');
                                                }
                                            }
                                        })
                                    },
                                })
                            }, 300)
                        }
                })
            },

    利用画布画图

            async myDrawImage(i, src) {
                const that = this;
                let canvas;
                if (i === 0) {
                    canvas = this.canvas1;
                } else if (i === 1) {
                    canvas = this.canvas2;
                } else if ( i=== 2) {
                    canvas = this.canvas3;
                }
                    uni.getImageInfo({
                        src,
                        success: async function (res) {
                            let canvasWidth = res.width //图片原始长宽
                            let canvasHeight = res.height;
                            let base = canvasWidth/canvasHeight;
                            if(canvasWidth>500){
                                    canvasWidth = 500;
                                    canvasHeight = Math.floor(canvasWidth/base);
                            }
                            let img = new Image();
                            img.src = src;
                            // const canvas = uni.createCanvasContext('canvas');
                            canvas.width = canvasWidth;
                            canvas.height = canvasHeight;
                            // 核心JS就这个
                            canvas.drawImage(src,0,0,res.width, res.height,0,0,canvasWidth,canvasHeight);
                            canvas.draw();
                        }
                })
            }

需要注意的问题

画布如果需要隐藏,不能直接将displaye设置成none,或者将透明度opacity: 0(我就是这么干的),这样会导致画布的大小使用默认值,会造成部分图片只会画出来一部分。
可以通过设置定位解决:
top: -999999rpx;


分割线

由于一开始图片太大,使我的七牛云的账户由1块多直接变成了负的两块多。心疼~