├── .prettierrc ├── tests ├── imagemin.png ├── original.png ├── tinypng.png └── imagemin_webp.webp ├── .vscode └── settings.json ├── types └── lib.d.ts ├── src ├── config.ts ├── compress │ ├── skip.ts │ ├── tinypng │ │ ├── index.ts │ │ └── tinypng.ts │ ├── imagemin.ts │ ├── image2webp.ts │ └── tinypngweb.ts ├── interface.ts ├── utils.ts └── index.ts ├── tsconfig.json ├── .github └── workflows │ └── nodejs-npm-publish.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 150 5 | } 6 | -------------------------------------------------------------------------------- /tests/imagemin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juzisang/picgo-plugin-compress/HEAD/tests/imagemin.png -------------------------------------------------------------------------------- /tests/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juzisang/picgo-plugin-compress/HEAD/tests/original.png -------------------------------------------------------------------------------- /tests/tinypng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juzisang/picgo-plugin-compress/HEAD/tests/tinypng.png -------------------------------------------------------------------------------- /tests/imagemin_webp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juzisang/picgo-plugin-compress/HEAD/tests/imagemin_webp.webp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.enabled": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } -------------------------------------------------------------------------------- /types/lib.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'imagemin-upng' { 2 | export default function upng(): any 3 | } 4 | 5 | declare module 'imagemin-webp' { 6 | export default imageminWebp 7 | } 8 | 9 | declare module 'imagemin-gif2webp' { 10 | export default imageminGif2webp 11 | } 12 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const TINYPNG_UPLOAD_URL = 'https://api.tinify.com/shrink' 2 | 3 | export const TINYPNG_WEBUPLOAD_URL = 'https://tinify.cn/web/shrink' 4 | 5 | export enum CompressType { 6 | tinypng = 'tinypng', 7 | imagemin = 'imagemin', 8 | image2webp = 'image2webp', 9 | } 10 | -------------------------------------------------------------------------------- /src/compress/skip.ts: -------------------------------------------------------------------------------- 1 | import PicGo from 'picgo' 2 | import { getImageBuffer, getImageInfo } from '../utils' 3 | import { CommonParams, ImageInfo } from '../interface' 4 | 5 | export function SkipCompress(ctx: PicGo, { imageUrl }: CommonParams): Promise { 6 | return getImageBuffer(ctx, imageUrl).then((buffer) => getImageInfo(imageUrl, buffer)) 7 | } 8 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ImageInfo { 2 | fileName: string 3 | extname: string 4 | buffer: Buffer 5 | width: number 6 | height: number 7 | } 8 | 9 | export interface CommonParams { 10 | imageUrl: string 11 | } 12 | 13 | export interface IConfig { 14 | compress: string 15 | key: string 16 | tinypngKey: string 17 | nameType: string 18 | } 19 | -------------------------------------------------------------------------------- /src/compress/tinypng/index.ts: -------------------------------------------------------------------------------- 1 | import PicGo from 'picgo' 2 | import { getImageInfo } from '../../utils' 3 | import { CommonParams, ImageInfo } from '../../interface' 4 | import Tinypng from './tinypng' 5 | 6 | export interface ITinypngOptions { 7 | key: string 8 | } 9 | 10 | export function TinypngKeyCompress(ctx: PicGo, { imageUrl, key }: CommonParams & ITinypngOptions): Promise { 11 | return Tinypng.init({ ctx, keys: key!.split(',') }) 12 | .then(() => Tinypng.upload(imageUrl)) 13 | .then((buffer) => { 14 | ctx.log.info('Tinypng 上传成功') 15 | return getImageInfo(imageUrl, buffer) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "allowJs": true, 9 | "declaration": false, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": false, 14 | "allowSyntheticDefaultImports": true, 15 | "baseUrl": ".", 16 | "lib": [ 17 | "es2017", 18 | "es2015", 19 | "es6" 20 | ] 21 | }, 22 | "include": [ 23 | "./src/**/*", 24 | "./types/*" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/compress/imagemin.ts: -------------------------------------------------------------------------------- 1 | import imagemin from 'imagemin' 2 | import mozjpeg from 'imagemin-mozjpeg' 3 | import upng from 'imagemin-upng' 4 | import PicGo from 'picgo' 5 | import { CommonParams, ImageInfo } from '../interface' 6 | import { getImageBuffer, getImageInfo } from '../utils' 7 | 8 | export function ImageminCompress(ctx: PicGo, { imageUrl }: CommonParams): Promise { 9 | ctx.log.info('imagemin 压缩开始') 10 | return getImageBuffer(ctx, imageUrl) 11 | .then((buffer) => imagemin.buffer(buffer, { plugins: [mozjpeg({ quality: 75, progressive: true }), upng()] })) 12 | .then((buffer) => { 13 | ctx.log.info('imagemin 压缩完成') 14 | return getImageInfo(imageUrl, buffer) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/compress/image2webp.ts: -------------------------------------------------------------------------------- 1 | import imagemin from 'imagemin' 2 | import imageminWebp from 'imagemin-webp' 3 | import { CommonParams, ImageInfo } from '../interface' 4 | import { getImageBuffer, getImageInfo } from '../utils' 5 | import PicGo from 'picgo' 6 | 7 | export function Image2WebPCompress(ctx: PicGo, { imageUrl }: CommonParams): Promise { 8 | ctx.log.info('Image2WebP 压缩开始') 9 | return getImageBuffer(ctx, imageUrl) 10 | .then((buffer) => { 11 | ctx.log.info('转换图片为WebP') 12 | return imagemin.buffer(buffer, { plugins: [imageminWebp({ quality: 75 })] }) 13 | }) 14 | .then((buffer) => { 15 | ctx.log.info('Image2WebP 压缩成功') 16 | const info = getImageInfo(imageUrl, buffer) 17 | const extname = '.webp' 18 | const fileName = info.fileName.replace(info.extname, extname) 19 | return { 20 | ...info, 21 | fileName, 22 | extname, 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: NPMPublish 5 | 6 | on: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | registry-url: 'https://registry.npmjs.org' 24 | - run: npm install 25 | - run: npm run build 26 | - name: NPM Publish 27 | uses: JS-DevTools/npm-publish@v1 28 | with: 29 | token: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 橘子 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/compress/tinypngweb.ts: -------------------------------------------------------------------------------- 1 | import PicGo from 'picgo' 2 | import { CommonParams, ImageInfo } from '../interface' 3 | import { TINYPNG_WEBUPLOAD_URL } from '../config' 4 | import { Response } from 'request' 5 | import { getImageBuffer, getImageInfo } from '../utils' 6 | 7 | function getHeaders() { 8 | const v = 59 + Math.round(Math.random() * 10) 9 | const v2 = Math.round(Math.random() * 100) 10 | return { 11 | origin: TINYPNG_WEBUPLOAD_URL, 12 | referer: TINYPNG_WEBUPLOAD_URL, 13 | 'content-type': 'application/x-www-form-urlencoded', 14 | 'user-agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v}.0.4044.${v2} Safari/537.36`, 15 | } 16 | } 17 | 18 | export function TinypngCompress(ctx: PicGo, { imageUrl }: CommonParams): Promise { 19 | return getImageBuffer(ctx, imageUrl).then((buffer) => { 20 | ctx.log.info('TinypngWeb 压缩开始') 21 | const req = ctx.Request.request({ url: TINYPNG_WEBUPLOAD_URL, method: 'POST', headers: getHeaders(), resolveWithFullResponse: true }) 22 | req.end(buffer) 23 | return req 24 | .then((data: Response) => { 25 | if (data.headers.location) { 26 | ctx.log.info('TinypngWeb 压缩成功:' + data.headers.location) 27 | ctx.log.info('下载 Tinypng 图片') 28 | return getImageBuffer(ctx, data.headers.location) 29 | } 30 | throw new Error('TinypngWeb 上传失败') 31 | }) 32 | .then((buffer) => { 33 | return getImageInfo(imageUrl, buffer) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import PicGo from 'picgo' 3 | import { Response } from 'request' 4 | import { imageSize } from 'image-size' 5 | import { extname, basename } from 'path' 6 | import { ImageInfo } from './interface' 7 | 8 | export function isNetworkUrl(url: string) { 9 | return url.startsWith('http://') || url.startsWith('https://') 10 | } 11 | 12 | export async function fetchImage(ctx: PicGo, url: string): Promise { 13 | return await ctx.Request.request({ method: 'GET', url, encoding: null }).on('response', (response: Response): void => { 14 | const contentType = response.headers['content-type'] 15 | if (contentType && !contentType.includes('image')) { 16 | throw new Error(`${url} 不是图片`) 17 | } 18 | }) 19 | } 20 | 21 | export function getImageBuffer(ctx: PicGo, imageUrl: string): Promise { 22 | if (isNetworkUrl(imageUrl)) { 23 | ctx.log.info('获取网络图片') 24 | return fetchImage(ctx, imageUrl) 25 | } else { 26 | ctx.log.info('获取本地图片') 27 | return fs.readFile(imageUrl) 28 | } 29 | } 30 | 31 | export function getImageInfo(imageUrl: string, buffer: Buffer): ImageInfo { 32 | const { width, height } = imageSize(buffer) 33 | return { 34 | buffer, 35 | width: width as number, 36 | height: height as number, 37 | fileName: basename(imageUrl), 38 | extname: extname(imageUrl), 39 | } 40 | } 41 | 42 | export function getUrlInfo(imageUrl: string) { 43 | return { 44 | fileName: basename(imageUrl), 45 | extname: extname(imageUrl), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picgo-plugin-compress", 3 | "version": "1.4.0", 4 | "description": "Image compression plugin for PicGo", 5 | "main": "dist/index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "repository": "git@github.com:JuZiSang/picgo-plugin-compress.git", 10 | "author": "juzisang ", 11 | "license": "MIT", 12 | "files": [ 13 | "/dist" 14 | ], 15 | "scripts": { 16 | "build": "tsc -p .", 17 | "dev": "tsc -w -p .", 18 | "patch": "npm version patch && git push origin master && git push origin --tags", 19 | "minor": "npm version minor && git push origin master && git push origin --tags", 20 | "major": "npm version major && git push origin master && git push origin --tags" 21 | }, 22 | "keywords": [ 23 | "picgo", 24 | "picgo-plugin", 25 | "picgo-gui-plugin" 26 | ], 27 | "devDependencies": { 28 | "@types/crypto-js": "^3.1.44", 29 | "@types/fs-extra": "^8.1.0", 30 | "@types/imagemin": "^7.0.0", 31 | "@types/imagemin-mozjpeg": "^8.0.0", 32 | "@types/node": "^12.12.36", 33 | "@types/request": "^2.48.4", 34 | "@types/request-promise-native": "^1.0.17", 35 | "@typescript-eslint/parser": "^2.28.0", 36 | "picgo": "1.4.8", 37 | "typescript": "^3.8.3" 38 | }, 39 | "dependencies": { 40 | "crypto-js": "^4.0.0", 41 | "fs-extra": "^9.0.0", 42 | "image-size": "^0.8.3", 43 | "imagemin": "^7.0.1", 44 | "imagemin-mozjpeg": "^8.0.0", 45 | "imagemin-upng": "^2.0.2", 46 | "imagemin-webp": "^6.0.0", 47 | "request": "^2.88.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .idea 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | ./dist 108 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import PicGo from 'picgo' 2 | import { PluginConfig } from 'picgo/dist/src/utils/interfaces' 3 | import { TinypngCompress } from './compress/tinypngweb' 4 | import { TinypngKeyCompress } from './compress/tinypng/index' 5 | import { ImageminCompress } from './compress/imagemin' 6 | import { Image2WebPCompress } from './compress/image2webp' 7 | import { CompressType } from './config' 8 | import { getUrlInfo } from './utils' 9 | import { IConfig } from './interface' 10 | import { SkipCompress } from './compress/skip' 11 | 12 | const ALLOW_EXTNAME = ['.png', '.jpg', '.webp', '.jpeg'] 13 | 14 | function handle(ctx: PicGo) { 15 | const config: IConfig = ctx.getConfig('transformer.compress') || ctx.getConfig('picgo-plugin-compress') 16 | const compress = config?.compress 17 | const key = config?.key || config?.tinypngKey 18 | 19 | ctx.log.info('压缩:' + compress) 20 | 21 | const tasks = ctx.input.map((imageUrl) => { 22 | ctx.log.info('图片地址:' + imageUrl) 23 | const info = getUrlInfo(imageUrl) 24 | ctx.log.info('图片信息:' + JSON.stringify(info)) 25 | if (ALLOW_EXTNAME.includes(info.extname.toLowerCase())) { 26 | switch (compress) { 27 | case CompressType.tinypng: 28 | return key ? TinypngKeyCompress(ctx, { imageUrl, key }) : TinypngCompress(ctx, { imageUrl }) 29 | case CompressType.imagemin: 30 | return ImageminCompress(ctx, { imageUrl }) 31 | case CompressType.image2webp: 32 | return Image2WebPCompress(ctx, { imageUrl }) 33 | default: 34 | return key ? TinypngKeyCompress(ctx, { imageUrl, key }) : TinypngCompress(ctx, { imageUrl }) 35 | } 36 | } 37 | ctx.log.warn('不支持的格式,跳过压缩') 38 | return SkipCompress(ctx, { imageUrl }) 39 | }) 40 | 41 | return Promise.all(tasks).then((output) => { 42 | ctx.log.info( 43 | '图片信息:' + JSON.stringify(output.map((item) => ({ fileName: item.fileName, extname: item.extname, height: item.height, width: item.width }))) 44 | ) 45 | ctx.output = output 46 | return ctx 47 | }) 48 | } 49 | 50 | module.exports = function (ctx: PicGo): any { 51 | return { 52 | transformer: 'compress', 53 | register() { 54 | ctx.helper.transformer.register('compress', { handle }) 55 | }, 56 | config(ctx: PicGo): PluginConfig[] { 57 | let config = ctx.getConfig('transformer.compress') || ctx.getConfig('picgo-plugin-compress') 58 | if (!config) { 59 | config = {} 60 | } 61 | return [ 62 | { 63 | name: 'compress', 64 | type: 'list', 65 | message: '选择压缩库', 66 | choices: Object.keys(CompressType), 67 | default: config.compress || CompressType.tinypng, 68 | required: true, 69 | }, 70 | { 71 | name: 'key', 72 | type: 'input', 73 | message: '申请key,不填默认使用WebApi,逗号隔开,可使用多个Key叠加使用次数', 74 | default: config.key || config.tinypngKey || null, 75 | required: false, 76 | }, 77 | ] 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## picgo-plugin-compress 2 | 3 | [![build](https://img.shields.io/github/workflow/status/juzisang/picgo-plugin-compress/NPMPublish/master?color=brightgreen)](https://github.com/JuZiSang/picgo-plugin-compress/actions) 4 | [![dm](https://img.shields.io/npm/dm/picgo-plugin-compress?color=brightgreen)](https://npmcharts.com/compare/picgo-plugin-compress?minimal=true) 5 | [![v](https://img.shields.io/npm/v/picgo-plugin-compress?color=brightgreen)](https://www.npmjs.com/package/picgo-plugin-compress) 6 | [![mit](https://img.shields.io/badge/license-mit-brightgreen.svg)](https://github.com/JuZiSang/picgo-plugin-compress/blob/master/LICENSE) 7 | 8 | 用于 [PicGo](https://github.com/Molunerfinn/PicGo) 的图片压缩插件,支持 [TinyPng](https://tinypng.com/) [ImageMin](https://github.com/imagemin/imagemin) 9 | 10 | ## 安装失败参考 11 | 12 | - https://github.com/JuZiSang/picgo-plugin-compress/issues/2 13 | 14 | ## 安装 15 | 16 | ### [PicGo-Core](https://github.com/PicGo/PicGo-Core) 安装 17 | 18 | - 安装 `picgo add compress` 19 | 20 | - 选择使用 `picgo use transformer` 21 | 22 | - 参数配置 `picgo config plugin compress` 23 | 24 | compress 选择压缩工具 25 | 默认选项 26 | 27 | - [tinypng](https://tinypng.com/) 无损压缩,需要上传到 tinypng 28 | - [imagemin](https://github.com/imagemin/imagemin) 压缩过程不需要经过网络,但是图片会有损耗 29 | - image2webp 本地有损压缩,支持 GIF 格式有损压缩 30 | 注意:有些图床(比如 sm.ms)不支持 webp 图片格式,会上传失败 31 | 32 | key 可选 33 | 34 | - 在 [developers](https://tinypng.com/developers) 中申请 35 | - 逗号`,`隔开,可使用多个 Key 叠加使用次数 36 | 37 | ### [PicGo-Gui](https://github.com/Molunerfinn/PicGo) 在线安装 38 | 39 | - 打开详细窗口 > 插件设置 > 搜索 `compress` 即可安装,配置同上 40 | - 离线安装参考[这里](https://picgo.github.io/PicGo-Core-Doc/zh/dev-guide/deploy.html#gui%E6%8F%92%E4%BB%B6) 41 | 42 | ## 压缩效果对比 43 | 44 | | 类型 | tinypng | imagemin | image2webp | 45 | | ------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | 46 | | 原大小 | 1.5 MB | 1.5 MB | 1.5 MB | 47 | | 压缩后 | 315 KB | 411 KB | 216 KB | 48 | | 效果图 | ![](https://raw.githubusercontent.com/JuZiSang/picgo-plugin-compress/master/tests/tinypng.png) | ![](https://raw.githubusercontent.com/JuZiSang/picgo-plugin-compress/master/tests/imagemin.png) | ![](https://raw.githubusercontent.com/JuZiSang/picgo-plugin-compress/master/tests/imagemin_webp.webp) | 49 | -------------------------------------------------------------------------------- /src/compress/tinypng/tinypng.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs-extra' 3 | import { getImageBuffer, isNetworkUrl } from '../../utils' 4 | import { TINYPNG_UPLOAD_URL } from '../../config' 5 | import Base64 from 'crypto-js/enc-base64' 6 | import Utf8 from 'crypto-js/enc-utf8' 7 | import PicGo from 'picgo' 8 | import { Response } from 'request' 9 | 10 | interface TinyPngOptions { 11 | keys: string[] 12 | ctx: PicGo 13 | } 14 | 15 | interface TinyCacheConfig { 16 | [key: string]: { 17 | key: string 18 | num: number 19 | } 20 | } 21 | 22 | class TinyPng { 23 | private cacheConfigPath = path.join(__dirname, 'config.json') 24 | private options!: TinyPngOptions 25 | private PicGo!: PicGo 26 | 27 | async init(options: TinyPngOptions) { 28 | this.PicGo = options.ctx 29 | this.options = options 30 | await this.readOrWriteConfig(this.options.keys) 31 | this.PicGo.log.info('TinyPng初始化') 32 | } 33 | 34 | async upload(url: string) { 35 | this.PicGo.log.info('TinyPng开始上传') 36 | if (isNetworkUrl(url)) { 37 | return this.uploadImage({ url, originalUrl: url, key: await this.getKey() }) 38 | } else { 39 | return this.uploadImage({ 40 | key: await this.getKey(), 41 | originalUrl: url, 42 | buffer: await getImageBuffer(this.PicGo, url), 43 | }) 44 | } 45 | } 46 | 47 | private async getKey() { 48 | const config = await this.readOrWriteConfig() 49 | const innerKeys = Object.keys(config).filter((key) => config[key].num !== -1) 50 | if (innerKeys.length <= 0) { 51 | throw new Error('使用次数用完') 52 | } 53 | return innerKeys[0] 54 | } 55 | 56 | private uploadImage(options: { key: string; originalUrl: string; url?: string; buffer?: Buffer }): Promise { 57 | this.PicGo.log.info('使用TinypngKey:' + options.key) 58 | 59 | const bearer = Base64.stringify(Utf8.parse(`api:${options.key}`)) 60 | 61 | const fetchOptions = { 62 | method: 'POST', 63 | url: TINYPNG_UPLOAD_URL, 64 | json: true, 65 | resolveWithFullResponse: true, 66 | headers: { 67 | Host: 'api.tinify.com', 68 | Authorization: `Basic ${bearer}`, 69 | }, 70 | } 71 | 72 | if (options.url) { 73 | this.PicGo.log.info('TinyPng 上传网络图片') 74 | Object.assign(fetchOptions.headers, { 75 | 'Content-Type': 'application/json', 76 | }) 77 | Object.assign(fetchOptions, { 78 | body: { 79 | source: { 80 | url: options.url, 81 | }, 82 | }, 83 | }) 84 | } 85 | 86 | const req = this.PicGo.Request.request(fetchOptions) 87 | 88 | if (options.buffer) { 89 | this.PicGo.log.info('TinyPng 上传本地图片') 90 | req.end(options.buffer) 91 | } 92 | 93 | return req.then((response: Response) => { 94 | this.setConfig(options.key, parseInt(response.headers['compression-count'] as any)) 95 | if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { 96 | console.log(response.statusCode) 97 | console.log(response.headers.location) 98 | return getImageBuffer(this.PicGo, response.headers.location as any) 99 | } 100 | if (response.statusCode === 429) { 101 | this.setConfig(options.key, -1) 102 | return this.upload(options.originalUrl) 103 | } 104 | throw new Error('未知错误') 105 | }) 106 | } 107 | 108 | private async setConfig(key: string, num: number) { 109 | const config = await this.readOrWriteConfig() 110 | config[key] = { 111 | key, 112 | num, 113 | } 114 | await fs.writeJSON(this.cacheConfigPath, config) 115 | } 116 | 117 | private async readOrWriteConfig(keys?: string[]): Promise { 118 | const config: TinyCacheConfig = {} 119 | if (await fs.pathExists(this.cacheConfigPath)) { 120 | Object.assign(config, await fs.readJSON(this.cacheConfigPath)) 121 | } else { 122 | await fs.writeJSON(this.cacheConfigPath, {}) 123 | } 124 | if (keys) { 125 | await fs.writeJSON( 126 | this.cacheConfigPath, 127 | keys.reduce((res, key) => { 128 | if (!res[key]) { 129 | res[key] = { 130 | key, 131 | num: 0, 132 | } 133 | } 134 | return res 135 | }, config) 136 | ) 137 | } 138 | return config 139 | } 140 | } 141 | 142 | export default new TinyPng() 143 | --------------------------------------------------------------------------------