关于图片压缩(pc端、小程序端、node端)

571 阅读9分钟

前端及node端的图片压缩处理

前端

此处我的业务场景是后台管理和小程序,后台管理就是正常的vue+elementUI,小程序是原生无UI框架,后台管理端主要涉及到的图片上传为文章主图上传、富文本编辑器中的图片上传,此处先做正常的文章主图上传压缩处理。

input框的图片选择限制选择文件类型为图片下的pngjpgjpeg

pc端封装

H5端的图片压缩是使用的canvas重新绘图来实现的。具体代码如下

/**
 * 压缩图片方法
 * @param {file} file 文件
 * @param {Number} quality 图片质量(取值0-1之间默认0.92)
 */
function compressImg(file, quality) {
    var qualitys = 0.52
    console.log(parseInt((file.size / 1024).toFixed(2)))
    if (parseInt((file.size / 1024).toFixed(2)) < 1024) {
        qualitys = 0.85
    }
    if (5 * 1024 < parseInt((file.size / 1024).toFixed(2))) {
        qualitys = 0.92
    }
    if (quality) {
        qualitys = quality
    }
    if (file[0]) {
        return Promise.all(Array.from(file).map(e => this.compressImg(e,
            qualitys))) // 如果是 file 数组返回 Promise 数组
    } else {
        return new Promise((resolve) => {
            console.log(file)
            if ((file.size / 1024).toFixed(2) < 300) {
                resolve({
                    file: file
                })
            } else {
                const reader = new FileReader() // 创建 FileReader
                reader.onload = ({
                    target: {
                        result: src
                    }
                }) => {
                    const image = new Image() // 创建 img 元素
                    image.onload = async () => {
                        const canvas = document.createElement('canvas') // 创建 canvas 元素
                        const context = canvas.getContext('2d')
                        var targetWidth = image.width
                        var targetHeight = image.height
                        var originWidth = image.width
                        var originHeight = image.height
                        if (1 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 10 * 1024) {
                            var maxWidth = 1600
                            var maxHeight = 1600
                            targetWidth = originWidth
                            targetHeight = originHeight
                            // 图片尺寸超过的限制
                            if (originWidth > maxWidth || originHeight > maxHeight) {
                                if (originWidth / originHeight > maxWidth / maxHeight) {
                                    // 更宽,按照宽度限定尺寸
                                    targetWidth = maxWidth
                                    targetHeight = Math.round(maxWidth * (originHeight / originWidth))
                                } else {
                                    targetHeight = maxHeight
                                    targetWidth = Math.round(maxHeight * (originWidth / originHeight))
                                }
                            }
                        }
                        if (10 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 20 * 1024) {
                            maxWidth = 1400
                            maxHeight = 1400
                            targetWidth = originWidth
                            targetHeight = originHeight
                            // 图片尺寸超过的限制
                            if (originWidth > maxWidth || originHeight > maxHeight) {
                                if (originWidth / originHeight > maxWidth / maxHeight) {
                                    // 更宽,按照宽度限定尺寸
                                    targetWidth = maxWidth
                                    targetHeight = Math.round(maxWidth * (originHeight / originWidth))
                                } else {
                                    targetHeight = maxHeight
                                    targetWidth = Math.round(maxHeight * (originWidth / originHeight))
                                }
                            }
                        }
                        canvas.width = targetWidth
                        canvas.height = targetHeight
                        context.clearRect(0, 0, targetWidth, targetHeight)
                        context.drawImage(image, 0, 0, targetWidth, targetHeight) // 绘制 canvas
                        const canvasURL = canvas.toDataURL('image/jpeg', qualitys)
                        const buffer = atob(canvasURL.split(',')[1])
                        let length = buffer.length
                        const bufferArray = new Uint8Array(new ArrayBuffer(length))
                        while (length--) {
                            bufferArray[length] = buffer.charCodeAt(length)
                        }
                        const miniFile = new File([bufferArray], file.name, {
                            type: 'image/jpeg'
                        })
                        console.log({
                            file: miniFile,
                            origin: file,
                            beforeSrc: src,
                            afterSrc: canvasURL,
                            beforeKB: Number((file.size / 1024).toFixed(2)),
                            afterKB: Number((miniFile.size / 1024).toFixed(2)),
                            qualitys: qualitys
                        })
                        resolve({
                            file: miniFile,
                            origin: file,
                            beforeSrc: src,
                            afterSrc: canvasURL,
                            beforeKB: Number((file.size / 1024).toFixed(2)),
                            afterKB: Number((miniFile.size / 1024).toFixed(2))
                        })
                    }
                    image.src = src
                }
                reader.readAsDataURL(file)
            }
        })
    }
}

小程序端封装

当前业务中的小程序端涉及到的图片上传就是用户在补充信息时需要进行上传头像,用户在浏览个人资料的时候可以更改自己的头像

小程序端进行图片压缩主要有三种方式可选择,一种是小程序官方提供的接口wx.compressImage()官方文档提供的接口能力,很怪,ios大多数情况下是可以的,但是安卓有时候反而会把图片变大,不做选择。第二种是用canvas重绘来进行图片压缩,可自定义性比较强一点。第三种就是找一下有没有图片压缩的插件之类的了。我选用的是第二种方式。

小程序端的canvas绘制相关api有做更新,这里做老版和新版的写法说明,老版写法参考的文章微信小程序对图片进行canvas压缩的方法示例详解

老版写法尝试成功,新版写法未做尝试。

老版写法

// 封装utils功能文件
// 通过canvas将图片压缩至指定大小
// 判断图片大小是否满足需求,limitSize的单位是kb
function imageSizeIsLessLimitSize(imagePath, limitSize, lessCallback, moreCallback) {
    //获取文件信息
    console.log(imagePath);
    console.log(limitSize);
    wx.getFileInfo({
        filePath: imagePath,
        success: (res) => {
            console.log("压缩前图片大小", res.size / 1024, 'kb');
            //如果图片太大了走moreCallback
            if (res.size > 1024 * limitSize) {
                console.log('图片太大了');
                moreCallback()
            }
            //图片满足要求了走lessCallback
            else {
                console.log('图片满足要求');
                lessCallback()
            }
        }
    })
}

//将图片画在画布上并获取画好之后的图片的路径
function getCanvasImage(canvasId, imagePath, imageW, imageH, getImgSuccess) {
    //创建画布内容
    const ctx = wx.createCanvasContext(canvasId);
    //图片画上去,imageW和imageH是画上去的尺寸,图像和画布间隔都是0
    ctx.drawImage(imagePath, 0, 0, imageW, imageH);
    //这里一定要加定时器,给足够的时间去画(所以每次递归最少要耗时200ms,多次递归很耗时!)  老版实现方式是需要调用draw方法才会进行绘制,所以需要写个定时器,但是新版的是会同步执行的
    ctx.draw(false, setTimeout(function () {
        wx.canvasToTempFilePath({
            canvasId: canvasId,
            x: 0,
            y: 0,
            width: imageW,
            height: imageH,
            quality: 1, //最高质量,只通过尺寸放缩去压缩,画的时候都按最高质量来画
            success: (res) => {
                getImgSuccess(res.tempFilePath);
            }
        })
    }, 200));
}

//主函数,默认限制大小1024kb即1mb,drawWidth是绘画区域的大小
//初始值传入为画布自身的边长(我们这是一个正方形的画布)
function getLessLimitSizeImage(canvasId, imagePath, limitSize = 1024, drawWidth, callback) {
    //判断图片尺寸是否满足要求
    imageSizeIsLessLimitSize(imagePath, limitSize,
        (lessRes) => {
            //满足要求走callback,将压缩后的文件路径返回
            console.log('111');
            callback(imagePath);
        },
        (moreRes) => {
            console.log('222');
            //不满足要求需要压缩的时候
            wx.getImageInfo({
                src: imagePath,
                success: (imageInfo) => {
                    let maxSide = Math.max(imageInfo.width, imageInfo.height);
                    let windowW = drawWidth;
                    let scale = 1;
                    /*
                    这里的目的是当绘画区域缩小的比图片自身尺寸还要小的时候
                    取图片长宽的最大值,然后和当前绘画区域计算出需要放缩的比例
                    然后再画经过放缩后的尺寸,保证画出的一定是一个完整的图片。由于每次递归绘画区域都会缩小,
                    所以不用担心scale永远都是1绘画尺寸永远不变的情况,只要不满足压缩后体积的要求
                    就会缩小绘画区域,早晚会有绘画区域小于图片尺寸的情况发生
                    */
                    console.log(maxSide);
                    console.log(windowW);
                    if (maxSide > windowW) {
                        scale = windowW / maxSide;
                    }
                    console.log(scale);
                    // trunc是去掉小数
                    let imageW = Math.trunc(imageInfo.width * scale);
                    let imageH = Math.trunc(imageInfo.height * scale);
                    console.log('调用压缩', imageW, imageH);
                    // 图片在规定绘画区域上画并获取新的图片的path
                    getCanvasImage(canvasId, imagePath, imageW, imageH,
                        (pressImgPath) => {
                            /*
                            再去检查是否满足要求,始终缩小绘画区域,让图片适配绘画区域
                            这里乘以0.95是必须的,如果不缩小绘画区域,会出现尺寸比绘画区域小,
                            而体积比要求压缩体积大的情况出现,就会无穷递归下去,因为scale的值永远是1
                            但0.95不是固定的,你可以根据需要自己改,0到1之间,越小则绘画区域缩小的越快
                            但不建议取得太小,绘画区域缩小的太快,压出来的将总是很糊的
                            */
                            getLessLimitSizeImage(canvasId, pressImgPath, limitSize, drawWidth * 0.95, callback);
                        }
                    )
                }
            })
        }
    )
}

export default getLessLimitSizeImage
<!-- wxml --> 	
<!--用于图片压缩的canvas画布,不在页面中展示,且id固定不可变-->
<canvas style="width: {{cw}}px; height: {{cw}}px;position: absolute; z-index: -1; left: -10000rpx;; top: -10000rpx;" canvas-id="zipCanvas"></canvas>
<!--画布结束-->
// js
// 引入
import getLessLimitSizeImage from '../../../utils/compressImage'
//画板边长默认是屏幕宽度,正方形画布
data : {
    cw: wx.getSystemInfoSync().windowWidth,
}
// 在选择图片或者拍摄图片时调用
wx.chooseMedia({
            count: 1,
            mediaType: ['image'],
            sourceType: ['album', 'camera'],
            sizeType: ['original'],
            success: (res) => {
                let canvasId = 'zipCanvas' //注意这里的id和你在页面中写的html代码的canvas的id要一致
                let imagePath = res.tempFiles[0].tempFilePath; //原图的路径
                let limitSize = 300; //大小限制300kb,此处的限制是,超出这个大小的图片才会被进行压缩,不超出的会直接返回图片原地址
                let drawWidth = wx.getSystemInfoSync().windowWidth; //初始绘画区域是画布自身的宽度也就是屏幕宽度
                getLessLimitSizeImage(canvasId, imagePath, limitSize, drawWidth, (resPath) => {
                    console.log(resPath);
                    //resPath就是压缩后图片的路径,然后想做什么都随你
                })
            }
        })

新版的canvas绘制写法

// 主要是canvas这个函数的一些内容写法不同
//将图片画在画布上并获取画好之后的图片的路径
function getCanvasImage(canvasId, imagePath, imageW, imageH, getImgSuccess) {
    wx.createSelectorQuery()
        .select('#' + canvasId) // 在 WXML 中填入的 id
        .fields({
            node: true,
            size: true
        })
        .exec((res) => {
            // Canvas 对象
            const canvas = res[0].node
            // Canvas 画布的实际绘制宽高
            const renderWidth = res[0].width
            const renderHeight = res[0].height
            // Canvas 绘制上下文
            const ctx = canvas.getContext('2d')
            // 初始化画布大小
            const dpr = wx.getWindowInfo().pixelRatio
            canvas.width = renderWidth * dpr
            canvas.height = renderHeight * dpr
            ctx.scale(dpr, dpr)
            // 绘制前清空画布
            ctx.clearRect(0, 0, canvas.width, canvas.height)
            const image = canvas.createImage()
            image.onload = () => {
                ctx.drawImage(
                    image,
                    0,
                    0,
                    imageW,
                    imageH,
                )
            }
            image.src = imagePath
            setTimeout(() => {
                wx.canvasToTempFilePath({
                    canvas: canvas,
                    canvasId: canvasId,
                    x: 0,
                    y: 0,
                    width: imageW,
                    height: imageH,
                    quality: 1, //最高质量,只通过尺寸放缩去压缩,画的时候都按最高质量来画
                    success: (res) => {
                        getImgSuccess(res.tempFilePath);
                    }
                })
            }, 200);
        })
}
<!-- wxml -->
<canvas style="width: {{cw}}px; height: {{cw}}px;position: absolute; z-index: -1; left: -10000rpx;; top: -10000rpx;" id="zipCanvas" type="2d"></canvas>

这里的图片压缩是用图片清晰度来换取空间的,所以图片压缩的越小,就越模糊,如果业务场景是需要压缩体积且足够清晰,此种方法不可取。

node

后端我用的是node端做的接口,框架是express,主要是两个场景,一个是接口接收文件数据时先进行图片压缩处理然后保存在服务器本地或者上传到七牛云,此处选择的是保存在服务器本地。另一个场景是脚本执行,选择服务器某个图片资源文件夹,进行图片压缩处理。

接口接收文件数据进行图片压缩处理保存在服务器本地(也可选择接七牛云图片存储)

选择服务器本地图片进行压缩处理

此处选择的方案是使用npmtinify,具体的api功能可以查看官网tiniPNG,封装成js文件,随意更改图片资源的地址执行文件以压缩文件夹里的图片资源,但是tinify的限制是每个月免费500次请求次数,超出次数进行收费,所以目前我还没找到最优解。

刚开始我也尝试了另一个npmimages的使用,但是发现无法对png图片进行压缩,或者是压缩效果不明显,因为规定的quality压缩质量字段对png形式的图片是不起作用的,只好放弃。

tinify虽然不能规定压缩质量,但是压缩效果还是比较令人满意的,压缩前后对比,图片质量几乎没有明显的下降。

下载tinify

npm i tinify --save

如下为示例代码

/**
 * 在图片上传接口中使用
 */
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require("path");
var resJson = require('../../utils/resJson');
const tinify = require('tinify');
tinify.key = '**************'; //在tinify官网获取

// 单张图片上传图片地址
const IMAGES_PATH = path.resolve(__dirname, `../../public/images/public`);

// 操作图片压缩的
const compress = (path, name) => {
    var trueName = path + '/' + name;
    var sourse = tinify.fromFile(trueName); //输入文件
    sourse.toFile(trueName); //输出文件
}

// 单个图片上传
let upload = multer({
    storage: multer.diskStorage({
        destination: function (req, file, cb) {
            cb(null, './public/images/public/');
        },
        filename: function (req, file, cb) {
            var changedName = (new Date().getTime()) + '-' + file.originalname;
            cb(null, changedName);
        }
    })
});

//单个图片上传
router.post('/', upload.single('image'), (req, res) => {
    try {
        compress(IMAGES_PATH, req.file.filename)
        resJson.success(res, {
            imgUrl: 'images/public/' + req.file.filename
        })
    } catch (err) {
        resJson.srror(res, '头像上传失败', err)
    }
});

module.exports = router;
/**
 * 根据设置的路径,对文件夹下的所有图片(包括子文件夹)进行压缩处理
 */
const fs = require("fs");
const path = require("path");
const tinify = require('tinify');
tinify.key = '**************'; //在官网申请到的APIKEY,

// 单张图片上传图片地址
const IMAGES_PATH = path.resolve(__dirname, `../public/images/public`);

/**
 * @description: 进行图片压缩
 * @event: 
 * @param {*} path 要压缩图片的文件夹
 * @return {*}
 */

const compress = (path) => {
    fs.readdir(path, function (err, files) {
        if (err) {
            console.log('error:\n' + err);
            return;
        }
        files.forEach(function (file) {
            fs.stat(path + '/' + file, function (err, stat) {
                if (err) {
                    console.log(err);
                    return;
                }
                if (stat.isDirectory()) {
                    // 如果是文件夹遍历
                    compress(path + '/' + file);
                } else {
                    //遍历图片
                    var name = path + '/' + file;
                    var sourse = tinify.fromFile(name); //输入文件
                    sourse.toFile(name); //输出文件
                    if (err) {
                        console.log(err)
                    }
                }
            });
        });
    });
}

compress(IMAGES_PATH)

还有一可选方案,图片在上传之后,通过tinify转成webp形式的图片,体积会很小,然后存储在数据库中的图片地址字段为base64形式,这样小程序中也能正常展示,在查阅相关资料的时候看到有这样的方案,未做尝试。

本文作者:人模人样的搬砖老段

本文链接:前端及node端的图片压缩处理

如有错误,请及时评论或者知乎私信或者 B 站私信哦~

最后更新于:2022/10/11

ps:直接从我博客搬过来了