td-web-cli 0.1.28 → 0.1.30
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/dist/index.js +94 -46
- package/dist/modules/image/compressImage/index.d.ts +11 -0
- package/dist/modules/image/compressImage/index.d.ts.map +1 -0
- package/dist/modules/image/compressImage/index.js +320 -0
- package/dist/modules/image/index.d.ts +8 -0
- package/dist/modules/image/index.d.ts.map +1 -0
- package/dist/modules/image/index.js +55 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -4,66 +4,114 @@
|
|
|
4
4
|
* 通过交互式选择执行不同模块功能
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
+
import fs from 'fs';
|
|
7
8
|
import { select, Separator } from '@inquirer/prompts';
|
|
8
9
|
import { logger, loggerError } from './utils/index.js';
|
|
9
10
|
import { i18n } from './modules/i18n/index.js';
|
|
11
|
+
import { image } from './modules/image/index.js';
|
|
10
12
|
import { tools } from './modules/tools/index.js';
|
|
13
|
+
// 读取 package.json 中的版本号
|
|
14
|
+
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
11
15
|
const program = new Command();
|
|
16
|
+
/**
|
|
17
|
+
* 设置基础命令行选项
|
|
18
|
+
*/
|
|
19
|
+
function setupBasicOptions() {
|
|
20
|
+
program
|
|
21
|
+
.version(version, '-v, --version', '显示版本号')
|
|
22
|
+
.description('td-web-cli 命令行工具')
|
|
23
|
+
.usage('[options]')
|
|
24
|
+
.helpOption('-h, --help', '显示帮助信息')
|
|
25
|
+
.addHelpText('after', `
|
|
26
|
+
示例:
|
|
27
|
+
$ td-web-cli # 进入交互式选择模块
|
|
28
|
+
$ td-web-cli -v # 显示版本号
|
|
29
|
+
$ td-web-cli -h # 显示帮助信息
|
|
30
|
+
`.trim());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 交互式选择模块并执行
|
|
34
|
+
*/
|
|
35
|
+
async function runInteractiveMode() {
|
|
36
|
+
// 定义可用模块选项
|
|
37
|
+
const moduleChoices = [
|
|
38
|
+
{
|
|
39
|
+
name: '国际化',
|
|
40
|
+
value: 'i18n',
|
|
41
|
+
description: '国际化相关功能',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: '图片',
|
|
45
|
+
value: 'image',
|
|
46
|
+
description: '图片相关功能',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: '小工具',
|
|
50
|
+
value: 'tools',
|
|
51
|
+
description: '小工具相关功能',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
// 交互式选择模块
|
|
55
|
+
const answer = await select({
|
|
56
|
+
message: '请选择要执行的模块:',
|
|
57
|
+
choices: [
|
|
58
|
+
...moduleChoices,
|
|
59
|
+
new Separator(), // 分割线,便于未来扩展更多模块
|
|
60
|
+
],
|
|
61
|
+
default: 'i18n', // 默认选项
|
|
62
|
+
loop: true, // 选项循环滚动
|
|
63
|
+
});
|
|
64
|
+
// 查找选择模块的名称,方便日志输出
|
|
65
|
+
const selectedModule = moduleChoices.find((item) => item.value === answer);
|
|
66
|
+
if (!selectedModule) {
|
|
67
|
+
logger.warn('未选择有效模块,程序已退出');
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
logger.info(`用户选择模块:${selectedModule.name}`);
|
|
71
|
+
// 根据选择执行对应模块
|
|
72
|
+
switch (answer) {
|
|
73
|
+
case 'i18n':
|
|
74
|
+
logger.info(`${selectedModule.name}模块开始执行`);
|
|
75
|
+
await i18n(program);
|
|
76
|
+
logger.info(`${selectedModule.name}模块执行完成`);
|
|
77
|
+
break;
|
|
78
|
+
case 'image':
|
|
79
|
+
logger.info(`${selectedModule.name}模块开始执行`);
|
|
80
|
+
await image(program);
|
|
81
|
+
logger.info(`${selectedModule.name}模块执行完成`);
|
|
82
|
+
break;
|
|
83
|
+
case 'tools':
|
|
84
|
+
logger.info(`${selectedModule.name}模块开始执行`);
|
|
85
|
+
await tools(program);
|
|
86
|
+
logger.info(`${selectedModule.name}模块执行完成`);
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
logger.warn(`${selectedModule.name}模块暂未实现,程序已退出`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
12
93
|
/**
|
|
13
94
|
* 主程序入口函数
|
|
14
|
-
*
|
|
95
|
+
* 解析命令行参数,若未提供任何选项则进入交互式选择
|
|
15
96
|
*/
|
|
16
97
|
async function main() {
|
|
17
98
|
try {
|
|
18
99
|
logger.info('td-web-cli程序启动');
|
|
19
|
-
//
|
|
100
|
+
// 设置基础命令行选项
|
|
101
|
+
setupBasicOptions();
|
|
102
|
+
// 解析命令行参数(如果用户提供了 -v 或 -h,commander 会自动退出)
|
|
20
103
|
program.parse(process.argv);
|
|
21
104
|
logger.info(`命令行参数解析完成:${process.argv.slice(2).join(' ')}`);
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
description: '国际化相关功能',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: '小工具',
|
|
31
|
-
value: 'tools',
|
|
32
|
-
description: '小工具相关功能',
|
|
33
|
-
},
|
|
34
|
-
];
|
|
35
|
-
// 交互式选择模块
|
|
36
|
-
const answer = await select({
|
|
37
|
-
message: '请选择要执行的模块:',
|
|
38
|
-
choices: [
|
|
39
|
-
...moduleChoices,
|
|
40
|
-
new Separator(), // 分割线,便于未来扩展更多模块
|
|
41
|
-
],
|
|
42
|
-
default: 'i18n', // 默认选项
|
|
43
|
-
loop: true, // 选项循环滚动
|
|
44
|
-
});
|
|
45
|
-
// 查找选择模块的名称,方便日志输出
|
|
46
|
-
const selectedModule = moduleChoices.find((item) => item.value === answer);
|
|
47
|
-
if (!selectedModule) {
|
|
48
|
-
logger.warn('未选择有效模块,程序已退出');
|
|
49
|
-
process.exit(0);
|
|
105
|
+
// 如果没有其他参数(如 -v、-h 等),则进入交互模式
|
|
106
|
+
// 注意:program.args 包含未被选项消费的参数,若无额外参数则执行交互
|
|
107
|
+
if (program.args.length === 0) {
|
|
108
|
+
logger.info('未检测到额外参数,进入交互式选择模式');
|
|
109
|
+
await runInteractiveMode();
|
|
50
110
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
logger.info(`${selectedModule.name}模块开始执行`);
|
|
56
|
-
await i18n(program);
|
|
57
|
-
logger.info(`${selectedModule.name}模块执行完成`);
|
|
58
|
-
break;
|
|
59
|
-
case 'tools':
|
|
60
|
-
logger.info(`${selectedModule.name}模块开始执行`);
|
|
61
|
-
await tools(program);
|
|
62
|
-
logger.info(`${selectedModule.name}模块执行完成`);
|
|
63
|
-
break;
|
|
64
|
-
default:
|
|
65
|
-
logger.warn(`${selectedModule.name}模块暂未实现,程序已退出`);
|
|
66
|
-
process.exit(0);
|
|
111
|
+
else {
|
|
112
|
+
// 如果有额外参数,可能用户直接传入了子命令,但当前未实现子命令,提示帮助
|
|
113
|
+
logger.warn(`未知参数:${program.args.join(' ')},请使用 --help 查看用法`, true);
|
|
114
|
+
program.help(); // 显示帮助并退出
|
|
67
115
|
}
|
|
68
116
|
}
|
|
69
117
|
catch (error) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* 图片压缩主功能
|
|
4
|
+
* - 交互式输入图片路径
|
|
5
|
+
* - 选择压缩级别(无损/视觉无损/有损/高损)
|
|
6
|
+
* - 询问是否自动缩放超大图片
|
|
7
|
+
* - 自动识别格式并使用对应压缩参数
|
|
8
|
+
* - 输出文件添加6位随机后缀,保存在原目录
|
|
9
|
+
*/
|
|
10
|
+
export declare function compressImage(program: Command): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/image/compressImage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwCpC;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,iBA6SnD"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { input, select, confirm, Separator } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { logger, loggerError, normalizeGitBashPath, } from '../../../utils/index.js';
|
|
6
|
+
// 支持处理的图片格式
|
|
7
|
+
const SUPPORTED_FORMATS = [
|
|
8
|
+
'.jpg',
|
|
9
|
+
'.jpeg',
|
|
10
|
+
'.png',
|
|
11
|
+
'.webp',
|
|
12
|
+
'.avif',
|
|
13
|
+
'.tiff',
|
|
14
|
+
'.gif',
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* 生成6位随机字符串(用于文件名后缀)
|
|
18
|
+
*/
|
|
19
|
+
function generateShortSuffix() {
|
|
20
|
+
return Math.random().toString(36).substring(2, 8);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 获取文件的输出路径(同目录,原文件名+6位随机字符串+扩展名)
|
|
24
|
+
*/
|
|
25
|
+
function getOutputPath(inputPath) {
|
|
26
|
+
const dir = path.dirname(inputPath);
|
|
27
|
+
const ext = path.extname(inputPath);
|
|
28
|
+
const baseName = path.basename(inputPath, ext);
|
|
29
|
+
const suffix = generateShortSuffix();
|
|
30
|
+
return path.join(dir, `${baseName}_${suffix}${ext}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 图片压缩主功能
|
|
34
|
+
* - 交互式输入图片路径
|
|
35
|
+
* - 选择压缩级别(无损/视觉无损/有损/高损)
|
|
36
|
+
* - 询问是否自动缩放超大图片
|
|
37
|
+
* - 自动识别格式并使用对应压缩参数
|
|
38
|
+
* - 输出文件添加6位随机后缀,保存在原目录
|
|
39
|
+
*/
|
|
40
|
+
export async function compressImage(program) {
|
|
41
|
+
var _a;
|
|
42
|
+
// 交互式输入图片路径并校验
|
|
43
|
+
const answer = await input({
|
|
44
|
+
message: '请输入图片文件路径:',
|
|
45
|
+
validate: (value) => {
|
|
46
|
+
const cleaned = value.trim().replace(/^['"]|['"]$/g, '');
|
|
47
|
+
if (cleaned.length === 0) {
|
|
48
|
+
return '路径不能为空';
|
|
49
|
+
}
|
|
50
|
+
const normalizedPath = normalizeGitBashPath(cleaned);
|
|
51
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
52
|
+
return '文件不存在,请输入有效路径';
|
|
53
|
+
}
|
|
54
|
+
const ext = path.extname(normalizedPath).toLowerCase();
|
|
55
|
+
if (!SUPPORTED_FORMATS.includes(ext)) {
|
|
56
|
+
return `不支持的文件格式,支持的格式:${SUPPORTED_FORMATS.join(', ')}`;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const imagePath = normalizeGitBashPath(answer);
|
|
62
|
+
const moduleChoices = [
|
|
63
|
+
{
|
|
64
|
+
name: '无损压缩',
|
|
65
|
+
description: '完全保留原始画质,体积减小极少(5%~15%)',
|
|
66
|
+
value: 'lossless',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: '视觉无损压缩',
|
|
70
|
+
description: '肉眼几乎无法察觉差异,体积减小明显(20%~50%)',
|
|
71
|
+
value: 'visually_lossless',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: '有损压缩',
|
|
75
|
+
description: '画质轻微下降,体积大幅减小(40%~70%)',
|
|
76
|
+
value: 'lossy',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: '高损压缩',
|
|
80
|
+
description: '画质明显下降,体积最小(60%~90%)',
|
|
81
|
+
value: 'high_lossy',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
// 选择压缩类型(四种级别,带详细描述)
|
|
85
|
+
const compressType = await select({
|
|
86
|
+
message: '请选择压缩级别:',
|
|
87
|
+
choices: [
|
|
88
|
+
...moduleChoices,
|
|
89
|
+
new Separator(), // 分割线,方便未来扩展更多功能
|
|
90
|
+
],
|
|
91
|
+
default: 'visually_lossless', // 默认选项
|
|
92
|
+
loop: true, // 是否循环滚动选项
|
|
93
|
+
});
|
|
94
|
+
// 询问是否自动缩放超大图片
|
|
95
|
+
const shouldResize = await confirm({
|
|
96
|
+
message: '是否自动缩放超大图片(宽度超过 2560px 时缩小至 2560px)?',
|
|
97
|
+
default: true,
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
logger.info(`开始处理图片:${imagePath},压缩级别:${(_a = moduleChoices.find((item) => item.value === compressType)) === null || _a === void 0 ? void 0 : _a.name}`, true);
|
|
101
|
+
// 读取图片元数据,获取格式信息
|
|
102
|
+
const image = sharp(imagePath);
|
|
103
|
+
const metadata = await image.metadata();
|
|
104
|
+
const format = metadata.format;
|
|
105
|
+
if (!format) {
|
|
106
|
+
throw new Error('无法识别图片格式');
|
|
107
|
+
}
|
|
108
|
+
logger.info(`图片格式:${format},原始尺寸:${metadata.width}x${metadata.height}`, true);
|
|
109
|
+
// 初始化处理管道
|
|
110
|
+
let pipeline = image;
|
|
111
|
+
// 自动缩放逻辑(用户同意且图片宽度超出阈值)
|
|
112
|
+
const MAX_WIDTH = 2560;
|
|
113
|
+
if (shouldResize && metadata.width && metadata.width > MAX_WIDTH) {
|
|
114
|
+
// 防御性检查:确保 width 是有效正数
|
|
115
|
+
if (metadata.width > 0 && metadata.width < 100000) {
|
|
116
|
+
pipeline = pipeline.resize(MAX_WIDTH, null, {
|
|
117
|
+
withoutEnlargement: true,
|
|
118
|
+
fit: 'inside',
|
|
119
|
+
});
|
|
120
|
+
logger.info(`图片尺寸已从 ${metadata.width}px 缩小至 ${MAX_WIDTH}px`, true);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
logger.warn(`图片宽度异常 (${metadata.width}),跳过尺寸调整`, true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (shouldResize) {
|
|
127
|
+
logger.info(`图片宽度未超过 ${MAX_WIDTH}px,无需缩放`, true);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
logger.info('已跳过自动缩放', true);
|
|
131
|
+
}
|
|
132
|
+
// 根据格式和压缩类型构建处理管道
|
|
133
|
+
switch (format) {
|
|
134
|
+
case 'jpeg':
|
|
135
|
+
case 'jpg':
|
|
136
|
+
if (compressType === 'lossless') {
|
|
137
|
+
// 无损:最高质量,关闭色度抽样
|
|
138
|
+
pipeline = pipeline.jpeg({
|
|
139
|
+
quality: 100,
|
|
140
|
+
chromaSubsampling: '4:4:4',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if (compressType === 'visually_lossless') {
|
|
144
|
+
// 视觉无损:高品质有损,轻微色度抽样
|
|
145
|
+
pipeline = pipeline.jpeg({
|
|
146
|
+
quality: 90,
|
|
147
|
+
progressive: true,
|
|
148
|
+
chromaSubsampling: '4:2:0',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else if (compressType === 'lossy') {
|
|
152
|
+
// 有损:标准有损参数
|
|
153
|
+
pipeline = pipeline.jpeg({
|
|
154
|
+
quality: 75,
|
|
155
|
+
progressive: true,
|
|
156
|
+
chromaSubsampling: '4:2:0',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// 高损:低质量,最大化压缩
|
|
161
|
+
pipeline = pipeline.jpeg({
|
|
162
|
+
quality: 55,
|
|
163
|
+
progressive: true,
|
|
164
|
+
chromaSubsampling: '4:2:0',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case 'png':
|
|
169
|
+
if (compressType === 'lossless') {
|
|
170
|
+
// 无损:最高压缩级别
|
|
171
|
+
pipeline = pipeline.png({
|
|
172
|
+
compressionLevel: 9,
|
|
173
|
+
adaptiveFiltering: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (compressType === 'visually_lossless') {
|
|
177
|
+
// 视觉无损:高质量调色板模式
|
|
178
|
+
pipeline = pipeline.png({
|
|
179
|
+
compressionLevel: 9,
|
|
180
|
+
palette: true,
|
|
181
|
+
quality: 90,
|
|
182
|
+
effort: 10,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (compressType === 'lossy') {
|
|
186
|
+
// 有损:中等质量调色板
|
|
187
|
+
pipeline = pipeline.png({
|
|
188
|
+
compressionLevel: 9,
|
|
189
|
+
palette: true,
|
|
190
|
+
quality: 70,
|
|
191
|
+
effort: 10,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// 高损:低质量调色板,大幅减少颜色
|
|
196
|
+
pipeline = pipeline.png({
|
|
197
|
+
compressionLevel: 9,
|
|
198
|
+
palette: true,
|
|
199
|
+
quality: 45,
|
|
200
|
+
effort: 10,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case 'webp':
|
|
205
|
+
if (compressType === 'lossless') {
|
|
206
|
+
// 无损:启用 lossless 模式
|
|
207
|
+
pipeline = pipeline.webp({
|
|
208
|
+
lossless: true,
|
|
209
|
+
quality: 100,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (compressType === 'visually_lossless') {
|
|
213
|
+
// 视觉无损:高质量有损 WebP
|
|
214
|
+
pipeline = pipeline.webp({
|
|
215
|
+
lossless: false,
|
|
216
|
+
quality: 85,
|
|
217
|
+
effort: 6,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else if (compressType === 'lossy') {
|
|
221
|
+
// 有损:中等质量
|
|
222
|
+
pipeline = pipeline.webp({
|
|
223
|
+
lossless: false,
|
|
224
|
+
quality: 65,
|
|
225
|
+
effort: 6,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// 高损:低质量
|
|
230
|
+
pipeline = pipeline.webp({
|
|
231
|
+
lossless: false,
|
|
232
|
+
quality: 45,
|
|
233
|
+
effort: 6,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case 'avif':
|
|
238
|
+
if (compressType === 'lossless') {
|
|
239
|
+
// 无损:启用 lossless 模式
|
|
240
|
+
pipeline = pipeline.avif({
|
|
241
|
+
lossless: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (compressType === 'visually_lossless') {
|
|
245
|
+
// 视觉无损:高质量有损 AVIF
|
|
246
|
+
pipeline = pipeline.avif({
|
|
247
|
+
lossless: false,
|
|
248
|
+
quality: 60,
|
|
249
|
+
effort: 9,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else if (compressType === 'lossy') {
|
|
253
|
+
// 有损:中等质量
|
|
254
|
+
pipeline = pipeline.avif({
|
|
255
|
+
lossless: false,
|
|
256
|
+
quality: 45,
|
|
257
|
+
effort: 9,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// 高损:极低质量
|
|
262
|
+
pipeline = pipeline.avif({
|
|
263
|
+
lossless: false,
|
|
264
|
+
quality: 30,
|
|
265
|
+
effort: 9,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case 'tiff':
|
|
270
|
+
// TIFF 支持 LZW 无损压缩或 JPEG 有损压缩
|
|
271
|
+
if (compressType === 'lossless') {
|
|
272
|
+
pipeline = pipeline.tiff({
|
|
273
|
+
compression: 'lzw',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
else if (compressType === 'visually_lossless') {
|
|
277
|
+
// 视觉无损:轻微有损
|
|
278
|
+
pipeline = pipeline.tiff({
|
|
279
|
+
compression: 'jpeg',
|
|
280
|
+
quality: 90,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else if (compressType === 'lossy') {
|
|
284
|
+
pipeline = pipeline.tiff({
|
|
285
|
+
compression: 'jpeg',
|
|
286
|
+
quality: 75,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
pipeline = pipeline.tiff({
|
|
291
|
+
compression: 'jpeg',
|
|
292
|
+
quality: 55,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
case 'gif':
|
|
297
|
+
// sharp 对 GIF 处理有限,保持原样
|
|
298
|
+
logger.warn('GIF 格式仅支持原样输出,不进行额外压缩', true);
|
|
299
|
+
break;
|
|
300
|
+
default:
|
|
301
|
+
throw new Error(`暂不支持的格式:${format}`);
|
|
302
|
+
}
|
|
303
|
+
// 移除所有元数据以进一步减小体积
|
|
304
|
+
pipeline = pipeline.withMetadata({});
|
|
305
|
+
const outputPath = getOutputPath(imagePath);
|
|
306
|
+
// 执行压缩并输出文件
|
|
307
|
+
await pipeline.toFile(outputPath);
|
|
308
|
+
// 获取压缩后文件大小,计算压缩率
|
|
309
|
+
const originalSize = fs.statSync(imagePath).size;
|
|
310
|
+
const compressedSize = fs.statSync(outputPath).size;
|
|
311
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
|
|
312
|
+
logger.info(`压缩完成!\n输出文件:${outputPath}\n原始大小:${(originalSize / 1024).toFixed(2)} KB\n压缩后大小:${(compressedSize / 1024).toFixed(2)} KB\n压缩率:${ratio}%`, true);
|
|
313
|
+
logger.info(`图片压缩成功!文件已保存至:${outputPath}`, true);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
loggerError(error, logger);
|
|
317
|
+
console.error('程序执行时发生异常,已记录日志,程序已退出');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/image/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,OAAO,iBAmD3C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { select, Separator } from '@inquirer/prompts';
|
|
2
|
+
import { logger, loggerError } from '../../utils/index.js';
|
|
3
|
+
import { compressImage } from './compressImage/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* 图片模块主入口
|
|
6
|
+
* 提供多个图片相关功能的交互式选择
|
|
7
|
+
* @param program Commander命令行实例,用于传递参数和配置
|
|
8
|
+
*/
|
|
9
|
+
export async function image(program) {
|
|
10
|
+
try {
|
|
11
|
+
logger.info('图片模块启动,等待用户选择功能');
|
|
12
|
+
// 定义可用功能选项
|
|
13
|
+
const moduleChoices = [
|
|
14
|
+
{
|
|
15
|
+
name: '压缩图片',
|
|
16
|
+
value: 'compressImage',
|
|
17
|
+
description: '对图片大小进行压缩',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
// 交互式选择需要执行的功能
|
|
21
|
+
const answer = await select({
|
|
22
|
+
message: '请选择要执行的功能:',
|
|
23
|
+
choices: [
|
|
24
|
+
...moduleChoices,
|
|
25
|
+
new Separator(), // 分割线,方便未来扩展更多功能
|
|
26
|
+
],
|
|
27
|
+
default: 'compressImage', // 默认选项
|
|
28
|
+
loop: true, // 是否循环滚动选项
|
|
29
|
+
});
|
|
30
|
+
// 查找选择功能的名称,方便日志输出
|
|
31
|
+
const selectedModule = moduleChoices.find((item) => item.value === answer);
|
|
32
|
+
if (!selectedModule) {
|
|
33
|
+
logger.warn('未选择有效功能,程序已退出');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
logger.info(`用户选择功能:${selectedModule.name}`);
|
|
37
|
+
// 根据选择执行对应功能
|
|
38
|
+
switch (answer) {
|
|
39
|
+
case 'compressImage':
|
|
40
|
+
logger.info(`${selectedModule.name}功能开始执行`);
|
|
41
|
+
await compressImage(program);
|
|
42
|
+
logger.info(`${selectedModule.name}功能执行完成`);
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
logger.warn(`${selectedModule.name}功能暂未实现,程序已退出`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// 记录错误日志,方便排查
|
|
51
|
+
loggerError(error, logger);
|
|
52
|
+
console.error('程序执行时发生异常,已记录日志,程序已退出');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-web-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "A CLI tool for efficiency",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"commander": "^14.0.2",
|
|
49
49
|
"minimatch": "^10.2.4",
|
|
50
50
|
"node-html-parser": "^7.1.0",
|
|
51
|
+
"sharp": "^0.34.5",
|
|
51
52
|
"xlsx": "^0.18.5"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|