├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── package.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── external └── sharp │ ├── darwin │ └── sharp.node │ ├── linux │ └── sharp.node │ └── win32 │ └── sharp.node ├── fonts ├── Arial-Unicode-MS.ttf └── watermark.png ├── logo.png ├── package.json ├── scripts └── rebuild.js ├── src ├── attr.ts ├── config.ts ├── index.ts ├── input.ts ├── output.ts ├── text2svg.ts └── util.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["@babel/preset-env"], 5 | "plugins": ["add-module-exports"] 6 | }, 7 | "production": { 8 | "presets": ["@babel/preset-env", "minify"], 9 | "plugins": ["add-module-exports"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | #test 2 | /lib 3 | /scripts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: ["plugin:@typescript-eslint/recommended"], 4 | plugins: ["@typescript-eslint"], 5 | env: { 6 | browser: true, 7 | node: true 8 | }, 9 | 10 | parserOptions: { 11 | ecmaVersion: 2019, 12 | sourceType: "module" 13 | }, 14 | rules: { 15 | "@typescript-eslint/interface-name-prefix": [ 16 | "warn", 17 | { prefixWithI: "always" } 18 | ], 19 | "prefer-const": "off" 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Context 13 | env: 14 | GITHUB_CONTEXT: ${{ toJson(github) }} 15 | run: echo "$GITHUB_CONTEXT" 16 | - uses: actions/checkout@v1 17 | with: 18 | fetch-depth: 1 19 | - name: Use Node.js 12.x 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 12.x 23 | registry-url: "https://registry.npmjs.org" 24 | - name: Install 25 | run: | 26 | SKIP_REBUILD=1 yarn install 27 | - name: Lint 28 | run: | 29 | yarn lint 30 | - name: Test 31 | run: | 32 | yarn test 33 | - name: Build 34 | run: | 35 | yarn build 36 | - name: publish 37 | run: npm publish 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional REPL history 31 | .node_repl_history 32 | 33 | # Editors 34 | .idea 35 | 36 | # Lib 37 | lib 38 | 39 | # npm package lock 40 | package-lock.json 41 | yarn.lock 42 | 43 | others 44 | .DS_Store 45 | .cache 46 | dist 47 | fixture/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | .nyc_output 7 | 8 | # Dependency directories 9 | node_modules 10 | 11 | # npm package lock 12 | package-lock.json 13 | yarn.lock 14 | 15 | # project files 16 | src 17 | test 18 | examples 19 | CHANGELOG.md 20 | .travis.yml 21 | .editorconfig 22 | .eslintignore 23 | .eslintrc 24 | .babelrc 25 | .gitignore 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.2.1](https://github.com/Dec-F/picgo-plugin-watermark/compare/v1.2.0...v1.2.1) (2021-10-11) 6 | 7 | ## [1.2.0](https://github.com/Dec-F/picgo-plugin-watermark/compare/v1.1.0...v1.2.0) (2021-08-11) 8 | 9 | ### [1.1.0](https://github.com/Dec-F/picgo-plugin-watermark/compare/v1.0.0...v1.1.0) (2021-07-15) 10 | 11 | ## [1.0.0](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.1.2...v1.0.0) (2020-02-26) 12 | 13 | ### [0.1.2](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.1.1...v0.1.2) (2020-02-26) 14 | 15 | 16 | ### Features 17 | 18 | * register beforeuploadplugins & linux sharp ([e0bf891](https://github.com/Dec-F/picgo-plugin-watermark/commit/e0bf89101bdcead495072d59c5cdc75a500092a3)) 19 | 20 | ### [0.1.1](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.1.0...v0.1.1) (2020-02-25) 21 | 22 | ## [0.1.0](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.11...v0.1.0) (2020-02-25) 23 | 24 | 25 | ### Features 26 | 27 | * build in sharp ([30d6528](https://github.com/Dec-F/picgo-plugin-watermark/commit/30d6528cc87cd2047e0167dfdad0f9cfaef37f80)) 28 | 29 | ### [0.0.11](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.10...v0.0.11) (2019-12-27) 30 | 31 | ### [0.0.10](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.8...v0.0.10) (2019-12-27) 32 | 33 | ### [0.0.8](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.7...v0.0.8) (2019-12-25) 34 | 35 | ### [0.0.4](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.3...v0.0.4) (2019-12-25) 36 | 37 | ### Bug Fixes 38 | 39 | - fix electron-rebuild running in the wrong path ([a024f7d](https://github.com/Dec-F/picgo-plugin-watermark/commit/a024f7d4644f701a562c5912ad47abb82fd21a61)) 40 | 41 | ### [0.0.3](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.2...v0.0.3) (2019-12-25) 42 | 43 | ### Features 44 | 45 | - add the minimum image size limit ([5d4eaf7](https://github.com/Dec-F/picgo-plugin-watermark/commit/5d4eaf7f4f2491e8b23b11deaf0917ee9980ca28)) 46 | 47 | ### Bug Fixes 48 | 49 | - wrong judgment ([03d9130](https://github.com/Dec-F/picgo-plugin-watermark/commit/03d913034e6c15f48e385486df8dd769ddecbd33)) 50 | 51 | ### [0.0.2](https://github.com/Dec-F/picgo-plugin-watermark/compare/v0.0.1...v0.0.2) (2019-12-24) 52 | 53 | ### Features 54 | 55 | - add logo ([139e8e4](https://github.com/Dec-F/picgo-plugin-watermark/commit/139e8e4d1e63e9299d24a1c37ea9d6405e69ece0)) 56 | 57 | ### 0.0.1 (2019-12-24) 58 | 59 | ### Features 60 | 61 | - Complete basic feature ([ceaef73](https://github.com/Dec-F/picgo-plugin-watermark/commit/ceaef7314a584360a282499be047777cc6f56171)) 62 | 63 | ### Bug Fixes 64 | 65 | - compatible with non-numeric fontSize ([423278f](https://github.com/Dec-F/picgo-plugin-watermark/commit/423278fcafb4a70a6e9726a7f45e4ebc9fcc871c)), closes [#1](https://github.com/Dec-F/picgo-plugin-watermark/issues/1) [#2](https://github.com/Dec-F/picgo-plugin-watermark/issues/2) 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dineshkumar Pandiyan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # picgo-plugin-watermark 2 | 3 | # Screenshot 4 | 5 | ![](https://gitee.com/Dec-F/ImageHosting/raw/master/img/2019/12/25/20191225174743.png) 6 | 7 | ![](https://gitee.com/Dec-F/ImageHosting/raw/master/img/2000-57139f0ecc19a932873e59a055d486d8.jpg) 8 | 9 | ![](https://gitee.com/Dec-F/ImageHosting/raw/master/img/2019/12/27/20191227170849.jpg) 10 | 11 | # Features 12 | 13 | Add watermark to picture 14 | 15 | # Installation 16 | 17 | Open [PicGo](https://github.com/Molunerfinn/PicGo)>=2.2.0 and add this plugin `picgo-plugin-watermark` 18 | 19 | ### Setting 20 | 21 | #### fontFamily 22 | 23 | 字体文件路径。E.g.`/Users/fonts/Arial-Unicode-MS.ttf`。 24 | 25 | 默认只支持英文水印,中文支持需要自行导入中文字体文件。 26 | 27 | #### text 28 | 29 | 水印文字。E.g.`hello world` 30 | 31 | #### textColor 32 | 33 | 水印文字的颜色,支持rgb和hex格式。E.g.`rgb(178,178,178)`、`#b2b2b2` 34 | 35 | #### fontSize 36 | 37 | 字体大小,默认`14` 38 | 39 | #### image 40 | 41 | 水印图片路径。E.g.`/Users/watermark.png`,优先级大于文字 42 | 43 | #### position 44 | 45 | 位置,默认`rb` 46 | 47 | ```js 48 | export enum PositionType { 49 | lt = "left-top", 50 | ct = "center-top", 51 | rt = "right-top", 52 | lm = "left-middle", 53 | cm = "center-middle", 54 | rm = "right-middle", 55 | lb = "left-bottom", 56 | cb = "center-bottom", 57 | rb = "right-bottom" 58 | } 59 | ``` 60 | 61 | #### minSize 62 | 63 | 原图最小尺寸,小于这一尺寸不添加水印。E.g.200\*200。 64 | 65 | 高度或宽度任何一个小于限制,都会触发 66 | 67 | # ChangeLog 68 | 69 | [ChangeLog](https://github.com/Dec-F/picgo-plugin-watermark/blob/master/CHANGELOG.md) 70 | -------------------------------------------------------------------------------- /external/sharp/darwin/sharp.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/external/sharp/darwin/sharp.node -------------------------------------------------------------------------------- /external/sharp/linux/sharp.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/external/sharp/linux/sharp.node -------------------------------------------------------------------------------- /external/sharp/win32/sharp.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/external/sharp/win32/sharp.node -------------------------------------------------------------------------------- /fonts/Arial-Unicode-MS.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/fonts/Arial-Unicode-MS.ttf -------------------------------------------------------------------------------- /fonts/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/fonts/watermark.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/98d620eb037732a2ed010c72a8bc8e7651d692e3/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picgo-plugin-watermark", 3 | "version": "1.2.1", 4 | "description": "PicGo's watermark plugin", 5 | "main": "./lib/index.js", 6 | "bin": {}, 7 | "scripts": { 8 | "dev": "tsc -w -p .", 9 | "build": "tsc -p .", 10 | "clean": "rimraf lib", 11 | "test": "echo 'test'", 12 | "lint": "eslint src/*.ts --fix", 13 | "release": "standard-version" 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "files": [ 19 | "lib", 20 | "logo.png" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Dec-F/picgo-plugin-watermark" 25 | }, 26 | "homepage": "https://github.com/Dec-F/picgo-plugin-watermark", 27 | "keywords": [ 28 | "npm", 29 | "node", 30 | "watermark", 31 | "picgo", 32 | "picgo-gui-plugin" 33 | ], 34 | "author": "Dec-F", 35 | "contributors": [ 36 | "panyanbin (https://www.panyanbin.com)" 37 | ], 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@types/fs-extra": "^9.0.12", 41 | "@types/node": "^12.12.21", 42 | "@types/sharp": "^0.23.1", 43 | "@typescript-eslint/eslint-plugin": "^2.7.0", 44 | "@typescript-eslint/parser": "^2.7.0", 45 | "chai": "^4.1.2", 46 | "eslint": "^5.16.0", 47 | "mocha": "^6.1.3", 48 | "picgo": "^1.3.7", 49 | "standard-version": "^8.0.1", 50 | "typescript": "^3.7.2" 51 | }, 52 | "dependencies": { 53 | "color": "^3.1.3", 54 | "dayjs": "^1.10.6", 55 | "fs-extra": "^10.0.0", 56 | "sharp": "^0.29.1", 57 | "text-to-svg": "^3.1.5" 58 | }, 59 | "engines": { 60 | "node": ">12.13.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/rebuild.js: -------------------------------------------------------------------------------- 1 | const platform = require("os").platform; 2 | const fs = require("fs-extra"); 3 | const path = require("path"); 4 | 5 | const sharpNodeUrl =`https://raw.githubusercontent.com/fhyoga/picgo-plugin-watermark/v1.0.0/external/sharp/${platform}/sharp.node`; 6 | 7 | const downloadFile = (url, dest) => { 8 | return new Promise((resolve, reject) => { 9 | var ws = fs.createWriteStream(dest); 10 | const request = /^https:\/\//.test(url) ? require('https') : require('http') 11 | request.get(url, (response) => { 12 | if (response.statusCode !== 200) { 13 | reject(response.statusMessage) 14 | return 15 | } 16 | console.log('sharp.node file is downloading...') 17 | response.pipe(ws); 18 | ws.on('finish', () => { 19 | console.log('') 20 | console.log('sharp.node download complete') 21 | ws.close(); 22 | resolve() 23 | }); 24 | response.on('data', () => { 25 | process.stdout.write('=') 26 | }) 27 | }).on('error', (err) => { 28 | // Handle errors 29 | // Delete the file async. (But we don't check the result) 30 | console.log('') 31 | console.log('sharp.node download fail') 32 | fs.unlink(dest, () => {}); 33 | reject(err.message) 34 | }) 35 | }) 36 | } 37 | 38 | // is CI? 39 | console.log(process.env.SKIP_REBUILD); 40 | if (process.env.SKIP_REBUILD) return 41 | 42 | (async () => { 43 | const targetDir = path.join( 44 | __dirname, 45 | `../../../node_modules/sharp` 46 | ) 47 | // as a npm package to install 48 | if (fs.existsSync(targetDir)) { 49 | const targetPath = path.join( 50 | __dirname, 51 | `../../../node_modules/sharp/build/Release/sharp.node` 52 | ); 53 | // Most modern macOS, Windows and Linux systems 54 | // sharp(0.28.3) package will auto fetch sharp.node file 55 | if (!fs.existsSync(targetPath)) { 56 | const sourcePath = path.join( 57 | __dirname, 58 | `../external/sharp/${platform}/sharp.node` 59 | ); 60 | if (!fs.existsSync(sourcePath)) { 61 | try { 62 | fs.mkdirpSync(path.dirname(sourcePath)) 63 | // download sharp.node 64 | await downloadFile(sharpNodeUrl, sourcePath) 65 | } catch (error) { 66 | throw new Error(error) 67 | } 68 | } 69 | try { 70 | await fs.rename(sourcePath, targetPath); 71 | } catch (error) { 72 | throw new Error("copy sharp error") 73 | } 74 | } 75 | } 76 | console.log('watermark plugin install completed') 77 | })() 78 | -------------------------------------------------------------------------------- /src/attr.ts: -------------------------------------------------------------------------------- 1 | export const fontOptions = { 2 | x: 0, 3 | y: 0, 4 | fontSize: 14, 5 | anchor: "top", 6 | attributes: { fill: "#b2b2b2" } 7 | }; 8 | // Distance to the edge 9 | export const OFFSET = { 10 | X: 20, 11 | Y: 20 12 | }; 13 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import Picgo from "picgo"; 2 | import { IPluginConfig } from "picgo/dist/src/utils/interfaces"; 3 | 4 | import {IConfig} from './util' 5 | 6 | export const config: (ctx: Picgo) => IPluginConfig[] = ctx => { 7 | let userConfig = ctx.getConfig("picgo-plugin-watermark"); 8 | if (!userConfig) { 9 | userConfig = { 10 | image: '', 11 | fontFamily: '', 12 | fontSize: '', 13 | textColor: '', 14 | minSize: '', 15 | position: '', 16 | text: '', 17 | }; 18 | } 19 | return [ 20 | { 21 | name: "fontFamily", 22 | type: "input", 23 | default: userConfig.fontFamily, 24 | required: false, 25 | message: "字体文件路径;水印中有汉字时,此项必须有", 26 | alias: "字体文件路径" 27 | }, 28 | { 29 | name: "text", 30 | type: "input", 31 | default: userConfig.text, 32 | required: false, 33 | message: "文字,默认只支持英文,中文支持需要配置字体文件路径", 34 | alias: "水印文字" 35 | }, 36 | { 37 | name: "textColor", 38 | type: "input", 39 | default: userConfig.textColor, 40 | required: false, 41 | message: "文字的颜色,支持rgb和hex格式,如rgb(178, 178, 178)或#b2b2b2", 42 | alias: "水印文字颜色" 43 | }, 44 | { 45 | name: "fontSize", 46 | type: "input", 47 | default: userConfig.fontSize, 48 | required: false, 49 | message: "默认 14px", 50 | alias: "字体大小" 51 | }, 52 | { 53 | name: "image", 54 | type: "input", 55 | default: userConfig.image, 56 | required: false, 57 | message: "水印图片的绝对路径,优先级高于文字", 58 | alias: "水印图片路径" 59 | }, 60 | { 61 | name: "position", 62 | type: "input", 63 | default: userConfig.position, 64 | required: false, 65 | message: "E.g:右上为'rt',更多信息查看Readme", 66 | alias: "水印位置" 67 | }, 68 | { 69 | name: "minSize", 70 | type: "input", 71 | default: userConfig.minSize, 72 | required: false, 73 | message: "最小尺寸限制,小于这一尺寸不添加水印。E.g.200*200", 74 | alias: "最小尺寸" 75 | } 76 | ]; 77 | }; 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Picgo from "picgo"; 2 | 3 | import { parseAndValidate, IConfig } from "./util"; 4 | import { loadFontFamily, getSvg } from "./text2svg"; 5 | import { config } from "./config"; 6 | import { inputAddWaterMarkHandle } from "./input"; 7 | 8 | const handle = async (ctx: Picgo): Promise => { 9 | const input = ctx.input; 10 | const userConfig = ctx.getConfig("picgo-plugin-watermark"); 11 | 12 | const [ 13 | errors, 14 | { 15 | text, 16 | position = "rb", 17 | parsedFontSize, 18 | image, 19 | fontFamily, 20 | minWidth, 21 | minHeight, 22 | textColor, 23 | } 24 | ] = parseAndValidate(userConfig); 25 | 26 | // Verify configuration 27 | if (errors.length) { 28 | ctx.emit("notification", { 29 | title: "watermark设置错误", 30 | body: errors.join(",") + "设置错误,请检查" 31 | }); 32 | // To prevent the next step 33 | throw new Error(); 34 | } 35 | 36 | try { 37 | loadFontFamily(fontFamily || undefined); 38 | } catch (error) { 39 | ctx.log.error("字体文件载入失败"); 40 | ctx.log.error(error); 41 | ctx.emit("notification", { 42 | title: "watermark设置错误", 43 | body: "字体文件载入失败,请检查字体文件路径" 44 | }); 45 | // To prevent the next step 46 | throw new Error(); 47 | } 48 | 49 | let waterMark = null; 50 | if (image) { 51 | waterMark = image; 52 | } else { 53 | const svgOptions: {[key: string]: any} = {} 54 | parsedFontSize && (svgOptions.fontSize = parsedFontSize) 55 | textColor && (svgOptions.fill = textColor) 56 | waterMark = Buffer.from( 57 | getSvg(text, svgOptions) 58 | ); 59 | } 60 | try { 61 | ctx.input = await inputAddWaterMarkHandle( 62 | ctx, 63 | { 64 | input, 65 | minHeight, 66 | minWidth, 67 | position, 68 | waterMark 69 | }, 70 | ctx.log 71 | ); 72 | } catch (error) { 73 | ctx.log.error(error); 74 | ctx.emit("notification", { 75 | title: "watermark转化错误", 76 | body: "可能是水印图或字体文件路径无效,请检查。" 77 | }); 78 | // To prevent the next step 79 | throw new Error(); 80 | } 81 | return ctx; 82 | }; 83 | 84 | export = (ctx: Picgo): any => { 85 | const register: () => void = () => { 86 | ctx.helper.beforeTransformPlugins.register("watermark", { handle }); 87 | }; 88 | return { 89 | register, 90 | config 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | import Picgo from "picgo"; 2 | import dayjs from 'dayjs'; 3 | import fs from 'fs-extra' 4 | 5 | import sharp from "sharp"; 6 | import path from "path"; 7 | import Logger from "picgo/dist/src/lib/Logger"; 8 | import { getCoordinateByPosition, PositionType, getImageBufferData } from "./util"; 9 | 10 | interface IInput { 11 | input: any[]; 12 | minWidth: number; 13 | minHeight: number; 14 | position: string; 15 | waterMark: string | Buffer; 16 | } 17 | 18 | export const inputAddWaterMarkHandle: ( 19 | ctx: Picgo, 20 | iinput: IInput, 21 | logger: Logger 22 | ) => Promise = async (ctx, imageInput, logger) => { 23 | const { input, minWidth, minHeight, waterMark, position } = imageInput; 24 | const sharpedWaterMark = sharp(waterMark); 25 | const waterMarkMeta = await sharpedWaterMark.metadata(); 26 | 27 | const addedWaterMarkInput = await Promise.all( 28 | input.map(async image => { 29 | let addWaterMarkImagePath = ''; 30 | 31 | // get image buffer data 32 | const imageBuffer = await getImageBufferData(ctx, image) 33 | 34 | const sharpedImage = sharp(imageBuffer); 35 | 36 | const { width, height, format } = await sharpedImage.metadata(); 37 | const coordinate = getCoordinateByPosition({ 38 | width, 39 | height, 40 | waterMark: { 41 | width: waterMarkMeta.width, 42 | height: waterMarkMeta.height, 43 | position: PositionType[position] 44 | } 45 | }); 46 | logger.info(JSON.stringify(coordinate)); 47 | logger.info(`watermark 图片文件格式:${format}`) 48 | 49 | // Picture width or length is too short, do not add watermark 50 | // Or trigger minimum size limit 51 | if ( 52 | coordinate.left <= 0 || 53 | coordinate.top <= 0 || 54 | width <= minWidth || 55 | height <= minHeight 56 | ) { 57 | addWaterMarkImagePath = image; 58 | logger.info('watermark 图片尺寸不满足,跳过水印添加') 59 | } else { 60 | // 如果图片是 picgo 生成的图片,则说明是剪切板图片 61 | // https://github.com/PicGo/PicGo-Core/blob/85ecd16253ea58910de63511ea95e6f6fb6249d6/src/utils/getClipboardImage.ts#L25 62 | if (ctx.baseDir === path.dirname(image)) { 63 | addWaterMarkImagePath = image 64 | } else { 65 | // not a clipboard image, generate a new file to save watermark image 66 | // 如果不是剪切板图片,说明图片为通过拖拽或选择的本地图片、或者是网络图片的url,需要在 picgo 目录下生成一个图片 67 | // 用于存放添加水印后的图片 68 | const extname = format || path.extname(image) || 'png' 69 | addWaterMarkImagePath = path.join(ctx.baseDir, `${dayjs().format('YYYYMMDDHHmmss')}.${extname}`) 70 | 71 | ctx.once('failed', () => { 72 | // 删除 picgo 生成的图片文件,例如 `~/.picgo/20200621205720.png` 73 | fs.remove(addWaterMarkImagePath).catch((e) => { ctx.log.error(e) }) 74 | }) 75 | ctx.once('finished', () => { 76 | // 删除 picgo 生成的图片文件,例如 `~/.picgo/20200621205720.png` 77 | fs.remove(addWaterMarkImagePath).catch((e) => { ctx.log.error(e) }) 78 | }) 79 | } 80 | 81 | await sharpedImage 82 | .composite([ 83 | { 84 | input: await sharpedWaterMark.toBuffer(), 85 | ...coordinate 86 | } 87 | ]).toFile(addWaterMarkImagePath) 88 | 89 | logger.info('watermark 水印添加成功') 90 | } 91 | return addWaterMarkImagePath 92 | }) 93 | ); 94 | return addedWaterMarkInput; 95 | }; 96 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | import sharp from "sharp"; 2 | import path from "path"; 3 | import Logger from "picgo/dist/src/lib/Logger"; 4 | import { getCoordinateByPosition, PositionType } from "./util"; 5 | 6 | interface IInput { 7 | input: any[]; 8 | minWidth: number; 9 | minHeight: number; 10 | position: string; 11 | waterMark: string | Buffer; 12 | } 13 | interface IOutput { 14 | width: number; 15 | height: number; 16 | fileName: string; 17 | extname: string; 18 | buffer: Buffer; 19 | } 20 | export const outputGen: ( 21 | iinput: IInput, 22 | logger: Logger 23 | ) => Promise = async (imageInput, logger) => { 24 | const { input, minWidth, minHeight, waterMark, position } = imageInput; 25 | const sharpedWaterMark = sharp(waterMark); 26 | const waterMarkMeta = await sharpedWaterMark.metadata(); 27 | const output = await Promise.all( 28 | input.map(async image => { 29 | const fileName = path.basename(image); 30 | const extname = path.extname(image); 31 | const sharpedImage = sharp(image); 32 | const { width, height } = await sharpedImage.metadata(); 33 | const coordinate = getCoordinateByPosition({ 34 | width, 35 | height, 36 | waterMark: { 37 | width: waterMarkMeta.width, 38 | height: waterMarkMeta.height, 39 | position: PositionType[position] 40 | } 41 | }); 42 | logger.info(JSON.stringify(coordinate)); 43 | let transformedImage = { 44 | width, 45 | height, 46 | fileName, 47 | extname, 48 | buffer: null 49 | }; 50 | 51 | // Picture width or length is too short, do not add watermark 52 | // Or trigger minimum size limit 53 | if ( 54 | coordinate.left <= 0 || 55 | coordinate.top <= 0 || 56 | width <= minWidth || 57 | height <= minHeight 58 | ) { 59 | transformedImage.buffer = await sharpedImage.toBuffer(); 60 | } else { 61 | transformedImage.buffer = await sharpedImage 62 | .composite([ 63 | { 64 | input: await sharpedWaterMark.toBuffer(), 65 | ...coordinate 66 | } 67 | ]) 68 | .toBuffer(); 69 | } 70 | return transformedImage; 71 | }) 72 | ); 73 | return output; 74 | }; 75 | -------------------------------------------------------------------------------- /src/text2svg.ts: -------------------------------------------------------------------------------- 1 | import TextToSVG from "text-to-svg"; 2 | import { fontOptions } from "./attr"; 3 | 4 | let textToSVG = null; 5 | export const loadFontFamily = (fontFamily: string): void => { 6 | textToSVG = TextToSVG.loadSync(fontFamily); 7 | }; 8 | export const getSvg = ( 9 | text: string, 10 | options?: { fontSize?: number; [propName: string]: any } 11 | ): string => { 12 | const svgOptions: {[propName: string]: any} = { 13 | attributes: {}, 14 | ...fontOptions 15 | } 16 | if (options) { 17 | svgOptions.attributes = { 18 | ...svgOptions.attributes, 19 | ...options 20 | } 21 | if (options.fontSize) { 22 | svgOptions.fontSize = options.fontSize 23 | } 24 | } 25 | return textToSVG.getSVG(text, svgOptions); 26 | }; 27 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import Picgo from "picgo"; 3 | import Color from 'color'; 4 | 5 | import { OFFSET } from "./attr"; 6 | 7 | export enum PositionType { 8 | lt = "left-top", 9 | ct = "center-top", 10 | rt = "right-top", 11 | lm = "left-middle", 12 | cm = "center-middle", 13 | rm = "right-middle", 14 | lb = "left-bottom", 15 | cb = "center-bottom", 16 | rb = "right-bottom" 17 | } 18 | 19 | interface ICoordinate { 20 | left: number; 21 | top: number; 22 | } 23 | export const getCoordinateByPosition = (prop: { 24 | width: number; 25 | height: number; 26 | waterMark: { 27 | width: number; 28 | height: number; 29 | position: PositionType; 30 | }; 31 | }): ICoordinate => { 32 | const { width, height, waterMark } = prop; 33 | const p = waterMark.position.split("-"); 34 | return p.reduce( 35 | (acc, pos) => { 36 | switch (pos) { 37 | case "left": 38 | acc.left = OFFSET.X; 39 | break; 40 | case "center": 41 | acc.left = Math.floor((width - waterMark.width) / 2); 42 | break; 43 | case "right": 44 | acc.left = Math.floor(width - OFFSET.X - waterMark.width); 45 | break; 46 | case "top": 47 | acc.top = OFFSET.Y; 48 | break; 49 | case "middle": 50 | acc.top = Math.floor((height - waterMark.height) / 2); 51 | break; 52 | case "bottom": 53 | acc.top = Math.floor(height - OFFSET.Y - waterMark.height); 54 | break; 55 | } 56 | return acc; 57 | }, 58 | { left: 0, top: 0 } 59 | ); 60 | }; 61 | 62 | export interface IConfig { 63 | text: string; 64 | textColor: string; 65 | position: string; 66 | fontSize: string; 67 | image: string; 68 | fontFamily: string; 69 | minSize: string; 70 | minWidth?: number; 71 | minHeight?: number; 72 | parsedFontSize?: number; 73 | } 74 | export const parseAndValidate: ( 75 | config: IConfig 76 | ) => [string[], IConfig] = config => { 77 | const { position, fontSize, minSize, textColor } = config; 78 | const parsedFontSize = parseInt(fontSize); 79 | let parsedConfig: IConfig = { ...config }; 80 | let errors = []; 81 | // 无效数字且不为空 82 | if (isNaN(parsedFontSize) && fontSize !== null) { 83 | errors.push("fontSize"); 84 | } else { 85 | parsedConfig.parsedFontSize = parsedFontSize; 86 | } 87 | if (position && !PositionType[position]) { 88 | errors.push("position"); 89 | } 90 | if (minSize) { 91 | let [minWidth, minHeight] = minSize.split("*").map((v: string) => +v); 92 | if (!minWidth || !minHeight) { 93 | errors.push("minSize"); 94 | } else { 95 | parsedConfig.minHeight = minHeight; 96 | parsedConfig.minWidth = minWidth; 97 | } 98 | } 99 | if (textColor) { 100 | try { 101 | parsedConfig.textColor = Color(textColor).hex() 102 | } catch (error) { 103 | errors.push('textColor') 104 | } 105 | } 106 | return [ 107 | errors, 108 | { 109 | ...config, 110 | ...parsedConfig 111 | } 112 | ]; 113 | }; 114 | 115 | // 是否是网络图片 116 | export const isUrl: (url: string) => boolean = (url) => { 117 | return /^https?:\/\//.test(url) 118 | } 119 | 120 | export const downloadImage: (ctx: Picgo, url: string) => Promise = async (ctx, url) => { 121 | return await ctx.request({ method: 'GET', url, encoding: null }) 122 | .on('error', function(err) { 123 | ctx.log.error(`网络图片下载失败,${url}`) 124 | ctx.log.error(err) 125 | }).on('response', (response: Response): void => { 126 | const contentType = response.headers['content-type'] 127 | if (contentType && !contentType.includes('image')) { 128 | throw new Error(`${url} is not image`) 129 | } 130 | }) 131 | } 132 | 133 | export const getImageBufferData: (ctx: Picgo, imageUrl: string) => Promise = (ctx, imageUrl) => { 134 | if (isUrl(imageUrl)) { 135 | return downloadImage(ctx, imageUrl) 136 | } else { 137 | return fs.readFile(imageUrl) 138 | } 139 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "skipLibCheck": true, 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "baseUrl": ".", 17 | "paths": {}, 18 | "outDir": "lib" 19 | } 20 | } 21 | --------------------------------------------------------------------------------