@zhaoshijun/compress 1.1.5 → 1.2.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 +22 -0
- package/package.json +1 -1
- package/src/config/defaults.js +12 -1
- package/src/core/compressor.js +17 -4
- package/src/core/watermark.js +245 -0
package/bin/compress.js
CHANGED
|
@@ -33,6 +33,17 @@ program
|
|
|
33
33
|
.option('--watermark-opacity <opacity>', '水印透明度 (0-1)', parseFloat)
|
|
34
34
|
.option('--watermark-density <density>', '水印密度 (1-10)', parseInt)
|
|
35
35
|
.option('--watermark-color <color>', '水印颜色 (hex 或 rgba)')
|
|
36
|
+
.option('--watermark-mode <mode>', '水印模式 (text/image/tiled)')
|
|
37
|
+
.option('--watermark-image <path>', '水印图片路径')
|
|
38
|
+
.option('--watermark-x <x>', '水印 X 坐标', parseInt)
|
|
39
|
+
.option('--watermark-y <y>', '水印 Y 坐标', parseInt)
|
|
40
|
+
.option('--watermark-width <width>', '水印宽度', parseInt)
|
|
41
|
+
.option('--watermark-height <height>', '水印高度', parseInt)
|
|
42
|
+
.option('--watermark-position <position>', '水印位置 (top-left/top-right/bottom-left/bottom-right/center)')
|
|
43
|
+
.option('--watermark-padding <padding>', '水印边距', parseInt)
|
|
44
|
+
.option('--watermark-angle <angle>', '水印倾斜角度', parseInt)
|
|
45
|
+
.option('--watermark-spacing-x <spacingX>', '水印水平间隔', parseInt)
|
|
46
|
+
.option('--watermark-spacing-y <spacingY>', '水印垂直间隔', parseInt)
|
|
36
47
|
.action(async (options) => {
|
|
37
48
|
try {
|
|
38
49
|
const startTime = Date.now();
|
|
@@ -50,6 +61,17 @@ program
|
|
|
50
61
|
if (options.watermarkOpacity !== undefined) config.watermark.opacity = options.watermarkOpacity;
|
|
51
62
|
if (options.watermarkDensity !== undefined) config.watermark.density = options.watermarkDensity;
|
|
52
63
|
if (options.watermarkColor) config.watermark.color = options.watermarkColor;
|
|
64
|
+
if (options.watermarkMode) config.watermark.mode = options.watermarkMode;
|
|
65
|
+
if (options.watermarkImage) config.watermark.imagePath = options.watermarkImage;
|
|
66
|
+
if (options.watermarkX !== undefined) config.watermark.x = options.watermarkX;
|
|
67
|
+
if (options.watermarkY !== undefined) config.watermark.y = options.watermarkY;
|
|
68
|
+
if (options.watermarkWidth !== undefined) config.watermark.width = options.watermarkWidth;
|
|
69
|
+
if (options.watermarkHeight !== undefined) config.watermark.height = options.watermarkHeight;
|
|
70
|
+
if (options.watermarkPosition) config.watermark.position = options.watermarkPosition;
|
|
71
|
+
if (options.watermarkPadding !== undefined) config.watermark.padding = options.watermarkPadding;
|
|
72
|
+
if (options.watermarkAngle !== undefined) config.watermark.angle = options.watermarkAngle;
|
|
73
|
+
if (options.watermarkSpacingX !== undefined) config.watermark.spacingX = options.watermarkSpacingX;
|
|
74
|
+
if (options.watermarkSpacingY !== undefined) config.watermark.spacingY = options.watermarkSpacingY;
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
// 确定输入源
|
package/package.json
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -28,11 +28,22 @@ export const defaultConfig = {
|
|
|
28
28
|
suffix: '.min',
|
|
29
29
|
backup: false,
|
|
30
30
|
watermark: {
|
|
31
|
+
mode: 'text',
|
|
31
32
|
text: '',
|
|
32
33
|
opacity: 0.5,
|
|
33
34
|
density: 3,
|
|
34
35
|
color: '#ffffff',
|
|
35
|
-
fontSize: 24
|
|
36
|
+
fontSize: 24,
|
|
37
|
+
imagePath: '',
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
width: null,
|
|
41
|
+
height: null,
|
|
42
|
+
position: 'top-left',
|
|
43
|
+
padding: 10,
|
|
44
|
+
angle: -30,
|
|
45
|
+
spacingX: 100,
|
|
46
|
+
spacingY: 100
|
|
36
47
|
},
|
|
37
48
|
vite: {
|
|
38
49
|
cache: true
|
package/src/core/compressor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { addWatermark } from './watermark.js';
|
|
3
|
+
import { addWatermark, addImageWatermark, addTiledImageWatermark } from './watermark.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 压缩图片
|
|
@@ -38,9 +38,22 @@ export async function compressImage(input, options, filePath) {
|
|
|
38
38
|
const format = resizedMetadata.format;
|
|
39
39
|
|
|
40
40
|
// 先添加水印(如果需要)
|
|
41
|
-
if (options.watermark
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (options.watermark) {
|
|
42
|
+
// 文本水印
|
|
43
|
+
if (options.watermark.text && options.watermark.text.trim() !== '') {
|
|
44
|
+
const watermarkedBuffer = await addWatermark(instance, options.watermark, resizedMetadata);
|
|
45
|
+
instance = sharp(watermarkedBuffer);
|
|
46
|
+
}
|
|
47
|
+
// 单个图片水印
|
|
48
|
+
else if (options.watermark.imagePath && options.watermark.mode !== 'tiled') {
|
|
49
|
+
const watermarkedBuffer = await addImageWatermark(instance, options.watermark);
|
|
50
|
+
instance = sharp(watermarkedBuffer);
|
|
51
|
+
}
|
|
52
|
+
// 平铺图片水印
|
|
53
|
+
else if (options.watermark.imagePath && options.watermark.mode === 'tiled') {
|
|
54
|
+
const watermarkedBuffer = await addTiledImageWatermark(instance, options.watermark, resizedMetadata);
|
|
55
|
+
instance = sharp(watermarkedBuffer);
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
// 再进行压缩
|
package/src/core/watermark.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* 创建单个水印文本的 SVG
|
|
@@ -155,3 +157,246 @@ export async function addWatermark(sharpInstance, watermarkOptions, providedMeta
|
|
|
155
157
|
|
|
156
158
|
return result;
|
|
157
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 添加单个图片水印
|
|
163
|
+
* @param {Sharp} sharpInstance - sharp 实例
|
|
164
|
+
* @param {Object} options - 图片水印配置
|
|
165
|
+
* @param {string} options.imagePath - 水印图片路径(绝对或相对路径)
|
|
166
|
+
* @param {number} [options.x=0] - 水印 X 坐标
|
|
167
|
+
* @param {number} [options.y=0] - 水印 Y 坐标
|
|
168
|
+
* @param {number} [options.width] - 水印宽度(不设置则保持原尺寸)
|
|
169
|
+
* @param {number} [options.height] - 水印高度(不设置则保持原尺寸)
|
|
170
|
+
* @param {number} [options.opacity=1] - 水印透明度 (0-1)
|
|
171
|
+
* @param {string} [options.position='top-left'] - 水印位置(top-left, top-right, bottom-left, bottom-right, center)
|
|
172
|
+
* @param {number} [options.padding=10] - 边距(当使用 position 时生效)
|
|
173
|
+
* @returns {Promise<Buffer>} 添加水印后的图片 Buffer
|
|
174
|
+
*/
|
|
175
|
+
export async function addImageWatermark(sharpInstance, options) {
|
|
176
|
+
const {
|
|
177
|
+
imagePath,
|
|
178
|
+
x = 0,
|
|
179
|
+
y = 0,
|
|
180
|
+
width,
|
|
181
|
+
height,
|
|
182
|
+
opacity = 1,
|
|
183
|
+
position = 'top-left',
|
|
184
|
+
padding = 10
|
|
185
|
+
} = options;
|
|
186
|
+
|
|
187
|
+
if (!imagePath) {
|
|
188
|
+
throw new Error('Watermark image path is required');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (opacity < 0 || opacity > 1) {
|
|
192
|
+
throw new Error('Opacity must be between 0 and 1');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const targetMetadata = await sharpInstance.metadata();
|
|
196
|
+
|
|
197
|
+
let watermarkPath = imagePath;
|
|
198
|
+
|
|
199
|
+
if (!path.isAbsolute(imagePath)) {
|
|
200
|
+
watermarkPath = path.resolve(process.cwd(), imagePath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!await fs.pathExists(watermarkPath)) {
|
|
204
|
+
throw new Error(`Watermark image not found: ${watermarkPath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let watermarkImage = sharp(watermarkPath);
|
|
208
|
+
const watermarkMeta = await watermarkImage.metadata();
|
|
209
|
+
|
|
210
|
+
if (width || height) {
|
|
211
|
+
watermarkImage = watermarkImage.resize({
|
|
212
|
+
width: width || undefined,
|
|
213
|
+
height: height || undefined,
|
|
214
|
+
fit: 'inside'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opacity < 1) {
|
|
219
|
+
const watermarkBuffer = await watermarkImage.toBuffer();
|
|
220
|
+
watermarkImage = sharp(watermarkBuffer).ensureAlpha();
|
|
221
|
+
const { data } = await watermarkImage.raw().toBuffer({ resolveWithObject: true });
|
|
222
|
+
|
|
223
|
+
for (let i = 3; i < data.length; i += 4) {
|
|
224
|
+
data[i] = Math.floor(data[i] * opacity);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
watermarkImage = sharp(data, {
|
|
228
|
+
raw: {
|
|
229
|
+
width: watermarkMeta.width,
|
|
230
|
+
height: watermarkMeta.height,
|
|
231
|
+
channels: 4
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const watermarkBuffer = await watermarkImage.toBuffer();
|
|
237
|
+
const finalWatermarkMeta = await sharp(watermarkBuffer).metadata();
|
|
238
|
+
|
|
239
|
+
let compositeX = x;
|
|
240
|
+
let compositeY = y;
|
|
241
|
+
|
|
242
|
+
if (position !== 'custom') {
|
|
243
|
+
switch (position) {
|
|
244
|
+
case 'top-left':
|
|
245
|
+
compositeX = padding;
|
|
246
|
+
compositeY = padding;
|
|
247
|
+
break;
|
|
248
|
+
case 'top-right':
|
|
249
|
+
compositeX = targetMetadata.width - finalWatermarkMeta.width - padding;
|
|
250
|
+
compositeY = padding;
|
|
251
|
+
break;
|
|
252
|
+
case 'bottom-left':
|
|
253
|
+
compositeX = padding;
|
|
254
|
+
compositeY = targetMetadata.height - finalWatermarkMeta.height - padding;
|
|
255
|
+
break;
|
|
256
|
+
case 'bottom-right':
|
|
257
|
+
compositeX = targetMetadata.width - finalWatermarkMeta.width - padding;
|
|
258
|
+
compositeY = targetMetadata.height - finalWatermarkMeta.height - padding;
|
|
259
|
+
break;
|
|
260
|
+
case 'center':
|
|
261
|
+
compositeX = Math.floor((targetMetadata.width - finalWatermarkMeta.width) / 2);
|
|
262
|
+
compositeY = Math.floor((targetMetadata.height - finalWatermarkMeta.height) / 2);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result = await sharpInstance
|
|
268
|
+
.composite([{
|
|
269
|
+
input: watermarkBuffer,
|
|
270
|
+
left: compositeX,
|
|
271
|
+
top: compositeY,
|
|
272
|
+
blend: 'over'
|
|
273
|
+
}])
|
|
274
|
+
.toBuffer();
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 添加平铺图片水印
|
|
281
|
+
* @param {Sharp} sharpInstance - sharp 实例
|
|
282
|
+
* @param {Object} options - 平铺图片水印配置
|
|
283
|
+
* @param {string} options.imagePath - 水印图片路径(绝对或相对路径)
|
|
284
|
+
* @param {number} [options.width] - 水印宽度(不设置则保持原尺寸)
|
|
285
|
+
* @param {number} [options.height] - 水印高度(不设置则保持原尺寸)
|
|
286
|
+
* @param {number} [options.opacity=0.5] - 水印透明度 (0-1)
|
|
287
|
+
* @param {number} [options.angle=-30] - 水印倾斜角度(度)
|
|
288
|
+
* @param {number} [options.spacingX=100] - 水印水平间隔
|
|
289
|
+
* @param {number} [options.spacingY=100] - 水印垂直间隔
|
|
290
|
+
* @param {Object} [providedMetadata] - 提供的 metadata(避免重复获取)
|
|
291
|
+
* @returns {Promise<Buffer>} 添加水印后的图片 Buffer
|
|
292
|
+
*/
|
|
293
|
+
export async function addTiledImageWatermark(sharpInstance, options, providedMetadata) {
|
|
294
|
+
const {
|
|
295
|
+
imagePath,
|
|
296
|
+
width,
|
|
297
|
+
height,
|
|
298
|
+
opacity = 0.5,
|
|
299
|
+
angle = -30,
|
|
300
|
+
spacingX = 100,
|
|
301
|
+
spacingY = 100
|
|
302
|
+
} = options;
|
|
303
|
+
|
|
304
|
+
if (!imagePath) {
|
|
305
|
+
throw new Error('Watermark image path is required');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (opacity < 0 || opacity > 1) {
|
|
309
|
+
throw new Error('Opacity must be between 0 and 1');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const metadata = providedMetadata || await sharpInstance.metadata();
|
|
313
|
+
|
|
314
|
+
let watermarkPath = imagePath;
|
|
315
|
+
|
|
316
|
+
if (!path.isAbsolute(imagePath)) {
|
|
317
|
+
watermarkPath = path.resolve(process.cwd(), imagePath);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!await fs.pathExists(watermarkPath)) {
|
|
321
|
+
throw new Error(`Watermark image not found: ${watermarkPath}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let watermarkImage = sharp(watermarkPath);
|
|
325
|
+
const watermarkMeta = await watermarkImage.metadata();
|
|
326
|
+
|
|
327
|
+
if (width || height) {
|
|
328
|
+
watermarkImage = watermarkImage.resize({
|
|
329
|
+
width: width || undefined,
|
|
330
|
+
height: height || undefined,
|
|
331
|
+
fit: 'inside'
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (opacity < 1) {
|
|
336
|
+
const watermarkBuffer = await watermarkImage.toBuffer();
|
|
337
|
+
watermarkImage = sharp(watermarkBuffer).ensureAlpha();
|
|
338
|
+
const { data, info } = await watermarkImage.raw().toBuffer({ resolveWithObject: true });
|
|
339
|
+
|
|
340
|
+
for (let i = 3; i < data.length; i += 4) {
|
|
341
|
+
data[i] = Math.floor(data[i] * opacity);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
watermarkImage = sharp(data, {
|
|
345
|
+
raw: {
|
|
346
|
+
width: info.width,
|
|
347
|
+
height: info.height,
|
|
348
|
+
channels: 4
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (angle !== 0) {
|
|
354
|
+
const watermarkBuffer = await watermarkImage.toBuffer();
|
|
355
|
+
const rotatedMeta = await sharp(watermarkBuffer).metadata();
|
|
356
|
+
const radians = angle * Math.PI / 180;
|
|
357
|
+
const cos = Math.abs(Math.cos(radians));
|
|
358
|
+
const sin = Math.abs(Math.sin(radians));
|
|
359
|
+
const newWidth = Math.ceil(rotatedMeta.width * cos + rotatedMeta.height * sin);
|
|
360
|
+
const newHeight = Math.ceil(rotatedMeta.width * sin + rotatedMeta.height * cos);
|
|
361
|
+
|
|
362
|
+
watermarkImage = sharp(watermarkBuffer).rotate(angle, {
|
|
363
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
364
|
+
}).resize(newWidth, newHeight);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const watermarkBuffer = await watermarkImage.toBuffer();
|
|
368
|
+
const finalWatermarkMeta = await sharp(watermarkBuffer).metadata();
|
|
369
|
+
|
|
370
|
+
const cols = Math.ceil(metadata.width / spacingX) + 1;
|
|
371
|
+
const rows = Math.ceil(metadata.height / spacingY) + 1;
|
|
372
|
+
|
|
373
|
+
const composites = [];
|
|
374
|
+
for (let row = 0; row < rows; row++) {
|
|
375
|
+
for (let col = 0; col < cols; col++) {
|
|
376
|
+
composites.push({
|
|
377
|
+
input: watermarkBuffer,
|
|
378
|
+
left: col * spacingX,
|
|
379
|
+
top: row * spacingY
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const tiledWatermark = await sharp({
|
|
385
|
+
create: {
|
|
386
|
+
width: cols * spacingX + finalWatermarkMeta.width,
|
|
387
|
+
height: rows * spacingY + finalWatermarkMeta.height,
|
|
388
|
+
channels: 4,
|
|
389
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
.composite(composites)
|
|
393
|
+
.extract({ left: 0, top: 0, width: metadata.width, height: metadata.height })
|
|
394
|
+
.png()
|
|
395
|
+
.toBuffer();
|
|
396
|
+
|
|
397
|
+
const result = await sharpInstance
|
|
398
|
+
.composite([{ input: tiledWatermark, blend: 'over' }])
|
|
399
|
+
.toBuffer();
|
|
400
|
+
|
|
401
|
+
return result;
|
|
402
|
+
}
|