@zhaoshijun/compress 1.4.3 → 1.5.0

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/bin/compress.js CHANGED
@@ -387,6 +387,32 @@ ${chalk.bold('💧 使用示例:')}
387
387
  process.exit(1);
388
388
  }
389
389
 
390
+ // 验证数值参数范围
391
+ const validations = [
392
+ { value: config.watermark.opacity, name: '--watermark-opacity', min: 0, max: 1 },
393
+ { value: config.watermark.density, name: '--watermark-density', min: 1, max: 10 },
394
+ { value: config.watermark.fontSize, name: '--watermark-font-size', min: 8, max: 200 },
395
+ { value: config.watermark.width, name: '--watermark-width', min: 1 },
396
+ { value: config.watermark.height, name: '--watermark-height', min: 1 },
397
+ { value: config.watermark.padding, name: '--watermark-padding', min: 0 },
398
+ { value: config.watermark.angle, name: '--watermark-angle', min: -180, max: 180 },
399
+ { value: config.watermark.spacingX, name: '--watermark-spacing-x', min: 1 },
400
+ { value: config.watermark.spacingY, name: '--watermark-spacing-y', min: 1 }
401
+ ];
402
+
403
+ for (const validation of validations) {
404
+ if (validation.value !== undefined && validation.value !== null) {
405
+ if (validation.min !== undefined && validation.value < validation.min) {
406
+ console.error(chalk.red(`Error: ${validation.name} must be >= ${validation.min}, got ${validation.value}`));
407
+ process.exit(1);
408
+ }
409
+ if (validation.max !== undefined && validation.value > validation.max) {
410
+ console.error(chalk.red(`Error: ${validation.name} must be <= ${validation.max}, got ${validation.value}`));
411
+ process.exit(1);
412
+ }
413
+ }
414
+ }
415
+
390
416
  // 确定输入源
391
417
  const inputPattern = config.input || ['**/*.{jpg,jpeg,png,webp}'];
392
418
  const ignorePattern = [...DEFAULT_EXCLUDES];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhaoshijun/compress",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Image compression CLI and Vite plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,9 @@
12
12
  "./vite": "./vite/index.js"
13
13
  },
14
14
  "scripts": {
15
- "test": "echo \"Error: no test specified\" && exit 1"
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "test:coverage": "vitest run --coverage"
16
18
  },
17
19
  "dependencies": {
18
20
  "chalk": "^5.3.0",
@@ -37,6 +39,7 @@
37
39
  "node": ">=18.0.0"
38
40
  },
39
41
  "devDependencies": {
40
- "vite": "^7.3.1"
42
+ "vite": "^7.3.1",
43
+ "vitest": "^4.1.2"
41
44
  }
42
45
  }
@@ -1,6 +1,5 @@
1
1
  export const defaultConfig = {
2
2
  output: './compressed',
3
- quality: 88,
4
3
  keepMetadata: false,
5
4
  jpegOptions: {
6
5
  quality: 88,
@@ -2,6 +2,23 @@ import sharp from 'sharp';
2
2
  import path from 'path';
3
3
  import { addWatermark, addImageWatermark, addTiledImageWatermark } from './watermark.js';
4
4
 
5
+ export function getWatermarkMode(watermarkConfig) {
6
+ if (!watermarkConfig) return null;
7
+
8
+ const mode = watermarkConfig.mode || 'text';
9
+
10
+ if (mode === 'text' && watermarkConfig.text?.trim()) return 'text';
11
+ if (mode === 'image' && watermarkConfig.imagePath) return 'image';
12
+ if (mode === 'tiled' && watermarkConfig.imagePath) return 'tiled';
13
+
14
+ if (watermarkConfig.text?.trim()) return 'text';
15
+ if (watermarkConfig.imagePath) {
16
+ return watermarkConfig.mode === 'tiled' ? 'tiled' : 'image';
17
+ }
18
+
19
+ return null;
20
+ }
21
+
5
22
  /**
6
23
  * 只添加水印(不压缩)
7
24
  * @param {string|Buffer} input - 图片路径或 Buffer
@@ -15,22 +32,16 @@ export async function addWatermarkOnly(input, options, filePath) {
15
32
  const metadata = await instance.metadata();
16
33
 
17
34
  // 添加水印
18
- if (options.watermark) {
19
- // 文本水印
20
- if (options.watermark.mode === 'text' || (options.watermark.text && options.watermark.text.trim() !== '')) {
21
- const watermarkedBuffer = await addWatermark(instance, options.watermark, metadata);
22
- instance = sharp(watermarkedBuffer);
23
- }
24
- // 单个图片水印
25
- else if (options.watermark.mode === 'image' || (options.watermark.imagePath && options.watermark.mode !== 'tiled')) {
26
- const watermarkedBuffer = await addImageWatermark(instance, options.watermark);
27
- instance = sharp(watermarkedBuffer);
28
- }
29
- // 平铺图片水印
30
- else if (options.watermark.mode === 'tiled' || (options.watermark.imagePath && options.watermark.mode === 'tiled')) {
31
- const watermarkedBuffer = await addTiledImageWatermark(instance, options.watermark, metadata);
32
- instance = sharp(watermarkedBuffer);
33
- }
35
+ const watermarkMode = getWatermarkMode(options.watermark);
36
+ if (watermarkMode === 'text') {
37
+ const watermarkedBuffer = await addWatermark(instance, options.watermark, metadata);
38
+ instance = sharp(watermarkedBuffer);
39
+ } else if (watermarkMode === 'image') {
40
+ const watermarkedBuffer = await addImageWatermark(instance, options.watermark);
41
+ instance = sharp(watermarkedBuffer);
42
+ } else if (watermarkMode === 'tiled') {
43
+ const watermarkedBuffer = await addTiledImageWatermark(instance, options.watermark, metadata);
44
+ instance = sharp(watermarkedBuffer);
34
45
  }
35
46
 
36
47
  // 保持原始格式,不进行压缩
@@ -89,22 +100,16 @@ export async function compressImage(input, options, filePath) {
89
100
  const format = resizedMetadata.format;
90
101
 
91
102
  // 先添加水印(如果需要)
92
- if (options.watermark) {
93
- // 文本水印
94
- if (options.watermark.text && options.watermark.text.trim() !== '') {
95
- const watermarkedBuffer = await addWatermark(instance, options.watermark, resizedMetadata);
96
- instance = sharp(watermarkedBuffer);
97
- }
98
- // 单个图片水印
99
- else if (options.watermark.imagePath && options.watermark.mode !== 'tiled') {
100
- const watermarkedBuffer = await addImageWatermark(instance, options.watermark);
101
- instance = sharp(watermarkedBuffer);
102
- }
103
- // 平铺图片水印
104
- else if (options.watermark.imagePath && options.watermark.mode === 'tiled') {
105
- const watermarkedBuffer = await addTiledImageWatermark(instance, options.watermark, resizedMetadata);
106
- instance = sharp(watermarkedBuffer);
107
- }
103
+ const watermarkMode = getWatermarkMode(options.watermark);
104
+ if (watermarkMode === 'text') {
105
+ const watermarkedBuffer = await addWatermark(instance, options.watermark, resizedMetadata);
106
+ instance = sharp(watermarkedBuffer);
107
+ } else if (watermarkMode === 'image') {
108
+ const watermarkedBuffer = await addImageWatermark(instance, options.watermark);
109
+ instance = sharp(watermarkedBuffer);
110
+ } else if (watermarkMode === 'tiled') {
111
+ const watermarkedBuffer = await addTiledImageWatermark(instance, options.watermark, resizedMetadata);
112
+ instance = sharp(watermarkedBuffer);
108
113
  }
109
114
 
110
115
  // 再进行压缩
@@ -120,11 +125,7 @@ export async function compressImage(input, options, filePath) {
120
125
  instance = instance.webp(options.webpOptions || { quality: 88 });
121
126
  break;
122
127
  default:
123
- if (['jpeg', 'jpg', 'png', 'webp'].includes(format)) {
124
- // fallthrough
125
- } else {
126
- throw new Error(`Unsupported format: ${format}`);
127
- }
128
+ throw new Error(`Unsupported format: ${format}`);
128
129
  }
129
130
 
130
131
  // 如果需要保留元数据
@@ -2,6 +2,40 @@ import sharp from 'sharp';
2
2
  import path from 'path';
3
3
  import fs from 'fs-extra';
4
4
 
5
+ /**
6
+ * 应用透明度到图片(优化版:使用 sharp 内置方法)
7
+ * @param {Sharp} watermarkImage - sharp 实例
8
+ * @param {number} opacity - 透明度 (0-1)
9
+ * @returns {Promise<Sharp>} 处理后的 sharp 实例
10
+ */
11
+ async function applyOpacity(watermarkImage, opacity) {
12
+ if (opacity >= 1) return watermarkImage;
13
+
14
+ try {
15
+ const buffer = await watermarkImage.ensureAlpha().png().toBuffer();
16
+ const { data, info } = await sharp(buffer).raw().toBuffer({ resolveWithObject: true });
17
+
18
+ const channelCount = info.channels;
19
+ const alphaChannelIndex = channelCount - 1;
20
+ const length = data.length;
21
+
22
+ for (let i = alphaChannelIndex; i < length; i += channelCount) {
23
+ data[i] = (data[i] * opacity + 0.5) | 0;
24
+ }
25
+
26
+ return sharp(data, {
27
+ raw: {
28
+ width: info.width,
29
+ height: info.height,
30
+ channels: channelCount
31
+ }
32
+ });
33
+ } catch (error) {
34
+ console.warn('Failed to apply opacity, using original image:', error.message);
35
+ return watermarkImage;
36
+ }
37
+ }
38
+
5
39
  /**
6
40
  * 创建单个水印文本的 SVG
7
41
  * @param {string} text - 水印文本
@@ -11,7 +45,7 @@ import fs from 'fs-extra';
11
45
  * @param {number} options.fontSize - 字体大小
12
46
  * @returns {string} SVG 字符串
13
47
  */
14
- function createWatermarkSVG(text, options) {
48
+ export function createWatermarkSVG(text, options) {
15
49
  const { opacity, color, fontSize } = options;
16
50
  const rgbaColor = convertToRGBA(color, opacity);
17
51
 
@@ -37,7 +71,7 @@ function createWatermarkSVG(text, options) {
37
71
  * @param {number} opacity - 透明度
38
72
  * @returns {string} RGBA 颜色字符串
39
73
  */
40
- function convertToRGBA(color, opacity) {
74
+ export function convertToRGBA(color, opacity) {
41
75
  if (color.startsWith('rgba')) {
42
76
  return color.replace(/[\d.]+\)$/, `${opacity})`);
43
77
  }
@@ -218,25 +252,7 @@ export async function addImageWatermark(sharpInstance, options) {
218
252
  }
219
253
 
220
254
  if (opacity < 1) {
221
- try {
222
- const tempBuffer = await watermarkImage.ensureAlpha().png().toBuffer();
223
- const tempSharp = sharp(tempBuffer);
224
- const { data, info } = await tempSharp.raw().toBuffer({ resolveWithObject: true });
225
-
226
- for (let i = 3; i < data.length; i += 4) {
227
- data[i] = Math.floor(data[i] * opacity);
228
- }
229
-
230
- watermarkImage = sharp(data, {
231
- raw: {
232
- width: info.width,
233
- height: info.height,
234
- channels: 4
235
- }
236
- });
237
- } catch (error) {
238
- console.warn('Failed to apply opacity, using original image:', error.message);
239
- }
255
+ watermarkImage = await applyOpacity(watermarkImage, opacity);
240
256
  }
241
257
 
242
258
  const finalWatermarkBuffer = await watermarkImage.toBuffer();
@@ -343,25 +359,7 @@ export async function addTiledImageWatermark(sharpInstance, options, providedMet
343
359
  }
344
360
 
345
361
  if (opacity < 1) {
346
- try {
347
- const tempBuffer = await watermarkImage.ensureAlpha().png().toBuffer();
348
- const tempSharp = sharp(tempBuffer);
349
- const { data, info } = await tempSharp.raw().toBuffer({ resolveWithObject: true });
350
-
351
- for (let i = 3; i < data.length; i += 4) {
352
- data[i] = Math.floor(data[i] * opacity);
353
- }
354
-
355
- watermarkImage = sharp(data, {
356
- raw: {
357
- width: info.width,
358
- height: info.height,
359
- channels: 4
360
- }
361
- });
362
- } catch (error) {
363
- console.warn('Failed to apply opacity, using original image:', error.message);
364
- }
362
+ watermarkImage = await applyOpacity(watermarkImage, opacity);
365
363
  }
366
364
 
367
365
  if (angle !== 0) {
@@ -4,7 +4,7 @@ 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, watermarkOptions, resizeOptions) {
7
+ function generateCacheKey(content, watermarkOptions, resizeOptions) {
8
8
  const hash = crypto.createHash('sha256').update(content).digest('hex');
9
9
 
10
10
  let cacheKey = hash;
@@ -21,6 +21,11 @@ export async function getCache(content, watermarkOptions, resizeOptions) {
21
21
  cacheKey = `${cacheKey}-${resizeHash}`;
22
22
  }
23
23
 
24
+ return cacheKey;
25
+ }
26
+
27
+ export async function getCache(content, watermarkOptions, resizeOptions) {
28
+ const cacheKey = generateCacheKey(content, watermarkOptions, resizeOptions);
24
29
  const cachePath = path.join(CACHE_DIR, cacheKey);
25
30
 
26
31
  if (await fs.pathExists(cachePath)) {
@@ -30,22 +35,7 @@ export async function getCache(content, watermarkOptions, resizeOptions) {
30
35
  }
31
36
 
32
37
  export async function setCache(content, compressedContent, watermarkOptions, resizeOptions) {
33
- const hash = crypto.createHash('sha256').update(content).digest('hex');
34
-
35
- let cacheKey = hash;
36
- if (watermarkOptions && watermarkOptions.text) {
37
- const watermarkHash = crypto.createHash('sha256')
38
- .update(JSON.stringify(watermarkOptions))
39
- .digest('hex');
40
- cacheKey = `${cacheKey}-${watermarkHash}`;
41
- }
42
- if (resizeOptions) {
43
- const resizeHash = crypto.createHash('sha256')
44
- .update(JSON.stringify(resizeOptions))
45
- .digest('hex');
46
- cacheKey = `${cacheKey}-${resizeHash}`;
47
- }
48
-
38
+ const cacheKey = generateCacheKey(content, watermarkOptions, resizeOptions);
49
39
  const cachePath = path.join(CACHE_DIR, cacheKey);
50
40
 
51
41
  await fs.ensureDir(CACHE_DIR);