feat: add image watermark
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
# ---> macOS
|
||||
# General
|
||||
.DS_Store
|
||||
|
||||
12
image-watermark/deno.json
Normal file
12
image-watermark/deno.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": "deno run --watch main.ts",
|
||||
"watermark": "deno run --allow-read --allow-write --allow-env --allow-ffi main.ts",
|
||||
"test": "deno test --allow-read --allow-write --allow-env --allow-ffi"
|
||||
},
|
||||
"nodeModulesDir": "auto",
|
||||
"imports": {
|
||||
"@std/assert": "jsr:@std/assert@1",
|
||||
"sharp": "npm:sharp@^0.33.0"
|
||||
}
|
||||
}
|
||||
260
image-watermark/deno.lock
generated
Normal file
260
image-watermark/deno.lock
generated
Normal file
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@1": "1.0.13",
|
||||
"jsr:@std/cli@*": "1.0.27",
|
||||
"jsr:@std/internal@^1.0.6": "1.0.12",
|
||||
"npm:sharp@0.33": "0.33.5"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.13": {
|
||||
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/cli@1.0.27": {
|
||||
"integrity": "eba97edd0891871a7410e835dd94b3c260c709cca5983df2689c25a71fbe04de"
|
||||
},
|
||||
"@std/internal@1.0.12": {
|
||||
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@emnapi/runtime@1.8.1": {
|
||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||
"dependencies": [
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"@img/sharp-darwin-arm64@0.33.5": {
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-darwin-arm64"
|
||||
],
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-darwin-x64@0.33.5": {
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-darwin-x64"
|
||||
],
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-darwin-arm64@1.0.4": {
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-darwin-x64@1.0.4": {
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm64@1.0.4": {
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm@1.0.5": {
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-s390x@1.0.4": {
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["s390x"]
|
||||
},
|
||||
"@img/sharp-libvips-linux-x64@1.0.4": {
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.0.4": {
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.0.4": {
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-linux-arm64@0.33.5": {
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-arm64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-linux-arm@0.33.5": {
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-arm"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@img/sharp-linux-s390x@0.33.5": {
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-s390x"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["s390x"]
|
||||
},
|
||||
"@img/sharp-linux-x64@0.33.5": {
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linux-x64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-linuxmusl-arm64@0.33.5": {
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linuxmusl-arm64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@img/sharp-linuxmusl-x64@0.33.5": {
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-libvips-linuxmusl-x64"
|
||||
],
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@img/sharp-wasm32@0.33.5": {
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"dependencies": [
|
||||
"@emnapi/runtime"
|
||||
],
|
||||
"cpu": ["wasm32"]
|
||||
},
|
||||
"@img/sharp-win32-ia32@0.33.5": {
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@img/sharp-win32-x64@0.33.5": {
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"color-convert@2.0.1": {
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": [
|
||||
"color-name"
|
||||
]
|
||||
},
|
||||
"color-name@1.1.4": {
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"color-string@1.9.1": {
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dependencies": [
|
||||
"color-name",
|
||||
"simple-swizzle"
|
||||
]
|
||||
},
|
||||
"color@4.2.3": {
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"dependencies": [
|
||||
"color-convert",
|
||||
"color-string"
|
||||
]
|
||||
},
|
||||
"detect-libc@2.1.2": {
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="
|
||||
},
|
||||
"is-arrayish@0.3.4": {
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="
|
||||
},
|
||||
"semver@7.7.4": {
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"bin": true
|
||||
},
|
||||
"sharp@0.33.5": {
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"dependencies": [
|
||||
"color",
|
||||
"detect-libc",
|
||||
"semver"
|
||||
],
|
||||
"optionalDependencies": [
|
||||
"@img/sharp-darwin-arm64",
|
||||
"@img/sharp-darwin-x64",
|
||||
"@img/sharp-libvips-darwin-arm64",
|
||||
"@img/sharp-libvips-darwin-x64",
|
||||
"@img/sharp-libvips-linux-arm",
|
||||
"@img/sharp-libvips-linux-arm64",
|
||||
"@img/sharp-libvips-linux-s390x",
|
||||
"@img/sharp-libvips-linux-x64",
|
||||
"@img/sharp-libvips-linuxmusl-arm64",
|
||||
"@img/sharp-libvips-linuxmusl-x64",
|
||||
"@img/sharp-linux-arm",
|
||||
"@img/sharp-linux-arm64",
|
||||
"@img/sharp-linux-s390x",
|
||||
"@img/sharp-linux-x64",
|
||||
"@img/sharp-linuxmusl-arm64",
|
||||
"@img/sharp-linuxmusl-x64",
|
||||
"@img/sharp-wasm32",
|
||||
"@img/sharp-win32-ia32",
|
||||
"@img/sharp-win32-x64"
|
||||
],
|
||||
"scripts": true
|
||||
},
|
||||
"simple-swizzle@0.2.4": {
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"dependencies": [
|
||||
"is-arrayish"
|
||||
]
|
||||
},
|
||||
"tslib@2.8.1": {
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
|
||||
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
|
||||
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
|
||||
"https://deno.land/x/imagescript@1.3.0/png/src/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d",
|
||||
"https://deno.land/x/imagescript@1.3.0/png/src/png.mjs": "96ef0ceff1b5a6cd9304749e5f187b4ab238509fb5f9a8be8ee934240271ed8d",
|
||||
"https://deno.land/x/imagescript@1.3.0/png/src/zlib.mjs": "9867dc3fab1d31b664f9344b0d7e977f493d9c912a76c760d012ed2b89f7061c",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/buffer.js": "952cb1beb8827e50a493a5d1f29a4845e8c648789406d389dd51f51205ba02d8",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/crc32.js": "573d6222b3605890714ebc374e687ec2aa3e9a949223ea199483e47ca4864f7d",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/png.js": "fbed9117e0a70602645d70df9c103ff6e79c03e987bd5c1685dcb4200729b6de",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/font.js": "9e75d842608c057045698d6a7cdf5ffd27241b5cdea0391c89a1917b31294524",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/gif.js": "8b86f7b96486bb8ff50fbc7c7487f86cb5cef85e6acd71e1def78a1aa2f12e4f",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/jpeg.js": "75295e2fcf96b4f7bb894b3844fdaa8140d63169d28b466b5d5be89d59a7b6e6",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/png.js": "0659536a8dd8f892c8346e268b2754b4414fad0ec1e9794dfcde1ba1c804ee02",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/svg.js": "f5c8a9d1977b51a7c07549ceb6bbbaca9497321a193f28b3dc229a42d91bcf14",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/tiff.js": "c2d7bdaef094df25aae1752e75167f485e89275d76a1379e39d8949580b7af4f",
|
||||
"https://deno.land/x/imagescript@1.3.0/utils/wasm/zlib.js": "749875f83abffe24d3b977475a0cbd5f9b52bee1fbdbef61ec183cbfc17805f6",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/framebuffer.mjs": "add44ff184636659714b3c6d4b896f628545451abffbc30b5bcc2e8d9a73d012",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/blur.mjs": "80716f1ffab8a2aeb54a036f583bf51a2b9dd37e005adc000add803df8e8a12f",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/color.mjs": "5e72cdcbf97dc939a2795223f01e3cb0544c0c56b03ea2aa026050df58348814",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/crop.mjs": "69431fa6f687fd9f0c31eff0ec27d7ac925275005e53a37f0c3fab4cc4d9a9ea",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/fill.mjs": "cf1b9488314753fbc9ebf03410ac74c2a34ea5a69fb6892cd6e8366cd1930d93",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/flip.mjs": "825a34a66567dcf15e76a719f1bf2f66fb106503cd69942292b1b0ae05c5718e",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/index.mjs": "423ba687119be2bba8cec72890577d3afa3621b6b8108912242fe937a183f2aa",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/iterator.mjs": "c2adf3d90ce00719a02c48c97634574176a3501ff026676259bd71aa8f5d69b9",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/overlay.mjs": "7e6e2c2ffd25006d52597ab8babc5f8f503d388a3fdf2fbc0eaea02799a020c9",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/resize.mjs": "814e78ebce8eaf8f1f918688db7b52a141405e06a36ed4b25d04413d69e7d17b",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/ops/rotate.mjs": "a1b65616717bd2eed8db406affea3263b4674dada46b56441ef38167a187455d",
|
||||
"https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d"
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@1",
|
||||
"npm:sharp@0.33"
|
||||
]
|
||||
}
|
||||
}
|
||||
133
image-watermark/main.ts
Executable file
133
image-watermark/main.ts
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env -S deno run -A
|
||||
|
||||
import sharp from "sharp";
|
||||
import { Buffer } from "node:buffer";
|
||||
import {parseArgs} from "jsr:@std/cli/parse-args";
|
||||
|
||||
export interface WatermarkOptions {
|
||||
/** 水印文字 */
|
||||
text: string;
|
||||
/** 字体大小 (默认 24) */
|
||||
fontSize?: number;
|
||||
/** 字体颜色 (默认白色,格式:#FFFFFF 或 rgba) */
|
||||
color?: string;
|
||||
/** 背景颜色 (默认透明,格式:#00000080 或 rgba) */
|
||||
backgroundColor?: string;
|
||||
/** 背景内边距 (默认 5) */
|
||||
backgroundPadding?: number;
|
||||
/** 距离右边距的像素 (默认 10) */
|
||||
marginRight?: number;
|
||||
/** 距离底边距的像素 (默认 10) */
|
||||
marginBottom?: number;
|
||||
/** 输出路径 (默认在原文件名后添加 _watermarked) */
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图片右下角添加文字水印
|
||||
* @param inputPath 输入图片路径
|
||||
* @param options 水印选项
|
||||
*/
|
||||
export async function addWatermark(
|
||||
inputPath: string,
|
||||
options: WatermarkOptions,
|
||||
): Promise<void> {
|
||||
const {
|
||||
text,
|
||||
fontSize = 24,
|
||||
color = "black",
|
||||
backgroundColor,
|
||||
backgroundPadding = 5,
|
||||
marginRight = 10,
|
||||
marginBottom = 10,
|
||||
} = options;
|
||||
|
||||
let outputPath = options.outputPath;
|
||||
if (!outputPath) {
|
||||
const lastDot = inputPath.lastIndexOf(".");
|
||||
outputPath = lastDot !== -1
|
||||
? `${inputPath.slice(0, lastDot)}_watermarked${inputPath.slice(lastDot)}`
|
||||
: `${inputPath}_watermarked`;
|
||||
}
|
||||
|
||||
// 获取原图信息
|
||||
const metadata = await sharp(inputPath).metadata();
|
||||
const width = metadata.width!;
|
||||
const height = metadata.height!;
|
||||
|
||||
// 估算文字宽度(使用更精确的系数,根据常见字体调整)
|
||||
// sans-serif 字体在 SVG 中的平均宽度系数约为 0.5-0.6
|
||||
const textWidth = Math.ceil(text.length * fontSize * 0.55);
|
||||
const textHeight = Math.ceil(fontSize * 0.9);
|
||||
|
||||
// 背景矩形尺寸
|
||||
const bgWidth = textWidth + backgroundPadding * 2;
|
||||
const bgHeight = textHeight + backgroundPadding * 2;
|
||||
|
||||
// 文字和背景位置(右下角对齐)
|
||||
// text-anchor="end" 意味着文字的右边缘在指定的 x 坐标
|
||||
const textX = width - marginRight - 20;
|
||||
const textY = height - marginBottom;
|
||||
|
||||
// 背景矩形的左上角坐标
|
||||
// 背景右边缘与文字右边缘对齐
|
||||
const rectX = textX - bgWidth + 20;
|
||||
const rectY = textY - bgHeight + backgroundPadding + 2;
|
||||
|
||||
// 创建最终 SVG 水印
|
||||
const svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
${backgroundColor ? `<rect x="${rectX}" y="${rectY}" width="${bgWidth}" height="${bgHeight}" fill="${backgroundColor}" rx="4"/>` : ""}
|
||||
<text x="${textX}" y="${textY}" font-family="sans-serif" font-size="${fontSize}" fill="${color}" text-anchor="end" dominant-baseline="alphabetic">${escapeXml(text)}</text>
|
||||
</svg>`;
|
||||
|
||||
// 将 SVG 水印叠加到原图
|
||||
await sharp(inputPath)
|
||||
.composite([
|
||||
{
|
||||
input: Buffer.from(svg),
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
])
|
||||
.toFile(outputPath);
|
||||
|
||||
console.log(`Added watermark: ${outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 XML 特殊字符
|
||||
*/
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const flags = parseArgs(Deno.args, {
|
||||
boolean: ["help"],
|
||||
string: ["watermark"],
|
||||
});
|
||||
|
||||
const watermarkText = flags.watermark;
|
||||
const imagePaths = flags._;
|
||||
|
||||
if (!watermarkText || imagePaths.length == 0) {
|
||||
console.log("Usage: deno run -A main.ts --watermark 'watermark' <FILES>");
|
||||
console.log("Example: deno run -A main.ts --watermark '© 2026 MyName' image.jpg");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
for (const imagePath of imagePaths) {
|
||||
await addWatermark(imagePath+'', {
|
||||
fontSize: 32,
|
||||
text: watermarkText,
|
||||
color: 'black',
|
||||
backgroundPadding: 2,
|
||||
backgroundColor: 'white'
|
||||
});
|
||||
}
|
||||
}
|
||||
49
image-watermark/main_test.ts
Normal file
49
image-watermark/main_test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { assertEquals } from "@std/assert";
|
||||
import { addWatermark } from "./main.ts";
|
||||
|
||||
Deno.test("addWatermark - should add watermark to image", async () => {
|
||||
const { default: sharp } = await import("sharp");
|
||||
|
||||
// 创建测试图片 (200x200 红色方块)
|
||||
const testImagePath = "/tmp/test_image.png";
|
||||
await sharp({
|
||||
create: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
channels: 3,
|
||||
background: { r: 255, g: 0, b: 0 },
|
||||
},
|
||||
}).png().toFile(testImagePath);
|
||||
|
||||
const outputPath = "/tmp/test_image_watermarked.png";
|
||||
|
||||
try {
|
||||
// 添加水印(带背景色)
|
||||
await addWatermark(testImagePath, {
|
||||
text: "Test Watermark",
|
||||
fontSize: 16,
|
||||
color: "white",
|
||||
backgroundColor: "#80000000",
|
||||
backgroundPadding: 5,
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
outputPath,
|
||||
});
|
||||
|
||||
// 验证输出文件存在
|
||||
const stat = await Deno.stat(outputPath);
|
||||
assertEquals(stat.isFile, true);
|
||||
} finally {
|
||||
// 清理
|
||||
try {
|
||||
await Deno.remove(testImagePath);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
try {
|
||||
await Deno.remove(outputPath);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user