撸一个图片压缩脚本(node)

382 阅读2分钟

因为公司网站每次上传图片都要手动压缩,图片多了效率就很低,所以干脆写一个脚本,配置在package.json中,这样想压缩在终端执行一下命令,就一键压缩啦😄

压缩工具

尝试用了两个压缩图片的库,一个是Tinify,一个是imagemin,感觉压缩效果没啥区别,不过Tinify 的api每个月有500张的限制额度,要去官网上获取一下key值。

实现思路

实现过程很简单:

  1. 递归查找指定目录下的图片文件,存入一个数组中,并创建对应输出目录(递归查找为了保持原有的目录结构,即使是多层嵌套也某闷题啦😋)
  2. 遍历数组,压缩图片,输出到指定目录(对于压缩失败的图片,直接复制原文件到目标目录)
  3. 统计压缩前后文件体积对比

核心代码

Tinify

   // 调用tinify压缩图片
    compressImage(oldPath,newPath){
        const rs = fs.createReadStream(oldPath)
        const ws = fs.createWriteStream(newPath)
        return new Promise((resolve,reject)=>{
            rs.on("data",(chunkData)=>{
                tinify.fromBuffer(chunkData).toBuffer((error,resultData)=>{
                    if(error) return reject(error.message)
                    else{
                        ws.end(resultData)
                        ws.on("finish",()=>resolve())
                        ws.on("err",()=>reject(err.message))
                    } 
                })
             })
        })
    }

imagemin

imagemin([inputPath], {
                destination: outputPath,
                plugins: [
                    imageminJpegtran({
                        quality: [0.6, 0.8]
                    }),
                    imageminPngquant({
                        quality: [0.6, 0.8]
                    })
                ]
                }).then((res)=>{
                    let { data } = res[0]
                    this.outputSize += data.length
                    console.log(`${inputPath}  压缩完成!`)
                })
                .catch((err)=>{
                    this.outputSize += imgData.length
                    console.log(`${inputPath} 压缩失败,原因如下:${err}`)
                    // 压缩失败则直接复制图片文件到目标文件夹
                    fs.copyFileSync(inputPath,targetPath)
                })           
        })

具体代码

Tinify

const fs = require("fs")
const tinify = require("tinify");
const path = require("path")


class TinifyImgApplication{
    constructor(tinifyKey,inputPath,outputPath){
        this.key = tinifyKey
        this.inputPath = inputPath
        this.outputPath = outputPath
        this.initTinify()
    }

    initTinify(){
        tinify.key = key
        tinify.validate((error)=>{
            if(error){
                console.log("error",error)
            }
            if (this.remainingCompressions() <= 0) {
                console.log('压缩数量已经用完');
            }
              // 初始化验证成功
              console.log('压缩图片工具初始化成功!');
        })
    }

     // 计算剩余压缩张数
    remainingCompressions () {
        return 500 - tinify.compressionCount;
    }

    byteSize = (byte = 0) => {
        Esize = ["B","KB", "MB","GB","TB","PB","EB","ZB","YB"]
        if (byte === 0) return '0 B'
        const unit = 1024
        const i = Math.floor(Math.log(byte) / Math.log(unit))
        return (byte / Math.pow(unit, i)).toPrecision(3) + ' ' + Esize[i]
      }

    //递归查找指定目录下的图片文件,并创建对应输出目录
    deepFindFile(inputPath,outputPath){
        let imgArrayInfo = []
        fs.readdirSync(inputPath).forEach(file=>{
            let filePath = path.resolve(inputPath,file)
            let targetPath = path.resolve(outputPath,file)
            let fileStat = fs.statSync(filePath)
            if(fileStat.isDirectory()){
            // 创建对应输出目录
            if(!fs.existsSync(targetPath)) fs.mkdirSync(targetPath)
                imgArrayInfo = [...imgArrayInfo,...this.deepFindFile(filePath,targetPath)]
            }else{
                const fileNameReg = /\.(jpe?g|png|svg|gif)$/ 
                if(fileNameReg.test(filePath)){
                    let imgData = fs.readFileSync(filePath)
                    imgArrayInfo.push({
                        inputPath:filePath, //图片文件 原路径
                        file:imgData, //图片文件 目标存放路径
                        targetPath
                    })
                }
            }
        })
        return imgArrayInfo
    }
    // 调用tinify压缩图片
    compressImage(oldPath,newPath){
        const rs = fs.createReadStream(oldPath)
        const ws = fs.createWriteStream(newPath)
        return new Promise((resolve,reject)=>{
            rs.on("data",(chunkData)=>{
                tinify.fromBuffer(chunkData).toBuffer((error,resultData)=>{
                    if(error) return reject(error.message)
                    else{
                        ws.end(resultData)
                        ws.on("finish",()=>resolve())
                        ws.on("err",()=>reject(err.message))
                    } 
                })
             })
        })
    }

    // 压缩图片,并输出到指定文件夹
    processImages(){
         // 创建输出根目录
        if(!fs.existsSync(this.outputPath)) fs.mkdirSync(this.outputPath)
        this.imageInfo = this.deepFindFile(this.inputPath,this.outputPath)
        this.imageInfo.forEach(i=>{
            console.log(i)
        })

        this.imageInfo.forEach(imgInfo=>{
            const { inputPath,targetPath,imgData }  = imgInfo
            this.compressImage(inputPath,targetPath)
            .then(()=>{
                console.log(`${inputPath}  压缩完成`)
            })
            .catch((err)=>{
                console.log(`${inputPath} 压缩失败,原因如下:${err}`)
                // 压缩失败则直接复制图片文件到目标文件夹
                fs.copyFileSync(inputPath,targetPath)
            })
    })
}
}

const key = "xxxxxxxx"
const tinifyImgApplication = new TinifyImgApplication(key,"./in1","./out1")
tinifyImgApplication.processImages()



imagemin

import fs from "fs"
import path from "path"
import imagemin from 'imagemin';
import imageminJpegtran from 'imagemin-jpegtran';
import imageminPngquant from 'imagemin-pngquant';

class MinifyImgApplication{
    constructor(inputPath,outputPath){
        this.inputPath = inputPath
        this.outputPath = outputPath
        this.inputSize = 0
        this.outputSize = 0
    }
   //字符单位转换
    byteSize = (byte = 0) => {
        const Esize = ["B","KB", "MB","GB","TB","PB","EB","ZB","YB"]
        if (byte === 0) return '0 B'
        const unit = 1024
        const i = Math.floor(Math.log(byte) / Math.log(unit))
        return (byte / Math.pow(unit, i)).toPrecision(3) + ' ' + Esize[i]
      }

    //递归查找指定目录下的图片文件,并创建对应输出目录
    deepFindFile(inputPath,outputPath){
        let imgArrayInfo = []
        fs.readdirSync(inputPath).forEach(file=>{
            let filePath = path.resolve(inputPath,file)
            let targetPath = path.resolve(outputPath,file)
            let fileStat = fs.statSync(filePath)
            if(fileStat.isDirectory()){
            // 创建对应输出目录
            if(!fs.existsSync(targetPath)) fs.mkdirSync(targetPath)
                imgArrayInfo = [...imgArrayInfo,...this.deepFindFile(filePath,targetPath)]
            }else{
                const fileNameReg = /\.(jpe?g|png|svg|gif)$/ 
                if(fileNameReg.test(filePath)){
                    let imgData = fs.readFileSync(filePath)
                    imgArrayInfo.push({
                        inputPath:filePath, //图片文件 原路径(这个包括图片文件名)
                        imgData, 
                        outputPath, //图片文件 目标存放路径
                        targetPath //图片文件 目标存放路径(这个包括图片文件名)
                    })
                }
            }
        })
        return imgArrayInfo
    }

    // 压缩图片,并输出到指定文件夹
     processImages(){
        // 创建输出根目录
        if(!fs.existsSync(this.outputPath)) fs.mkdirSync(this.outputPath)
        this.imageInfo =  this.deepFindFile(this.inputPath,this.outputPath)
        const promises = this.imageInfo.map((imgInfo) =>{
            const { inputPath,targetPath,imgData,outputPath }  = imgInfo
            this.inputSize += imgData.length
            return imagemin([inputPath], {
                destination: outputPath,
                plugins: [
                    imageminJpegtran({
                        quality: [0.6, 0.8]
                    }),
                    imageminPngquant({
                        quality: [0.6, 0.8]
                    })
                ]
                }).then((res)=>{
                    let { data } = res[0]
                    this.outputSize += data.length
                    console.log(`${inputPath}  压缩完成!`)
                })
                .catch((err)=>{
                    this.outputSize += imgData.length
                    console.log(`${inputPath} 压缩失败,原因如下:${err}`)
                    // 压缩失败则直接复制图片文件到目标文件夹
                    fs.copyFileSync(inputPath,targetPath)
                })           
        })
        
        return Promise.allSettled(promises)
        
    }

     run(){
        this.processImages()
        .then(()=>{
            console.log(`一共处理图片文件 ${this.imageInfo.length} 个`)
            console.log(`压缩前体积为${this.byteSize(this.inputSize)},压缩后体积为${this.byteSize(this.outputSize)} `)
        })
        .catch((err)=>{
            console.log('error:',err)
        })  
    }
}

const minifyApp = new MinifyImgApplication("./in","./in") //构造函数传入压缩图片的 源目录以及输出目录
minifyApp.run()




运行效果如下

运行的是imagemin的脚本

image.png