20

【AutoTinyPng】从程序员的角度来压缩图片

 2 years ago
source link: https://segmentfault.com/a/1190000041025940
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【AutoTinyPng】从程序员的角度来压缩图片

发布于 41 分钟前

Dear,大家好,我是“前端小鑫同学”,😇长期从事前端开发,安卓开发,热衷技术,在编程路上越走越远~


说来很奇怪,现在的不少技术交流群里面存在这一些“伪程序员”,就比如说下图的这段对话,用在线的图片压缩网站要对自己的大量图片进行压缩,居然嫌麻烦都跑群里面问要怎么办?

从程序员的角度来解决这个问题:
  1. 上班摸鱼法:一张一张来,干一张算一张。
  2. 土豪氪金法:通过网站开放的API进行简单编程进行批量处理,当然你处理的越多就需要支付一些费用。
  3. 展示技术法:适合在合理的数量内,难得的机会中复习一下你的编程知识,还能把活干好。
  4. 其他:。。。

image.png

打码前的准备:

  1. 我们选择展示技术法来做今天的Demo,我也觉得这是一个程序员的选择(丢给美工的事我。。。);
  2. 一款产品的质量也是需要逐渐进行打磨优化,tinypng在程序员中间还是流传的较为好用的一款产品,我们依然选择tinypng,用别人专业的工具做自己的事,漂亮!。

    思路介绍:

  3. 递归获取本地文件夹里的文件
  4. 过滤文件,格式必须是.jpg .png,大小小于5MB.(文件夹递归)
  5. 每次只处理一个文件(可以绕过20个的数量限制)
  6. 处理返回数据拿到远程优化图片地址
  7. 取回图片更新本地图片
  8. 纯node实现不依赖任何其他代码片段

    打码实现:

    仅适用Node提供的模块:
    const fs = require("fs");
    const { Console } = require("console");
    const path = require("path");
    const https = require("https");
    const URL = require("url").URL;
    通用浏览器标识,防止同一标识被服务器拦截:
    const USER_AGENT = [
      "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
      "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv,2.0.1) Gecko/20100101 Firefox/4.0.1",
      "Mozilla/5.0 (Windows NT 6.1; rv,2.0.1) Gecko/20100101 Firefox/4.0.1",
      "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
      "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
      "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
      "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; maxthon 2.0)",
      "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)",
    ];
    定义log,支持输出日志到文件:
    // 定义log,支持输出日志到文件
    class Log {
      options = {
     flags: "a", // append模式
     encoding: "utf8", // utf8编码
      };
    
      logger = {};
    
      /**
    * 初始化打印配置
    */
      constructor() {
     this.logger = new Console({
       stdout: fs.createWriteStream("./log.tinypng.stdout.log", this.options),
       stderr: fs.createWriteStream("./log.tinypng.stderr.log", this.options),
     });
      }
    
      /**
    * log级别
    * @param {*} message 输出信息
    */
      log(message) {
     if (message) {
       this.logger.log(message);
       console.log(message);
     }
      }
    
      /**
    * error级别
    * @param {*} message 输出err信息
    */
      error(message) {
     if (message) {
       this.logger.error(message);
       console.error(message);
     }
      }
    }
    
    // 实例化Log对象
    const Tlog = new Log();
    定义TinyPng对象:
    class TinyPng {
      // 配置信息: 后缀格式和最大文件大小受接收限制不允许调整
      config = {
     files: [],
     entryFolder: "./",
     deepLoop: false,
     extension: [".jpg", ".png"],
     max: 5200000, // 5MB == 5242848.754299136
     min: 100000, // 100KB
      };
    
      // 成功处理计数
      successCount = 0;
      // 失败处理计数
      failCount = 0;
    
      /**
    * TinyPng 构造器
    * @param {*} entry 入口文件
    * @param {*} deep 是否递归
    */
      constructor(entry, deep) {
     console.log(USER_AGENT[Math.floor(Math.random() * 10)]);
     if (entry != undefined) {
       this.config.entryFolder = entry;
     }
     if (deep != undefined) {
       this.config.deepLoop = deep;
     }
     // 过滤传入入口目录中符合调整的待处理文件
     this.fileFilter(this.config.entryFolder);
     Tlog.log(`本次执行脚本的配置:`);
     Object.keys(this.config).forEach((key) => {
       if (key !== "files") {
         Tlog.log(`配置${key}:${this.config[key]}`);
       }
     });
     Tlog.log(`等待处理文件的数量:${this.config.files.length}`);
      }
    
      /**
    * 执行压缩
    */
      compress() {
     Tlog.log("启动图像压缩,请稍等...");
     let asyncAll = [];
     if (this.config.files.length > 0) {
       this.config.files.forEach((img) => {
         asyncAll.push(this.fileUpload(img));
       });
       Promise.all(asyncAll)
         .then(() => {
           Tlog.log(
             `处理完毕: 成功: ${this.successCount}张, 成功率${
               this.successCount / this.config.files.length
             }`
           );
         })
         .catch((error) => {
           Tlog.error(error);
         });
     }
      }
    
      /**
    * 过滤待处理文件夹,得到待处理文件列表
    * @param {*} folder 待处理文件夹
    * @param {*} files 待处理文件列表
    */
      fileFilter(folder) {
     // 读取文件夹
     fs.readdirSync(folder).forEach((file) => {
       let fullFilePath = path.join(folder, file);
       // 读取文件信息
       let fileStat = fs.statSync(fullFilePath);
       // 过滤文件安全性/大小限制/后缀名
       if (
         fileStat.size <= this.config.max &&
         fileStat.size >= this.config.min &&
         fileStat.isFile() &&
         this.config.extension.includes(path.extname(file))
       ) {
         this.config.files.push(fullFilePath);
       }
       // 是都要深度递归处理文件夹
       else if (this.config.deepLoop && fileStat.isDirectory()) {
         this.fileFilter(fullFilePath);
       }
     });
      }
    
      /**
    * TinyPng 远程压缩 HTTPS 请求的配置生成方法
    */
      getAjaxOptions() {
     return {
       method: "POST",
       hostname: "tinypng.com",
       path: "/web/shrink",
       headers: {
         rejectUnauthorized: false,
         "X-Forwarded-For": Array(4)
           .fill(1)
           .map(() => parseInt(Math.random() * 254 + 1))
           .join("."),
         "Postman-Token": Date.now(),
         "Cache-Control": "no-cache",
         "Content-Type": "application/x-www-form-urlencoded",
         "User-Agent": USER_AGENT[Math.floor(Math.random() * 10)],
       },
     };
      }
    
      /**
    * TinyPng 远程压缩 HTTPS 请求
    * @param {string} img 待处理的文件
    * @success {
    *              "input": { "size": 887, "type": "image/png" },
    *              "output": { "size": 785, "type": "image/png", "width": 81, "height": 81, "ratio": 0.885, "url": "https://tinypng.com/web/output/7aztz90nq5p9545zch8gjzqg5ubdatd6" }
    *           }
    * @error  {"error": "Bad request", "message" : "Request is invalid"}
    */
      fileUpload(imgPath) {
     return new Promise((resolve) => {
       let req = https.request(this.getAjaxOptions(), (res) => {
         res.on("data", async (buf) => {
           let obj = JSON.parse(buf.toString());
           if (obj.error) {
             Tlog.log(`压缩失败!\n 当前文件:${imgPath} \n ${obj.message}`);
           } else {
             resolve(await this.fileUpdate(imgPath, obj));
           }
         });
       });
       req.write(fs.readFileSync(imgPath), "binary");
       req.on("error", (e) => {
         Tlog.log(`请求错误! \n 当前文件:${imgPath} \n, ${e}`);
       });
       req.end();
     }).catch((error) => {
       Tlog.log(error);
     });
      }
    
      // 该方法被循环调用,请求图片数据
      fileUpdate(entryImgPath, obj) {
     return new Promise((resolve) => {
       let options = new URL(obj.output.url);
       let req = https.request(options, (res) => {
         let body = "";
         res.setEncoding("binary");
         res.on("data", (data) => (body += data));
         res.on("end", () => {
           fs.writeFile(entryImgPath, body, "binary", (err) => {
             if (err) {
               Tlog.log(err);
             } else {
               this.successCount++;
               let message = `压缩成功 : 优化比例: ${(
                 (1 - obj.output.ratio) *
                 100
               ).toFixed(2)}% ,原始大小: ${(obj.input.size / 1024).toFixed(
                 2
               )}KB ,压缩大小: ${(obj.output.size / 1024).toFixed(
                 2
               )}KB ,文件:${entryImgPath}`;
               Tlog.log(message);
               resolve(message);
             }
           });
         });
       });
       req.on("error", (e) => {
         Tlog.log(e);
       });
       req.end();
     }).catch((error) => {
       Tlog.log(error);
     });
      }
    }
    
    module.exports = TinyPng;
    
    入口脚本:
    /**
     * 因网络原因和第三方接口防刷等技术限制导致部分图像处理失败
     */
    const TinyPng = require("./tinypng.compress.img");
    
    function getEntryPath() {
      let i = process.argv.findIndex((i) => i === "-p");
      if (process.argv[i + 1]) {
     return process.argv[i + 1];
      }
    }
    new TinyPng(getEntryPath(), true).compress();
    执行演示:

    image.png

    日志记录:

    image.png

    程序员还是要将重复的工作简单化,几年前就靠这份脚本将150多M的前端项目压到了20~30兆,你会想着说怎么还能有这样的项目,你们项目很大么?说实话就是不规范导致的,多年积累的文件你要一张张去处理你觉得靠谱么,你刚压缩完其他同事又提交了一堆大图片怎么办,那么最好将脚本改一下再加入到编译时的插件中,完美!


欢迎关注我的公众号“前端小鑫同学”,原创技术文章第一时间推送。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK