diff --git a/.gitignore b/.gitignore index d4777d2..525e708 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +node_modules/ # ---> macOS # General .DS_Store diff --git a/image-watermark/deno.json b/image-watermark/deno.json new file mode 100644 index 0000000..aec0520 --- /dev/null +++ b/image-watermark/deno.json @@ -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" + } +} diff --git a/image-watermark/deno.lock b/image-watermark/deno.lock new file mode 100644 index 0000000..a6620ea --- /dev/null +++ b/image-watermark/deno.lock @@ -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" + ] + } +} diff --git a/image-watermark/main.ts b/image-watermark/main.ts new file mode 100755 index 0000000..7368c58 --- /dev/null +++ b/image-watermark/main.ts @@ -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 { + 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 = ` + ${backgroundColor ? `` : ""} + ${escapeXml(text)} + `; + + // 将 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, "'"); +} + +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' "); + 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' + }); + } +} diff --git a/image-watermark/main_test.ts b/image-watermark/main_test.ts new file mode 100644 index 0000000..6b7bcca --- /dev/null +++ b/image-watermark/main_test.ts @@ -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 + } + } +});