@zhaoshijun/compress 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - ⚙️ **灵活配置**:支持配置文件 (`compress.config.js`),可精细控制压缩参数。
12
12
  - 🛡 **安全可靠**:支持备份原文件,默认不覆盖源文件(CLI 模式输出到指定目录)。
13
13
  - 📦 **智能缓存**:Vite 插件基于内容 Hash 缓存,避免重复压缩,提升构建速度。
14
+ - 💧 **水印功能**:支持添加文字水印,可自定义透明度、密度、颜色等参数。
14
15
 
15
16
  ## 安装
16
17
 
@@ -30,6 +31,7 @@ CLI 工具用于批量扫描并压缩本地图片文件。
30
31
 
31
32
  1. **初始化配置** (可选)
32
33
  生成默认的 `compress.config.js` 文件:
34
+
33
35
  ```bash
34
36
  npx @zhaoshijun/compress init
35
37
  ```
@@ -42,13 +44,18 @@ CLI 工具用于批量扫描并压缩本地图片文件。
42
44
 
43
45
  ### 命令行选项
44
46
 
45
- | 选项 | 简写 | 描述 |
46
- | --- | --- | --- |
47
- | `--config <path>` | `-c` | 指定配置文件路径 |
48
- | `--backup` | | 启用备份模式(将原文件复制为 `.backup`) |
49
- | `--dry-run` | | 演练模式,仅列出将要处理的文件,不进行实际压缩和写入 |
50
- | `--quiet` | `-q` | 静默模式,仅输出错误信息 |
51
- | `--help` | `-h` | 显示帮助信息 |
47
+ | 选项 | 简写 | 描述 |
48
+ | ----------------- | ---- | ---------------------------------------------------- |
49
+ | `--config <path>` | `-c` | 指定配置文件路径 |
50
+ | `--backup` | | 启用备份模式(将原文件复制为 `.backup`) |
51
+ | `--dry-run` | | 演练模式,仅列出将要处理的文件,不进行实际压缩和写入 |
52
+ | `--quiet` | `-q` | 静默模式,仅输出错误信息 |
53
+ | `--watermark` | `-w` | 启用水印 |
54
+ | `--watermark-text <text>` | | 水印文本内容 |
55
+ | `--watermark-opacity <opacity>` | | 水印透明度 (0-1) |
56
+ | `--watermark-density <density>` | | 水印密度 (1-10) |
57
+ | `--watermark-color <color>` | | 水印颜色 (hex 或 rgba) |
58
+ | `--help` | `-h` | 显示帮助信息 |
52
59
 
53
60
  ### 示例
54
61
 
@@ -58,8 +65,41 @@ npx @zhaoshijun/compress --dry-run
58
65
 
59
66
  # 使用指定配置文件并开启静默模式
60
67
  npx @zhaoshijun/compress -c ./config/compress.js -q
68
+
69
+ # 压缩图片并添加水印
70
+ npx @zhaoshijun/compress -w --watermark-text "Copyright 2024"
71
+
72
+ # 压缩图片并添加自定义水印
73
+ npx @zhaoshijun/compress -w --watermark-text "My Watermark" --watermark-opacity 0.3 --watermark-color "#ff0000"
74
+ ```
75
+
76
+ ## 水印功能
77
+
78
+ ### 单独添加水印
79
+
80
+ 如果只需要给图片添加水印而不进行压缩,可以使用 `watermark` 命令:
81
+
82
+ ```bash
83
+ # 给图片添加水印(输出到 ./watermarked 目录)
84
+ npx @zhaoshijun/compress watermark --watermark-text "Copyright 2024"
85
+
86
+ # 自定义水印参数
87
+ npx @zhaoshijun/compress watermark --watermark-text "Confidential" --watermark-opacity 0.7 --watermark-density 5 --watermark-color "rgba(255,0,0,0.5)"
88
+
89
+ # 指定输出目录
90
+ npx @zhaoshijun/compress watermark --watermark-text "Watermark" --output ./output
61
91
  ```
62
92
 
93
+ ### 水印参数说明
94
+
95
+ | 参数 | 说明 | 默认值 | 范围 |
96
+ | ---- | ---- | ------ | ---- |
97
+ | `text` | 水印文本内容 | '' | 必填 |
98
+ | `opacity` | 水印透明度 | 0.5 | 0-1 |
99
+ | `density` | 水印密度(平铺数量) | 3 | 1-10 |
100
+ | `color` | 水印颜色 | '#ffffff' | hex 或 rgba |
101
+ | `fontSize` | 水印字体大小 | 24 | 正整数 |
102
+
63
103
  ## Vite 插件
64
104
 
65
105
  该插件仅在 `vite build` 生产构建模式下生效,会自动压缩项目中引用的图片资源,并替换构建产物中的引用路径。
@@ -69,17 +109,43 @@ npx @zhaoshijun/compress -c ./config/compress.js -q
69
109
  在 `vite.config.js` 中引入并添加插件:
70
110
 
71
111
  ```javascript
72
- import { defineConfig } from 'vite';
73
- import { compressVitePlugin } from '@zhaoshijun/compress';
112
+ import { defineConfig } from "vite";
113
+ import { compressVitePlugin } from "@zhaoshijun/compress";
74
114
 
75
115
  export default defineConfig({
76
116
  plugins: [
77
117
  compressVitePlugin({
78
- // 可在此处覆盖 compress.config.js 中的配置
79
- quality: 80,
80
- cache: true
81
- })
82
- ]
118
+ // 1. 覆盖通用质量
119
+ quality: 75,
120
+
121
+ // 2. 覆盖 PNG 特定选项 (注意:如果覆盖对象,需要提供完整属性,或者插件逻辑里做深度合并)
122
+ // 根据当前代码实现是浅合并,所以如果只写 palette,compressionLevel 可能会丢失(如果默认值是在loader里处理的)
123
+ // 实际上 loader.js 做了深度合并,但 vite 插件里的 options 是顶层覆盖。
124
+ // 建议:如果只改一项,尽量在 compress.config.js 里改。
125
+ // 如果必须在这里改,传入完整对象:
126
+ pngOptions: {
127
+ compressionLevel: 6,
128
+ palette: false,
129
+ },
130
+
131
+ // 3. 甚至可以开启尺寸限制(虽然通常不建议在构建时改变原图尺寸)
132
+ resize: {
133
+ maxWidth: 1024,
134
+ },
135
+
136
+ // 4. 控制缓存
137
+ cache: false,
138
+
139
+ // 5. 启用水印
140
+ watermark: {
141
+ text: 'Copyright 2024',
142
+ opacity: 0.5,
143
+ density: 3,
144
+ color: '#ffffff',
145
+ fontSize: 24
146
+ }
147
+ }),
148
+ ],
83
149
  });
84
150
  ```
85
151
 
@@ -98,16 +164,16 @@ export default defineConfig({
98
164
  module.exports = {
99
165
  // [CLI专用] 输入文件模式,支持 glob 数组
100
166
  // 默认扫描所有子目录下的 jpg, jpeg, png, webp
101
- input: ['**/*.{jpg,jpeg,png,webp}'],
167
+ input: ["**/*.{jpg,jpeg,png,webp}"],
102
168
 
103
169
  // [CLI专用] 输出目录
104
170
  // 默认输出到当前目录下的 compressed 文件夹,保持原目录结构
105
- output: './compressed',
171
+ output: "./compressed",
106
172
 
107
173
  // 尺寸调整 (可选)
108
174
  // 注意:scale 与 maxWidth/maxHeight 互斥,优先使用 maxWidth/maxHeight
109
175
  resize: {
110
- maxWidth: 1920, // 最大宽度,保持纵横比
176
+ maxWidth: 1920, // 最大宽度,保持纵横比
111
177
  // maxHeight: 1080, // 最大高度
112
178
  // scale: 0.8 // 缩放比例 (0-1)
113
179
  },
@@ -119,20 +185,29 @@ module.exports = {
119
185
  // PNG 专属配置
120
186
  pngOptions: {
121
187
  compressionLevel: 9, // 压缩等级 0-9,默认 9
122
- palette: true // 是否启用调色板量化,默认 true (显著减小体积)
188
+ palette: true, // 是否启用调色板量化,默认 true (显著减小体积)
123
189
  },
124
190
 
125
191
  // 输出文件名后缀
126
192
  // 默认 '.min',例如 image.jpg -> image.min.jpg
127
- suffix: '.min',
193
+ suffix: ".min",
128
194
 
129
195
  // [CLI专用] 是否备份原文件
130
196
  backup: false,
131
197
 
198
+ // 水印配置
199
+ watermark: {
200
+ text: '', // 水印文本内容,留空则不添加水印
201
+ opacity: 0.5, // 水印透明度 (0-1)
202
+ density: 3, // 水印密度 (1-10),数值越大水印越多
203
+ color: '#ffffff', // 水印颜色,支持 hex 或 rgba 格式
204
+ fontSize: 24 // 水印字体大小
205
+ },
206
+
132
207
  // [Vite插件专用] 配置
133
208
  vite: {
134
- cache: true // 是否启用构建缓存,默认 true
135
- }
209
+ cache: true, // 是否启用构建缓存,默认 true
210
+ },
136
211
  };
137
212
  ```
138
213
 
@@ -145,6 +220,7 @@ module.exports = {
145
220
  ## 默认排除
146
221
 
147
222
  CLI 工具默认会排除以下目录:
223
+
148
224
  - `node_modules/`
149
225
  - `dist/`
150
226
  - `.git/`
@@ -157,3 +233,15 @@ A: 工具会自动检测并跳过损坏的图片,输出警告信息,不会
157
233
 
158
234
  **Q: Vite 插件会修改源代码中的图片吗?**
159
235
  A: 不会。Vite 插件仅在构建过程中处理资源,生成的压缩图片位于构建输出目录(如 `dist/assets`),源代码保持不变。
236
+
237
+ **Q: 水印会影响图片质量吗?**
238
+ A: 水印是在图片压缩后添加的,不会影响图片本身的压缩质量。水印的透明度可以通过 `opacity` 参数调整,建议设置为 0.3-0.7 之间以获得最佳效果。
239
+
240
+ **Q: 水印密度如何设置?**
241
+ A: 水印密度 `density` 参数控制水印的平铺数量,范围是 1-10。数值越大,水印越密集。默认值为 3,适合大多数场景。
242
+
243
+ **Q: 可以只添加水印不压缩吗?**
244
+ A: 可以。使用 `watermark` 命令可以单独给图片添加水印而不进行压缩,输出到 `./watermarked` 目录。
245
+
246
+ **Q: 水印颜色支持哪些格式?**
247
+ A: 水印颜色支持十六进制格式(如 `#ffffff`)和 RGBA 格式(如 `rgba(255,255,255,0.5)`)。
package/bin/compress.js CHANGED
@@ -25,6 +25,11 @@ program
25
25
  .option('--backup', '启用备份')
26
26
  .option('--dry-run', '仅列出将处理的文件,不写入磁盘')
27
27
  .option('-q, --quiet', '静默模式,仅输出错误')
28
+ .option('-w, --watermark', '启用水印')
29
+ .option('--watermark-text <text>', '水印文本')
30
+ .option('--watermark-opacity <opacity>', '水印透明度 (0-1)', parseFloat)
31
+ .option('--watermark-density <density>', '水印密度 (1-10)', parseInt)
32
+ .option('--watermark-color <color>', '水印颜色 (hex 或 rgba)')
28
33
  .action(async (options) => {
29
34
  try {
30
35
  const startTime = Date.now();
@@ -35,6 +40,15 @@ program
35
40
  // 合并命令行选项
36
41
  if (options.backup !== undefined) config.backup = options.backup;
37
42
 
43
+ // 合并水印选项
44
+ if (options.watermark) {
45
+ config.watermark = config.watermark || {};
46
+ if (options.watermarkText) config.watermark.text = options.watermarkText;
47
+ if (options.watermarkOpacity !== undefined) config.watermark.opacity = options.watermarkOpacity;
48
+ if (options.watermarkDensity !== undefined) config.watermark.density = options.watermarkDensity;
49
+ if (options.watermarkColor) config.watermark.color = options.watermarkColor;
50
+ }
51
+
38
52
  // 确定输入源
39
53
  // 如果 config.input 未定义,则默认为当前目录 '**/*.{jpg,jpeg,png,webp}'
40
54
  // 需要结合排除规则
@@ -187,6 +201,13 @@ program
187
201
  },
188
202
  suffix: '.min',
189
203
  backup: false,
204
+ watermark: {
205
+ text: '',
206
+ opacity: 0.5,
207
+ density: 3,
208
+ color: '#ffffff',
209
+ fontSize: 24
210
+ },
190
211
  vite: {
191
212
  cache: true
192
213
  }
@@ -197,3 +218,122 @@ program
197
218
  });
198
219
 
199
220
  program.parse();
221
+
222
+ // Watermark command
223
+ program
224
+ .command('watermark')
225
+ .description('给图片添加水印(不压缩)')
226
+ .option('-c, --config <path>', '配置文件路径')
227
+ .option('--watermark-text <text>', '水印文本')
228
+ .option('--watermark-opacity <opacity>', '水印透明度 (0-1)', parseFloat)
229
+ .option('--watermark-density <density>', '水印密度 (1-10)', parseInt)
230
+ .option('--watermark-color <color>', '水印颜色 (hex 或 rgba)')
231
+ .option('--output <dir>', '输出目录(默认为 ./watermarked)')
232
+ .action(async (options) => {
233
+ try {
234
+ const startTime = Date.now();
235
+
236
+ // 加载配置
237
+ const config = await loadConfig(options.config);
238
+
239
+ // 合并水印选项
240
+ if (options.watermarkText) config.watermark.text = options.watermarkText;
241
+ if (options.watermarkOpacity !== undefined) config.watermark.opacity = options.watermarkOpacity;
242
+ if (options.watermarkDensity !== undefined) config.watermark.density = options.watermarkDensity;
243
+ if (options.watermarkColor) config.watermark.color = options.watermarkColor;
244
+
245
+ if (!config.watermark.text) {
246
+ console.error(chalk.red('Error: Watermark text is required. Use --watermark-text option or set it in config file.'));
247
+ process.exit(1);
248
+ }
249
+
250
+ // 确定输入源
251
+ const inputPattern = config.input || ['**/*.{jpg,jpeg,png,webp}'];
252
+ const ignorePattern = [...DEFAULT_EXCLUDES];
253
+
254
+ // 查找文件
255
+ const spinner = ora('Scanning files...').start();
256
+ const files = await fg(inputPattern, {
257
+ ignore: ignorePattern,
258
+ absolute: true,
259
+ onlyFiles: true
260
+ });
261
+
262
+ const targetFiles = files.filter(file => isSupported(file));
263
+
264
+ spinner.succeed(`Found ${targetFiles.length} images.`);
265
+
266
+ if (targetFiles.length === 0) {
267
+ console.log(chalk.yellow('No images found to add watermark.'));
268
+ return;
269
+ }
270
+
271
+ // 准备进度条
272
+ const multibar = new MultiBar({
273
+ clearOnComplete: false,
274
+ hideCursor: true,
275
+ format: '{bar} | {percentage}% | {value}/{total} Files | Current: {file}'
276
+ }, Presets.shades_classic);
277
+
278
+ let processedCount = 0;
279
+ let errors = [];
280
+
281
+ const progressBar = multibar.create(targetFiles.length, 0, { file: 'Starting...' });
282
+
283
+ for (const file of targetFiles) {
284
+ try {
285
+ const relativePath = path.relative(process.cwd(), file);
286
+ progressBar.update(processedCount, { file: relativePath });
287
+
288
+ const originalStat = await fs.stat(file);
289
+ const originalSize = originalStat.size;
290
+
291
+ // 备份
292
+ if (config.backup) {
293
+ await fs.copy(file, `${file}.backup`);
294
+ }
295
+
296
+ // 读取并添加水印
297
+ const outputDir = path.resolve(process.cwd(), options.output || './watermarked');
298
+ const relDir = path.dirname(relativePath);
299
+ const fileName = path.basename(file);
300
+ const outDir = path.join(outputDir, relDir);
301
+ const outFilePath = path.join(outDir, fileName);
302
+
303
+ const watermarkedBuffer = await compressImage(file, config, file);
304
+
305
+ // 写入文件
306
+ await fs.ensureDir(outDir);
307
+ await fs.writeFile(outFilePath, watermarkedBuffer);
308
+
309
+ const newSize = watermarkedBuffer.length;
310
+ const sizeDiff = newSize - originalSize;
311
+ const sizeDiffPercent = ((sizeDiff / originalSize) * 100).toFixed(2);
312
+
313
+ multibar.log(`${chalk.green('✔')} ${relativePath}: ${prettyBytes(originalSize)} → ${prettyBytes(newSize)} (${sizeDiff >= 0 ? '+' : ''}${prettyBytes(sizeDiff)}, ${sizeDiff >= 0 ? '+' : ''}${sizeDiffPercent}%)\n`);
314
+
315
+ processedCount++;
316
+ } catch (err) {
317
+ errors.push({ file: path.relative(process.cwd(), file), error: err.message });
318
+ multibar.log(`${chalk.red('✖')} ${path.relative(process.cwd(), file)}: ${err.message}\n`);
319
+ }
320
+ }
321
+
322
+ multibar.stop();
323
+
324
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
325
+
326
+ console.log('\n' + chalk.bold('Watermark Summary:'));
327
+ console.log(`Total Processed: ${processedCount}/${targetFiles.length}`);
328
+ console.log(`Time Taken: ${duration}s`);
329
+
330
+ if (errors.length > 0) {
331
+ console.log(chalk.yellow(`\nSkipped ${errors.length} files due to errors:`));
332
+ errors.forEach(e => console.log(`- ${e.file}: ${e.error}`));
333
+ }
334
+
335
+ } catch (error) {
336
+ console.error(chalk.red('Error:'), error.message);
337
+ process.exit(1);
338
+ }
339
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhaoshijun/compress",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Image compression CLI and Vite plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "sharp": "^0.33.2"
27
27
  },
28
28
  "peerDependencies": {
29
- "vite": "^5.0.0"
29
+ "vite": ">=5.0.0"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
32
  "vite": {
@@ -7,6 +7,13 @@ export const defaultConfig = {
7
7
  },
8
8
  suffix: '.min',
9
9
  backup: false,
10
+ watermark: {
11
+ text: '',
12
+ opacity: 0.5,
13
+ density: 3,
14
+ color: '#ffffff',
15
+ fontSize: 24
16
+ },
10
17
  vite: {
11
18
  cache: true
12
19
  }
@@ -26,11 +26,14 @@ export async function loadConfig(configPath) {
26
26
  ...defaultConfig.pngOptions,
27
27
  ...(userConfig.pngOptions || {})
28
28
  },
29
+ watermark: {
30
+ ...defaultConfig.watermark,
31
+ ...(userConfig.watermark || {})
32
+ },
29
33
  vite: {
30
34
  ...defaultConfig.vite,
31
35
  ...(userConfig.vite || {})
32
36
  },
33
- // resize 如果存在则覆盖
34
37
  resize: userConfig.resize || undefined
35
38
  };
36
39
  }
@@ -1,5 +1,6 @@
1
1
  import sharp from 'sharp';
2
2
  import path from 'path';
3
+ import { addWatermark } from './watermark.js';
3
4
 
4
5
  /**
5
6
  * 压缩图片
@@ -61,5 +62,11 @@ export async function compressImage(input, options, filePath) {
61
62
  }
62
63
  }
63
64
 
64
- return instance.toBuffer();
65
+ let result = await instance.toBuffer();
66
+
67
+ if (options.watermark && options.watermark.text) {
68
+ result = await addWatermark(result, options.watermark);
69
+ }
70
+
71
+ return result;
65
72
  }
@@ -0,0 +1,152 @@
1
+ import sharp from 'sharp';
2
+
3
+ /**
4
+ * 创建单个水印文本的 SVG
5
+ * @param {string} text - 水印文本
6
+ * @param {Object} options - 水印选项
7
+ * @param {number} options.opacity - 透明度 (0-1)
8
+ * @param {string} options.color - 颜色 (hex 或 rgba)
9
+ * @param {number} options.fontSize - 字体大小
10
+ * @returns {string} SVG 字符串
11
+ */
12
+ function createWatermarkSVG(text, options) {
13
+ const { opacity, color, fontSize } = options;
14
+ const rgbaColor = convertToRGBA(color, opacity);
15
+
16
+ return `
17
+ <svg width="300" height="150" xmlns="http://www.w3.org/2000/svg">
18
+ <text x="50%" y="50%"
19
+ font-family="Arial, sans-serif"
20
+ font-size="${fontSize}"
21
+ font-weight="bold"
22
+ fill="${rgbaColor}"
23
+ text-anchor="middle"
24
+ dominant-baseline="middle"
25
+ transform="rotate(-30, 150, 75)">
26
+ ${text}
27
+ </text>
28
+ </svg>
29
+ `;
30
+ }
31
+
32
+ /**
33
+ * 将颜色转换为 RGBA 格式
34
+ * @param {string} color - 颜色值 (hex 或 rgba)
35
+ * @param {number} opacity - 透明度
36
+ * @returns {string} RGBA 颜色字符串
37
+ */
38
+ function convertToRGBA(color, opacity) {
39
+ if (color.startsWith('rgba')) {
40
+ return color.replace(/[\d.]+\)$/, `${opacity})`);
41
+ }
42
+
43
+ if (color.startsWith('rgb')) {
44
+ return color.replace(')', `, ${opacity})`).replace('rgb', 'rgba');
45
+ }
46
+
47
+ if (color.startsWith('#')) {
48
+ const hex = color.replace('#', '');
49
+ const r = parseInt(hex.substring(0, 2), 16);
50
+ const g = parseInt(hex.substring(2, 4), 16);
51
+ const b = parseInt(hex.substring(4, 6), 16);
52
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
53
+ }
54
+
55
+ return `rgba(128, 128, 128, ${opacity})`;
56
+ }
57
+
58
+ /**
59
+ * 创建平铺水印层
60
+ * @param {number} width - 图片宽度
61
+ * @param {number} height - 图片高度
62
+ * @param {string} text - 水印文本
63
+ * @param {Object} options - 水印选项
64
+ * @returns {Promise<Buffer>} 水印层 Buffer
65
+ */
66
+ async function createTiledWatermark(width, height, text, options) {
67
+ const { density, opacity, color, fontSize } = options;
68
+
69
+ const watermarkSVG = createWatermarkSVG(text, { opacity, color, fontSize });
70
+ const watermarkBuffer = Buffer.from(watermarkSVG);
71
+
72
+ const watermarkImage = sharp(watermarkBuffer);
73
+ const watermarkMeta = await watermarkImage.metadata();
74
+
75
+ const tileSize = Math.max(watermarkMeta.width, watermarkMeta.height);
76
+ const spacing = Math.floor(tileSize / density);
77
+
78
+ const cols = Math.ceil(width / spacing) + 1;
79
+ const rows = Math.ceil(height / spacing) + 1;
80
+
81
+ const tiledWidth = cols * tileSize;
82
+ const tiledHeight = rows * tileSize;
83
+
84
+ const composites = [];
85
+ for (let row = 0; row < rows; row++) {
86
+ for (let col = 0; col < cols; col++) {
87
+ composites.push({
88
+ input: watermarkBuffer,
89
+ left: col * spacing,
90
+ top: row * spacing
91
+ });
92
+ }
93
+ }
94
+
95
+ const tiledWatermark = await sharp({
96
+ create: {
97
+ width: tiledWidth,
98
+ height: tiledHeight,
99
+ channels: 4,
100
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
101
+ }
102
+ })
103
+ .composite(composites)
104
+ .extract({ left: 0, top: 0, width, height })
105
+ .png()
106
+ .toBuffer();
107
+
108
+ return tiledWatermark;
109
+ }
110
+
111
+ /**
112
+ * 给图片添加水印
113
+ * @param {string|Buffer} input - 图片路径或 Buffer
114
+ * @param {Object} watermarkOptions - 水印配置
115
+ * @param {string} watermarkOptions.text - 水印文本
116
+ * @param {number} [watermarkOptions.opacity=0.5] - 透明度 (0-1)
117
+ * @param {number} [watermarkOptions.density=3] - 水印密度 (1-10)
118
+ * @param {string} [watermarkOptions.color='#ffffff'] - 水印颜色
119
+ * @param {number} [watermarkOptions.fontSize=24] - 字体大小
120
+ * @returns {Promise<Buffer>} 添加水印后的图片 Buffer
121
+ */
122
+ export async function addWatermark(input, watermarkOptions) {
123
+ const { text, opacity = 0.5, density = 3, color = '#ffffff', fontSize = 24 } = watermarkOptions;
124
+
125
+ if (!text) {
126
+ throw new Error('Watermark text is required');
127
+ }
128
+
129
+ if (opacity < 0 || opacity > 1) {
130
+ throw new Error('Opacity must be between 0 and 1');
131
+ }
132
+
133
+ if (density < 1 || density > 10) {
134
+ throw new Error('Density must be between 1 and 10');
135
+ }
136
+
137
+ const image = sharp(input);
138
+ const metadata = await image.metadata();
139
+
140
+ const watermarkBuffer = await createTiledWatermark(
141
+ metadata.width,
142
+ metadata.height,
143
+ text,
144
+ { opacity, density, color, fontSize }
145
+ );
146
+
147
+ const result = await image
148
+ .composite([{ input: watermarkBuffer, blend: 'over' }])
149
+ .toBuffer();
150
+
151
+ return result;
152
+ }
@@ -4,9 +4,18 @@ import crypto from 'crypto';
4
4
 
5
5
  const CACHE_DIR = path.resolve('node_modules/.cache/@zhaoshijun/compress');
6
6
 
7
- export async function getCache(content) {
7
+ export async function getCache(content, watermarkOptions) {
8
8
  const hash = crypto.createHash('sha256').update(content).digest('hex');
9
- const cachePath = path.join(CACHE_DIR, hash);
9
+
10
+ let cacheKey = hash;
11
+ if (watermarkOptions && watermarkOptions.text) {
12
+ const watermarkHash = crypto.createHash('sha256')
13
+ .update(JSON.stringify(watermarkOptions))
14
+ .digest('hex');
15
+ cacheKey = `${hash}-${watermarkHash}`;
16
+ }
17
+
18
+ const cachePath = path.join(CACHE_DIR, cacheKey);
10
19
 
11
20
  if (await fs.pathExists(cachePath)) {
12
21
  return await fs.readFile(cachePath);
@@ -14,9 +23,18 @@ export async function getCache(content) {
14
23
  return null;
15
24
  }
16
25
 
17
- export async function setCache(content, compressedContent) {
26
+ export async function setCache(content, compressedContent, watermarkOptions) {
18
27
  const hash = crypto.createHash('sha256').update(content).digest('hex');
19
- const cachePath = path.join(CACHE_DIR, hash);
28
+
29
+ let cacheKey = hash;
30
+ if (watermarkOptions && watermarkOptions.text) {
31
+ const watermarkHash = crypto.createHash('sha256')
32
+ .update(JSON.stringify(watermarkOptions))
33
+ .digest('hex');
34
+ cacheKey = `${hash}-${watermarkHash}`;
35
+ }
36
+
37
+ const cachePath = path.join(CACHE_DIR, cacheKey);
20
38
 
21
39
  await fs.ensureDir(CACHE_DIR);
22
40
  await fs.writeFile(cachePath, compressedContent);
package/vite/index.js CHANGED
@@ -36,6 +36,13 @@ export function compressVitePlugin(options = {}) {
36
36
  if (options.quality) config.quality = options.quality;
37
37
  // 如果 options 里有 cache,覆盖 config.vite.cache
38
38
  if (options.cache !== undefined) config.vite.cache = options.cache;
39
+ // 特殊处理:如果 options 里有 watermark,深度合并
40
+ if (options.watermark) {
41
+ config.watermark = {
42
+ ...fileConfig.watermark,
43
+ ...options.watermark
44
+ };
45
+ }
39
46
  },
40
47
 
41
48
  async generateBundle(outputOptions, bundle) {
@@ -62,7 +69,7 @@ export function compressVitePlugin(options = {}) {
62
69
 
63
70
  // 缓存检查
64
71
  if (config.vite.cache) {
65
- const cached = await getCache(originalBuffer);
72
+ const cached = await getCache(originalBuffer, config.watermark);
66
73
  if (cached) {
67
74
  compressedBuffer = cached;
68
75
  isCached = true;
@@ -76,7 +83,7 @@ export function compressVitePlugin(options = {}) {
76
83
 
77
84
  // 写入缓存
78
85
  if (config.vite.cache) {
79
- await setCache(originalBuffer, compressedBuffer);
86
+ await setCache(originalBuffer, compressedBuffer, config.watermark);
80
87
  }
81
88
  } catch (error) {
82
89
  console.error(chalk.red(`[compress] Failed to compress ${fileName}: ${error.message}`));