├── README.md ├── index.js ├── package-lock.json └── package.json /README.md: -------------------------------------------------------------------------------- 1 | ## 初衷 2 | [tinypng](https://tinypng.com/) 网页版,其实是挺方便的。但是他有上传图片数量的限制,比如每天只能上传 20 张,如果超过这个数量,就会断断续续的出现 `Too many files uploaded at once` 错误 。所以才决定使用 Node 来开发一个绕过数量限制的 npm 包。 3 | 4 | 5 | ## 使用方法 6 | 安装: 7 | ```bash 8 | npm i super-tinypng -g # or yarn global add super-tinypng 9 | ``` 10 | 11 | 然后,在命令行进入到你想要压缩图片的目录,执行: 12 | ```bash 13 | super-tinypng 14 | ``` 15 | 16 | 如果想要处理指定输入和输出目录: 17 | ```bash 18 | super-tinypng --path /your/path/to --out /your/path/to 19 | ``` 20 | 21 | ## 说明 22 | - tinypng 默认是会对用户上传数量有限制的,使用了 `X-Forwarded-For` 头绕过该限制 23 | - ~~为了简化,不可以递归遍历文件夹~~ 24 | - ~~为了简化,不支持配置,只能压缩当前目录下的图片,并且会在当前目录下创建一个 output 目录,把压缩成功的图片放到里面~~ 25 | 26 | ## 免责声明 27 | 28 | 该仓库仅用于学习,如有商业用途,请购买官方的 pro 版:https://tinify.com/checkout/web-pro 29 | 30 | This Repo is only for study. 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 5 | * 参考: https://segmentfault.com/a/1190000015467084 6 | * 优化:通过 X-Forwarded-For 添加了动态随机伪IP,绕过 tinypng 的上传数量限制 7 | * 8 | * */ 9 | 10 | const fs = require('fs'); 11 | const path = require('path'); 12 | const https = require('https'); 13 | const { URL } = require('url'); 14 | // 获取命令行参数 15 | const args = process.argv; 16 | const helpText=`super-tinypng 图片压缩命令行工具 - 处理输入目录中的图片并将压缩后的图片保存到指定的输出目录。 17 | 18 | 用法: 19 | super-tinypng [选项] 20 | 21 | 选项: 22 | --path <路径> 指定输入目录的路径。默认值为当前目录。 23 | --out <路径> 指定输出目录的路径。默认值为输入目录下的 "output" 目录。 24 | 25 | 其他选项: 26 | --help 显示帮助信息并退出。 27 | --version 显示工具版本信息并退出。 28 | 29 | 示例用法: 30 | 1. 使用默认参数运行工具: 31 | super-tinypng 32 | 33 | 2. 指定输入目录并将结果保存在默认的输出目录: 34 | super-tinypng --path /path/to/input 35 | 36 | 3. 指定输入目录和输出目录的路径: 37 | super-tinypng --path /path/to/input --out /path/to/output 38 | 39 | 注意: 40 | - 输入目录中的文件将会被处理,处理结果将保存到输出目录中。 41 | - 如果输出目录不存在,工具将尝试创建它。`; 42 | 43 | const helpIndex = args.indexOf('--help'); 44 | 45 | if (helpIndex !== -1) { 46 | console.log(helpText); 47 | return 48 | } 49 | 50 | if (args.includes("--version")){ 51 | console.log('1.0.1') 52 | return 53 | } 54 | 55 | // 查找 "--path" 参数并获取其值 56 | const pathIndex = args.indexOf('--path'); 57 | let root = process.cwd(); 58 | 59 | if (pathIndex !== -1 && pathIndex + 1 < args.length) { 60 | root = args[pathIndex + 1]; 61 | if (!fs.existsSync(root)) { 62 | throw new Error( root + " 目录不存在") 63 | } 64 | } 65 | 66 | 67 | const exts = ['.jpg', '.png']; 68 | const max = 5200000; // 5MB == 5242848.754299136 69 | 70 | const options = { 71 | method: 'POST', 72 | hostname: 'tinypng.com', 73 | path: '/backend/opt/shrink', 74 | headers: { 75 | rejectUnauthorized: false, 76 | 'Postman-Token': Date.now(), 77 | 'Cache-Control': 'no-cache', 78 | 'Content-Type': 'application/x-www-form-urlencoded', 79 | 'User-Agent': 80 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' 81 | } 82 | }; 83 | 84 | fileList(root); 85 | 86 | // 生成随机IP, 赋值给 X-Forwarded-For 87 | function getRandomIP() { 88 | return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join('.') 89 | } 90 | 91 | // 获取文件列表 92 | function fileList(folder) { 93 | fs.readdir(folder, (err, files) => { 94 | if (err) console.error(err); 95 | files.forEach(file => { 96 | const filePath = path.join(folder, file); 97 | fs.stat(filePath, (err, stats) => { 98 | if (err) return console.error(err); 99 | if (stats.isDirectory()) { 100 | fileList(filePath); 101 | } else if ( 102 | // 必须是文件,小于5MB,后缀 jpg||png 103 | stats.size <= max && 104 | stats.isFile() && 105 | exts.includes(path.extname(file)) 106 | ) { 107 | // 通过 X-Forwarded-For 头部伪造客户端IP 108 | options.headers['X-Forwarded-For'] = getRandomIP(); 109 | fileUpload(filePath); 110 | } 111 | }); 112 | }); 113 | }); 114 | } 115 | 116 | // 异步API,压缩图片 117 | // {"error":"Bad request","message":"Request is invalid"} 118 | // {"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" }} 119 | function fileUpload(img) { 120 | var req = https.request(options, function(res) { 121 | res.on('data', buf => { 122 | let obj = JSON.parse(buf.toString()); 123 | if (obj.error) { 124 | console.log(`[${img}]:压缩失败!报错:${obj.message}`); 125 | } else { 126 | fileUpdate(img, obj); 127 | } 128 | }); 129 | }); 130 | 131 | req.write(fs.readFileSync(img), 'binary'); 132 | req.on('error', e => { 133 | console.error(e); 134 | }); 135 | req.end(); 136 | } 137 | 138 | // 该方法被循环调用,请求图片数据 139 | function fileUpdate(imgpath, obj) { 140 | const outputPathIndex = args.indexOf('--out'); 141 | let outputDir =path.join(root, 'output'); 142 | 143 | if (outputPathIndex !== -1 && outputPathIndex + 1 < args.length) { 144 | outputDir = args[outputPathIndex + 1]; 145 | } 146 | imgpath = path.join(outputDir, imgpath.replace(root, '')); 147 | const imgdir = path.dirname(imgpath); 148 | 149 | 150 | if (!fs.existsSync(imgdir)) { 151 | fs.mkdirSync(imgdir, { recursive: true }); 152 | } 153 | 154 | let options = new URL(obj.output.url); 155 | let req = https.request(options, res => { 156 | let body = ''; 157 | res.setEncoding('binary'); 158 | res.on('data', function(data) { 159 | body += data; 160 | }); 161 | 162 | res.on('end', function() { 163 | console.log("imgpath:" + imgpath) 164 | fs.writeFile(imgpath, body, 'binary', err => { 165 | if (err) return console.error(err); 166 | console.log( 167 | `[${imgpath}] \n 压缩成功,原始大小-${obj.input.size},压缩大小-${ 168 | obj.output.size 169 | },优化比例-${obj.output.ratio}` 170 | ); 171 | }); 172 | }); 173 | }); 174 | req.on('error', e => { 175 | console.error(e); 176 | }); 177 | req.end(); 178 | } 179 | 180 | 181 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super-tinypng", 3 | "version": "1.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "super-tinypng", 9 | "version": "1.0.1", 10 | "license": "ISC", 11 | "bin": { 12 | "super-tinypng": "index.js" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super-tinypng", 3 | "version": "1.0.1", 4 | "description": "Tinypng without \"Too many files uploaded at once\" limit", 5 | "main": "index.js", 6 | "bin": { 7 | "super-tinypng": "./index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/zhanyuzhang/super-tinypng.git" 15 | }, 16 | "keywords": [ 17 | "tinypng", 18 | "compress" 19 | ], 20 | "author": "chesszhang", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/zhanyuzhang/super-tinypng/issues" 24 | }, 25 | "homepage": "https://github.com/zhanyuzhang/super-tinypng#readme" 26 | } 27 | --------------------------------------------------------------------------------