feat: add image watermark

This commit is contained in:
2026-03-07 22:14:29 +08:00
parent 25d3b7d847
commit b091ad73d6
5 changed files with 455 additions and 0 deletions

133
image-watermark/main.ts Executable file
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
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'
});
}
}