@zhaoshijun/compress 1.0.1 → 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 +69 -0
- package/bin/compress.js +140 -0
- package/package.json +1 -1
- package/src/config/defaults.js +7 -0
- package/src/config/loader.js +4 -1
- package/src/core/compressor.js +8 -1
- package/src/core/watermark.js +152 -0
- package/src/utils/cache.js +22 -4
- package/vite/index.js +9 -2
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
|
|
|
@@ -49,6 +50,11 @@ CLI 工具用于批量扫描并压缩本地图片文件。
|
|
|
49
50
|
| `--backup` | | 启用备份模式(将原文件复制为 `.backup`) |
|
|
50
51
|
| `--dry-run` | | 演练模式,仅列出将要处理的文件,不进行实际压缩和写入 |
|
|
51
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) |
|
|
52
58
|
| `--help` | `-h` | 显示帮助信息 |
|
|
53
59
|
|
|
54
60
|
### 示例
|
|
@@ -59,8 +65,41 @@ npx @zhaoshijun/compress --dry-run
|
|
|
59
65
|
|
|
60
66
|
# 使用指定配置文件并开启静默模式
|
|
61
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
|
|
62
91
|
```
|
|
63
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
|
+
|
|
64
103
|
## Vite 插件
|
|
65
104
|
|
|
66
105
|
该插件仅在 `vite build` 生产构建模式下生效,会自动压缩项目中引用的图片资源,并替换构建产物中的引用路径。
|
|
@@ -96,6 +135,15 @@ export default defineConfig({
|
|
|
96
135
|
|
|
97
136
|
// 4. 控制缓存
|
|
98
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
|
+
}
|
|
99
147
|
}),
|
|
100
148
|
],
|
|
101
149
|
});
|
|
@@ -147,6 +195,15 @@ module.exports = {
|
|
|
147
195
|
// [CLI专用] 是否备份原文件
|
|
148
196
|
backup: false,
|
|
149
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
|
+
|
|
150
207
|
// [Vite插件专用] 配置
|
|
151
208
|
vite: {
|
|
152
209
|
cache: true, // 是否启用构建缓存,默认 true
|
|
@@ -176,3 +233,15 @@ A: 工具会自动检测并跳过损坏的图片,输出警告信息,不会
|
|
|
176
233
|
|
|
177
234
|
**Q: Vite 插件会修改源代码中的图片吗?**
|
|
178
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
package/src/config/defaults.js
CHANGED
package/src/config/loader.js
CHANGED
|
@@ -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
|
}
|
package/src/core/compressor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/utils/cache.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}`));
|