minify-pic-cli 1.2.0-beta.1 → 1.2.0-beta.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/index.js +127 -33
- package/lib/worker-pool.js +2 -2
- package/lib/worker.js +7 -5
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const ProgressDisplay = require("./lib/progress-display");
|
|
|
10
10
|
|
|
11
11
|
// 默认配置参数
|
|
12
12
|
const DEFAULT_CONFIG = {
|
|
13
|
-
|
|
13
|
+
targetDirs: [process.cwd()], // 需要压缩的目录数组(可包含子目录)
|
|
14
14
|
outputDir: path.join(process.cwd(), "output"), // 输出目录
|
|
15
15
|
quality: 80, // 压缩质量 0 - 100
|
|
16
16
|
gifColours: 128, // GIF调色板最大数量
|
|
@@ -18,20 +18,68 @@ const DEFAULT_CONFIG = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// 询问用户是否继续
|
|
21
|
-
function askUserToContinue() {
|
|
21
|
+
function askUserToContinue(dirStats) {
|
|
22
22
|
return new Promise((resolve) => {
|
|
23
23
|
const rl = readline.createInterface({
|
|
24
24
|
input: process.stdin,
|
|
25
25
|
output: process.stdout,
|
|
26
26
|
});
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
if (dirStats && dirStats.length > 0) {
|
|
29
|
+
console.log("\n检测到以下目录待压缩:");
|
|
30
|
+
dirStats.forEach((stat, index) => {
|
|
31
|
+
console.log(` ${index + 1}. ${stat.dir} (${stat.count} 张图片)`);
|
|
32
|
+
});
|
|
33
|
+
const totalCount = dirStats.reduce((sum, stat) => sum + stat.count, 0);
|
|
34
|
+
console.log(`总计: ${dirStats.length} 个目录, ${totalCount} 张图片\n`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`当前目录路径为: ${process.cwd()}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
rl.question("是否继续压缩? Y/N:", (answer) => {
|
|
29
40
|
rl.close();
|
|
30
41
|
resolve(answer.trim().toLowerCase() === "y");
|
|
31
42
|
});
|
|
32
43
|
});
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
// 验证目录有效性
|
|
47
|
+
function validateDirectories(dirs) {
|
|
48
|
+
const validDirs = [];
|
|
49
|
+
const invalidDirs = [];
|
|
50
|
+
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(dir)) {
|
|
54
|
+
console.warn(`警告: 目录不存在,跳过: ${dir}`);
|
|
55
|
+
invalidDirs.push({ dir, reason: '目录不存在' });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stat = fs.statSync(dir);
|
|
60
|
+
if (!stat.isDirectory()) {
|
|
61
|
+
console.warn(`警告: 路径不是目录,跳过: ${dir}`);
|
|
62
|
+
invalidDirs.push({ dir, reason: '不是目录' });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 检查读取权限
|
|
67
|
+
try {
|
|
68
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
69
|
+
validDirs.push(dir);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`错误: 目录无读取权限,跳过: ${dir}`);
|
|
72
|
+
invalidDirs.push({ dir, reason: '无读取权限' });
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`错误: 验证目录失败,跳过: ${dir}`, err.message);
|
|
76
|
+
invalidDirs.push({ dir, reason: err.message });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { validDirs, invalidDirs };
|
|
81
|
+
}
|
|
82
|
+
|
|
35
83
|
// 获取文件大小(带单位,自动转换MB/KB)
|
|
36
84
|
function getFileSizeWithUnit(filePath) {
|
|
37
85
|
const stats = fs.statSync(filePath);
|
|
@@ -49,7 +97,7 @@ function getFileSizeWithUnit(filePath) {
|
|
|
49
97
|
}
|
|
50
98
|
|
|
51
99
|
// 压缩单张图片
|
|
52
|
-
async function compressImage(filePath, config,
|
|
100
|
+
async function compressImage(filePath, config, sourceDir) {
|
|
53
101
|
const beforeSize = getFileSizeWithUnit(filePath);
|
|
54
102
|
sharp.cache(false);
|
|
55
103
|
|
|
@@ -84,8 +132,10 @@ async function compressImage(filePath, config, baseDir = config.targetDir) {
|
|
|
84
132
|
outputFileDir = path.dirname(outputFilePath);
|
|
85
133
|
} else {
|
|
86
134
|
// 保持目录结构输出到新目录
|
|
87
|
-
|
|
88
|
-
|
|
135
|
+
// 多目录支持: 使用sourceDir的basename作为一级子目录
|
|
136
|
+
const relativePath = path.relative(sourceDir, filePath);
|
|
137
|
+
const sourceDirBasename = path.basename(sourceDir);
|
|
138
|
+
outputFilePath = path.join(config.outputDir, sourceDirBasename, relativePath);
|
|
89
139
|
outputFileDir = path.dirname(outputFilePath);
|
|
90
140
|
}
|
|
91
141
|
|
|
@@ -121,7 +171,7 @@ async function compressImage(filePath, config, baseDir = config.targetDir) {
|
|
|
121
171
|
}
|
|
122
172
|
|
|
123
173
|
// 递归扫描目录,收集所有图片文件
|
|
124
|
-
function collectImageFiles(dir, config,
|
|
174
|
+
function collectImageFiles(dir, config, sourceDir) {
|
|
125
175
|
const files = fs.readdirSync(dir);
|
|
126
176
|
const imageFiles = [];
|
|
127
177
|
|
|
@@ -137,7 +187,7 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
|
|
|
137
187
|
|
|
138
188
|
if (stat.isDirectory()) {
|
|
139
189
|
if (!config.blackDirs.includes(file)) {
|
|
140
|
-
const subFiles = collectImageFiles(filePath, config,
|
|
190
|
+
const subFiles = collectImageFiles(filePath, config, sourceDir);
|
|
141
191
|
imageFiles.push(...subFiles);
|
|
142
192
|
} else {
|
|
143
193
|
console.log(`跳过的目录: ${filePath}`);
|
|
@@ -145,7 +195,8 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
|
|
|
145
195
|
} else if (/\.(png|jpe?g|gif)$/i.test(filePath)) {
|
|
146
196
|
imageFiles.push({
|
|
147
197
|
filePath,
|
|
148
|
-
size: stat.size
|
|
198
|
+
size: stat.size,
|
|
199
|
+
sourceDir: sourceDir // 记录所属源目录
|
|
149
200
|
});
|
|
150
201
|
}
|
|
151
202
|
}
|
|
@@ -153,8 +204,34 @@ function collectImageFiles(dir, config, baseDir = config.targetDir) {
|
|
|
153
204
|
return imageFiles;
|
|
154
205
|
}
|
|
155
206
|
|
|
207
|
+
// 收集多个目录的图片文件
|
|
208
|
+
function collectImageFilesFromMultipleDirs(dirs, config) {
|
|
209
|
+
const allImageFiles = [];
|
|
210
|
+
const dirStats = [];
|
|
211
|
+
|
|
212
|
+
for (const dir of dirs) {
|
|
213
|
+
console.log(`正在扫描目录: ${dir}`);
|
|
214
|
+
|
|
215
|
+
// 如果是非原地替换模式,跳过与输出目录相同的源目录
|
|
216
|
+
if (!config.replace && path.resolve(dir) === path.resolve(config.outputDir)) {
|
|
217
|
+
console.warn(`警告: 源目录与输出目录相同,跳过: ${dir}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const imageFiles = collectImageFiles(dir, config, dir);
|
|
222
|
+
allImageFiles.push(...imageFiles);
|
|
223
|
+
dirStats.push({
|
|
224
|
+
dir: dir,
|
|
225
|
+
count: imageFiles.length
|
|
226
|
+
});
|
|
227
|
+
console.log(` 找到 ${imageFiles.length} 张图片`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { allImageFiles, dirStats };
|
|
231
|
+
}
|
|
232
|
+
|
|
156
233
|
// 递归压缩目录下所有图片(单进程模式)
|
|
157
|
-
async function compressFiles(dir, config,
|
|
234
|
+
async function compressFiles(dir, config, sourceDir) {
|
|
158
235
|
const files = fs.readdirSync(dir);
|
|
159
236
|
|
|
160
237
|
for (const file of files) {
|
|
@@ -169,18 +246,18 @@ async function compressFiles(dir, config, baseDir = config.targetDir) {
|
|
|
169
246
|
|
|
170
247
|
if (stat.isDirectory()) {
|
|
171
248
|
if (!config.blackDirs.includes(file)) {
|
|
172
|
-
await compressFiles(filePath, config,
|
|
249
|
+
await compressFiles(filePath, config, sourceDir);
|
|
173
250
|
} else {
|
|
174
251
|
console.log(`跳过的目录: ${filePath}`);
|
|
175
252
|
}
|
|
176
253
|
} else if (/\.(png|jpe?g|gif)$/i.test(filePath)) {
|
|
177
|
-
await compressImage(filePath, config,
|
|
254
|
+
await compressImage(filePath, config, sourceDir);
|
|
178
255
|
}
|
|
179
256
|
}
|
|
180
257
|
}
|
|
181
258
|
|
|
182
259
|
// 多进程压缩
|
|
183
|
-
async function compressFilesParallel(imageFiles, config,
|
|
260
|
+
async function compressFilesParallel(imageFiles, config, setGlobalPool) {
|
|
184
261
|
const workerCount = WorkerPool.calculateWorkerCount(
|
|
185
262
|
imageFiles.length,
|
|
186
263
|
config.workers
|
|
@@ -201,10 +278,10 @@ async function compressFilesParallel(imageFiles, config, baseDir, setGlobalPool)
|
|
|
201
278
|
for (const fileInfo of imageFiles) {
|
|
202
279
|
try {
|
|
203
280
|
const beforeSize = fileInfo.size;
|
|
204
|
-
await compressImage(fileInfo.filePath, config,
|
|
281
|
+
await compressImage(fileInfo.filePath, config, fileInfo.sourceDir);
|
|
205
282
|
const afterSize = fs.statSync(
|
|
206
283
|
config.replace ? fileInfo.filePath :
|
|
207
|
-
path.join(config.outputDir, path.relative(
|
|
284
|
+
path.join(config.outputDir, path.basename(fileInfo.sourceDir), path.relative(fileInfo.sourceDir, fileInfo.filePath))
|
|
208
285
|
).size;
|
|
209
286
|
|
|
210
287
|
completed++;
|
|
@@ -252,7 +329,6 @@ async function compressFilesParallel(imageFiles, config, baseDir, setGlobalPool)
|
|
|
252
329
|
gifColours: config.gifColours,
|
|
253
330
|
outputDir: config.outputDir,
|
|
254
331
|
replace: config.replace,
|
|
255
|
-
baseDir: baseDir,
|
|
256
332
|
onProgress: (data) => {
|
|
257
333
|
progress.update(data);
|
|
258
334
|
}
|
|
@@ -294,7 +370,7 @@ const program = new Command();
|
|
|
294
370
|
program
|
|
295
371
|
.name("mpic")
|
|
296
372
|
.description("图片批量压缩工具")
|
|
297
|
-
.option("-d, --dir <dir>", "需要压缩的目录", DEFAULT_CONFIG.
|
|
373
|
+
.option("-d, --dir <dir>", "需要压缩的目录(逗号分隔多个目录)", val => val.split(",").map(d => d.trim()), DEFAULT_CONFIG.targetDirs)
|
|
298
374
|
.option("-o, --output <output>", "输出目录", DEFAULT_CONFIG.outputDir)
|
|
299
375
|
.option("-q, --quality <quality>", "压缩质量(0-100)", String(DEFAULT_CONFIG.quality))
|
|
300
376
|
.option("-g, --gif-colours <colours>", "GIF调色板最大数量(2-256)", String(DEFAULT_CONFIG.gifColours))
|
|
@@ -329,9 +405,14 @@ program
|
|
|
329
405
|
// 注册信号处理
|
|
330
406
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
331
407
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
332
|
-
|
|
408
|
+
|
|
409
|
+
// 合并配置 - 处理多目录
|
|
410
|
+
const inputDirs = Array.isArray(options.dir)
|
|
411
|
+
? options.dir.map(d => path.resolve(process.cwd(), d))
|
|
412
|
+
: [path.resolve(process.cwd(), options.dir)];
|
|
413
|
+
|
|
333
414
|
const config = {
|
|
334
|
-
|
|
415
|
+
targetDirs: inputDirs,
|
|
335
416
|
outputDir: options.replace ? null : path.resolve(process.cwd(), options.output),
|
|
336
417
|
quality: parseInt(options.quality, 10),
|
|
337
418
|
gifColours: parseInt(options.gifColours, 10),
|
|
@@ -340,29 +421,42 @@ program
|
|
|
340
421
|
workers: options.workers,
|
|
341
422
|
noParallel: options.noParallel || false,
|
|
342
423
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
424
|
+
|
|
425
|
+
// 验证目录
|
|
426
|
+
console.log("正在验证目录...");
|
|
427
|
+
const { validDirs, invalidDirs } = validateDirectories(config.targetDirs);
|
|
428
|
+
|
|
429
|
+
if (invalidDirs.length > 0) {
|
|
430
|
+
console.log(`\n跳过了 ${invalidDirs.length} 个无效目录`);
|
|
347
431
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
432
|
+
|
|
433
|
+
if (validDirs.length === 0) {
|
|
434
|
+
console.error("错误: 所有目录都无效,程序退出");
|
|
435
|
+
process.exit(1);
|
|
351
436
|
}
|
|
352
|
-
|
|
353
|
-
|
|
437
|
+
|
|
438
|
+
console.log(`\n有效目录: ${validDirs.length} 个`);
|
|
354
439
|
|
|
355
440
|
// 收集所有图片文件
|
|
356
|
-
console.log("正在扫描图片文件...");
|
|
357
|
-
const
|
|
441
|
+
console.log("\n正在扫描图片文件...");
|
|
442
|
+
const { allImageFiles, dirStats } = collectImageFilesFromMultipleDirs(validDirs, config);
|
|
358
443
|
|
|
359
|
-
if (
|
|
444
|
+
if (allImageFiles.length === 0) {
|
|
360
445
|
console.log("未找到需要压缩的图片文件");
|
|
361
446
|
process.exit(0);
|
|
362
447
|
}
|
|
363
448
|
|
|
449
|
+
let shouldContinue = options.yes;
|
|
450
|
+
if (!shouldContinue) {
|
|
451
|
+
shouldContinue = await askUserToContinue(dirStats);
|
|
452
|
+
}
|
|
453
|
+
if (!shouldContinue) {
|
|
454
|
+
console.log("已取消操作。");
|
|
455
|
+
process.exit(0);
|
|
456
|
+
}
|
|
457
|
+
|
|
364
458
|
try {
|
|
365
|
-
await compressFilesParallel(
|
|
459
|
+
await compressFilesParallel(allImageFiles, config, (pool) => {
|
|
366
460
|
globalPool = pool;
|
|
367
461
|
});
|
|
368
462
|
|
package/lib/worker-pool.js
CHANGED
|
@@ -250,13 +250,13 @@ class WorkerPool {
|
|
|
250
250
|
type: "task",
|
|
251
251
|
taskId,
|
|
252
252
|
filePath: task.filePath,
|
|
253
|
+
sourceDir: task.sourceDir,
|
|
253
254
|
config: {
|
|
254
255
|
quality: this.config.quality,
|
|
255
256
|
gifColours: this.config.gifColours,
|
|
256
257
|
outputDir: this.config.outputDir,
|
|
257
258
|
replace: this.config.replace
|
|
258
|
-
}
|
|
259
|
-
baseDir: this.config.baseDir
|
|
259
|
+
}
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
package/lib/worker.js
CHANGED
|
@@ -13,7 +13,7 @@ function getFileSize(filePath) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// 压缩单张图片
|
|
16
|
-
async function compressImage(filePath, config,
|
|
16
|
+
async function compressImage(filePath, config, sourceDir) {
|
|
17
17
|
const beforeSize = getFileSize(filePath);
|
|
18
18
|
const startTime = Date.now();
|
|
19
19
|
|
|
@@ -50,8 +50,10 @@ async function compressImage(filePath, config, baseDir) {
|
|
|
50
50
|
outputFileDir = path.dirname(outputFilePath);
|
|
51
51
|
} else {
|
|
52
52
|
// 保持目录结构输出到新目录
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// 多目录支持: 使用sourceDir的basename作为一级子目录
|
|
54
|
+
const relativePath = path.relative(sourceDir, filePath);
|
|
55
|
+
const sourceDirBasename = path.basename(sourceDir);
|
|
56
|
+
outputFilePath = path.join(config.outputDir, sourceDirBasename, relativePath);
|
|
55
57
|
outputFileDir = path.dirname(outputFilePath);
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -85,10 +87,10 @@ async function compressImage(filePath, config, baseDir) {
|
|
|
85
87
|
// 工作进程消息处理
|
|
86
88
|
process.on("message", async (message) => {
|
|
87
89
|
if (message.type === "task") {
|
|
88
|
-
const { taskId, filePath, config,
|
|
90
|
+
const { taskId, filePath, config, sourceDir } = message;
|
|
89
91
|
|
|
90
92
|
try {
|
|
91
|
-
const result = await compressImage(filePath, config,
|
|
93
|
+
const result = await compressImage(filePath, config, sourceDir);
|
|
92
94
|
|
|
93
95
|
process.send({
|
|
94
96
|
type: "success",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minify-pic-cli",
|
|
3
|
-
"version": "1.2.0-beta.
|
|
3
|
+
"version": "1.2.0-beta.2",
|
|
4
4
|
"description": "一个简单易用的图片批量压缩命令行工具,支持 PNG、JPEG、GIF 格式,适合前端和设计师快速优化图片体积。",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "node ./index.js",
|
|
16
|
-
"test": "mpic -d ./test/src -o ./test/output -y"
|
|
16
|
+
"test": "mpic -d ./test/src/assets,./test/src/imgs -o ./test/output -y -w 2"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"图片压缩",
|