@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 +26 -0
- package/package.json +6 -3
- package/src/config/defaults.js +0 -1
- package/src/core/compressor.js +38 -37
- package/src/core/watermark.js +38 -40
- package/src/utils/cache.js +7 -17
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.
|
|
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": "
|
|
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
|
}
|
package/src/config/defaults.js
CHANGED
package/src/core/compressor.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
// 如果需要保留元数据
|
package/src/core/watermark.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/utils/cache.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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);
|