原來 TinyPNG 可以這樣玩!


前端er, 又稱為切圖仔,平時經常需要用 PSD 導出 PNG 或 JPG,但是導出來的的圖片一般比較大,往往需要用一些其他工具壓縮后再發布到生產環境。

以前常用的做法是,使用 image-webpack-loader ,在 webpack 打包項目時自動壓縮圖片。但是這 loader 是基於 imagemin 的,他的壓縮失真比較嚴重,而且壓縮率也不是很高。經常出現一種情況就是,在本地開發的時候,圖片明明很清晰的,但是一旦發布到生產環境 ,因為經過了 img-webapck-loader 壓縮,導致圖片嚴重失真。也因為這個原因,美術小姐姐在驗收項目的時候,常常會把我叫過去,說:哎呀,我的 PSD 明明是很清晰的呀,為什么你放到網站上去就那么模糊了呢。我說因為壓縮過了呀。然后她說這個壓縮工具不給力呀,就推薦了一個在線壓縮工具給我: TinyPNG在線地址

初體驗 tinypng 真是把我驚呆了, 一般的 PNG 壓縮率竟然可以高達 50% - 70% ,並且肉眼看不出來任何的失真。

TinyPNG 的原理是將 PNG24 位真彩色圖片壓縮成 PNG8 位索引圖片,從而做到基本不損失畫質和觀感。至於具體算法怎么實現的也沒有深入研究守,有興趣的可以自行查閱相關資料。

tinypg 除了可以通過網頁端上傳圖片,也提供了 api 的方式 ,所以可以使用 node 或其他腳本語言進行上傳的,可以參考這里:
https://tinify.cn/developers/reference

然而,這么好用的壓縮工具有一個很明顯缺陷的,就是每天只能正常壓縮 20 張圖片。超過 20 張之后,就會經常出來 Too many files uploaded at once。如下紅色的部分:

當然,這是針對免費版用戶而言的。如果想要避免這個限制,你需要花 25 美金買一年的會員。當然,我們程序員大多數都是很吝嗇的,就連到外面吃飯要不要加個鹵蛋都要糾結半個小時的,更別說給 25 美金他了。

但是,程序員的特點是,都愛折騰!所以我決定要嘗試着繞過這 20 張圖片的上傳限制。一般來說,這種免登錄就可試用的系統,都是通過用戶IP來限制用戶的操作次數的,所以就決定從修改 IP 的方式來繞開這個限制。

最簡單的作法是使用動態的代理 IP, 比如這里 https://ip.ihuan.me/ 就有一些免費代理IP,但是,這些 IP 都是國外的,很慢的,我們的目的是要上傳並壓縮圖片,使用這么慢的代理顯然是不可行的。

所以就只能尋找其他的辦法。
就在陷入迷茫的時候,突然想到了一個事實 :目前的 web 架構大多數都是通過 nginx 作為反向代理的,如下:

從上圖可以看出,客戶端並不是直接請求應用服務的,而是通過統一接入層(往往是 nginx) 來轉發請求的。那么問題來,應用服務是如何獲取到客戶端的 IP 的呢?

玩過 nginx 的同學應該都知道 , 這個通用的解決方案就是 X-Forwarded-For 請求頭。

簡單的來說,就是所有的反向代理都實現一個統一的約定,在轉發請求給下游服務之前,把請求代理的 IP 地址寫入到 X-Forwarded-For 頭中,形成了一個 IP 地址列:

X-Forwarded-For: client, proxy1, proxy2

這個方案雖然不是正式的 HTTP 協議,但已經成為了一個事實標准,基本上所有的反向代理服務都實現了這個功能,以確保下游的服務可以感知到經過的反向代理,並從中獲取到用戶的 IP 地址。

好了,既然應用服務是通過 X-Forwarded-For 頭部來獲取客戶端 IP 的,那么我們能不能在客戶端偽造這個頭部,每次上傳圖片的時候都設置一個隨機的 IP 呢?所以就決定嘗試一下。結果發現,該方法確實可行!竟然可以欺騙 tinypng 的服務器,從而繞開了上傳次數的限制。

完整代碼如下:

const fs = require('fs');
const path = require('path');
const https = require('https');
const { URL } = require('url');


const cwd = process.cwd();

const root = cwd;
  exts = ['.jpg', '.png'],
  max = 5200000; // 5MB == 5242848.754299136


const options = {
  method: 'POST',
  hostname: 'tinypng.com',
  path: '/web/shrink',
  headers: {
    rejectUnauthorized: false,
    'Postman-Token': Date.now(),
    'Cache-Control': 'no-cache',
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent':
      'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
  }
};

fileList(root);


// 生成隨機IP, 賦值給 X-Forwarded-For
function getRandomIP() {
  return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join('.')
}

// 獲取文件列表
function fileList(folder) {
  fs.readdir(folder, (err, files) => {
    if (err) console.error(err);
    files.forEach(file => {
      fileFilter(path.join(folder, file));
    });
  });
}

// 過濾文件格式,返回所有jpg,png圖片
function fileFilter(file) {
  fs.stat(file, (err, stats) => {
    if (err) return console.error(err);
    if (
      // 必須是文件,小於5MB,后綴 jpg||png
      stats.size <= max &&
      stats.isFile() &&
      exts.includes(path.extname(file))
    ) {
      
      // 通過 X-Forwarded-For 頭部偽造客戶端IP
      options.headers['X-Forwarded-For'] = getRandomIP();

      fileUpload(file); // console.log('可以壓縮:' + file);
    }
    // if (stats.isDirectory()) fileList(file + '/');
  });
}

// 異步API,壓縮圖片
// {"error":"Bad request","message":"Request is invalid"}
// {"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" }}
function fileUpload(img) {
  var req = https.request(options, function(res) {
    res.on('data', buf => {
      let obj = JSON.parse(buf.toString());
      if (obj.error) {
        console.log(`[${img}]:壓縮失敗!報錯:${obj.message}`);
      } else {
        fileUpdate(img, obj);
      }
    });
  });

  req.write(fs.readFileSync(img), 'binary');
  req.on('error', e => {
    console.error(e);
  });
  req.end();
}
// 該方法被循環調用,請求圖片數據
function fileUpdate(imgpath, obj) {
  const outputDir = path.join(cwd , 'output');
  imgpath = path.join(cwd , 'output', imgpath.replace(cwd, ''));

  if(!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir);
  }

  let options = new URL(obj.output.url);
  let req = https.request(options, res => {
    let body = '';
    res.setEncoding('binary');
    res.on('data', function(data) {
      body += data;
    });

    res.on('end', function() {
      fs.writeFile(imgpath, body, 'binary', err => {
        if (err) return console.error(err);
        console.log(
          `[${imgpath}] \n 壓縮成功,原始大小-${obj.input.size},壓縮大小-${
            obj.output.size
          },優化比例-${obj.output.ratio}`
        );
      });
    });
  });
  req.on('error', e => {
    console.error(e);
  });
  req.end();
}

這段代碼是參考了 nodejs 全自動使用 Tinypng (免費版,無需任何配置)壓縮圖片 ,只是簡單的加上了動態的 IP 頭,從而實現了不受上傳次數限制。

核心的代碼在這里:

// 生成隨機IP, 賦值給 X-Forwarded-For
function getRandomIP() {
  return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join('.')
}

// ....
	options.headers['X-Forwarded-For']  = getRandomIP();
//....

代碼上傳到了 github ,有興趣的可以點擊下面連接查看並 star 關注 喔:

super-tinypng

同時,也發布到了 npm 上面。只需要全局安裝:


npm i super-tinypng -g

然后在你想要壓縮圖片的目錄里面運行 super-tinypng 就能自動壓縮圖片了,並且不會有數量限制!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM