update image watermark

This commit is contained in:
2026-03-08 16:34:44 +08:00
parent 27946da3f9
commit 3ac99748aa
4 changed files with 52 additions and 26 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea/
node_modules/ node_modules/
# ---> macOS # ---> macOS
# General # General

View File

@@ -6,6 +6,7 @@
}, },
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"imports": { "imports": {
"@std/cli/parse-args": "jsr:@std/cli/parse-args",
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"sharp": "npm:sharp@^0.33.0" "sharp": "npm:sharp@^0.33.0"
} }

View File

@@ -254,6 +254,7 @@
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@std/assert@1", "jsr:@std/assert@1",
"jsr:@std/cli@*",
"npm:sharp@0.33" "npm:sharp@0.33"
] ]
} }

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env -S deno run -A #!/usr/bin/env -S deno run -A
import sharp from "sharp"; import sharp from "sharp";
import { Buffer } from "node:buffer"; import {Buffer} from "node:buffer";
import {parseArgs} from "jsr:@std/cli/parse-args"; import {parseArgs} from "@std/cli/parse-args";
export interface WatermarkOptions { export interface WatermarkOptions {
/** 水印文字 */ /** 水印文字 */
@@ -23,6 +23,13 @@ export interface WatermarkOptions {
outputPath?: string; outputPath?: string;
} }
function getDefaultOutputPath(inputPath: string): string {
const lastDot = inputPath.lastIndexOf(".");
return lastDot !== -1
? `${inputPath.slice(0, lastDot)}_watermarked${inputPath.slice(lastDot)}`
: `${inputPath}_watermarked`;
}
/** /**
* 在图片右下角添加文字水印 * 在图片右下角添加文字水印
* @param inputPath 输入图片路径 * @param inputPath 输入图片路径
@@ -44,10 +51,7 @@ export async function addWatermark(
let outputPath = options.outputPath; let outputPath = options.outputPath;
if (!outputPath) { if (!outputPath) {
const lastDot = inputPath.lastIndexOf("."); outputPath = getDefaultOutputPath(inputPath);
outputPath = lastDot !== -1
? `${inputPath.slice(0, lastDot)}_watermarked${inputPath.slice(lastDot)}`
: `${inputPath}_watermarked`;
} }
// 获取原图信息 // 获取原图信息
@@ -68,30 +72,40 @@ export async function addWatermark(
// text-anchor="end" 意味着文字的右边缘在指定的 x 坐标 // text-anchor="end" 意味着文字的右边缘在指定的 x 坐标
const textX = width - marginRight - 20; const textX = width - marginRight - 20;
const textY = height - marginBottom; const textY = height - marginBottom;
// 背景矩形的左上角坐标 // 背景矩形的左上角坐标
// 背景右边缘与文字右边缘对齐 // 背景右边缘与文字右边缘对齐
const rectX = textX - bgWidth + 20; const rectX = textX - bgWidth + 20;
const rectY = textY - bgHeight + backgroundPadding + 2; const rectY = textY - bgHeight + backgroundPadding + 2;
// 创建最终 SVG 水印 // 创建最终 SVG 水印
const svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> const svg =
${backgroundColor ? `<rect x="${rectX}" y="${rectY}" width="${bgWidth}" height="${bgHeight}" fill="${backgroundColor}" rx="4"/>` : ""} `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<text x="${textX}" y="${textY}" font-family="sans-serif" font-size="${fontSize}" fill="${color}" text-anchor="end" dominant-baseline="alphabetic">${escapeXml(text)}</text> ${
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>`;
// 将 SVG 水印叠加到原图 // 将 SVG 水印叠加到原图
await sharp(inputPath) await sharp(inputPath).composite([
.composite([ {
{ input: Buffer.from(svg),
input: Buffer.from(svg), top: 0,
top: 0, left: 0,
left: 0, },
}, ])
])
.toFile(outputPath); .toFile(outputPath);
console.log(`Added watermark: ${outputPath}`); if (inputPath == outputPath) {
console.log(`Added watermark: ${inputPath}`);
} else {
console.log(`Added watermark: ${inputPath} -> ${outputPath}`);
}
} }
/** /**
@@ -108,8 +122,8 @@ function escapeXml(str: string): string {
if (import.meta.main) { if (import.meta.main) {
const flags = parseArgs(Deno.args, { const flags = parseArgs(Deno.args, {
boolean: ["help"], boolean: ["help", "unsafe-replace-file"],
string: ["watermark"], string: ["watermark"],
}); });
const watermarkText = flags.watermark; const watermarkText = flags.watermark;
@@ -117,17 +131,26 @@ if (import.meta.main) {
if (!watermarkText || imagePaths.length == 0) { if (!watermarkText || imagePaths.length == 0) {
console.log("Usage: deno run -A main.ts --watermark 'watermark' <FILES>"); console.log("Usage: deno run -A main.ts --watermark 'watermark' <FILES>");
console.log("Example: deno run -A main.ts --watermark '© 2026 MyName' image.jpg"); console.log(
"Example: deno run -A main.ts --watermark '© 2026 MyName' image.jpg",
);
Deno.exit(1); Deno.exit(1);
} }
for (const imagePath of imagePaths) { for (const imagePath of imagePaths) {
await addWatermark(imagePath+'', { const imagePathStr = imagePath as string;
const options = {
fontSize: 32, fontSize: 32,
text: watermarkText, text: watermarkText,
color: 'black', color: "black",
backgroundPadding: 2, backgroundPadding: 2,
backgroundColor: 'white' backgroundColor: "white",
}); } as WatermarkOptions;
if (flags["unsafe-replace-file"]) {
options.outputPath = imagePathStr;
}
await addWatermark(imagePathStr, options);
} }
} }