change-image-suffix 2.1.3 → 2.1.5
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/CHANGELOG.md +24 -0
- package/README.md +15 -13
- package/dist/index.js +102 -83
- package/package.json +14 -13
- /package/scripts/{postinstall.js → postinstall.cjs} +0 -0
- /package/scripts/{preuninstall.js → preuninstall.cjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.1.5](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.4...v2.1.5) (2026-05-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Chores
|
|
9
|
+
|
|
10
|
+
* upgrade deps (sharp 0.34, TS6, @types/node 25), migrate to ESM, require node >=24 ([d3a3234](https://github.com/GuoSirius/change-image-suffix/commit/d3a323410ccd1f5ed9f04443adc25c6edd96f7df))
|
|
11
|
+
|
|
12
|
+
### [2.1.4](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.3...v2.1.4) (2026-05-23)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* output to <format>/ dir, command injection, recursive nesting, and other issues ([32da38a](https://github.com/GuoSirius/change-image-suffix/commit/32da38aef36a7d736e91e0375573a905d565d864))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Chores
|
|
21
|
+
|
|
22
|
+
* update local settings permissions ([1a86130](https://github.com/GuoSirius/change-image-suffix/commit/1a86130de6005348d6ba8aaba26500d7688be996))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
* update README output convention, add .claude/ commit policy memory ([b689bc6](https://github.com/GuoSirius/change-image-suffix/commit/b689bc60ed397eb826954e289c3f89488bd97449))
|
|
28
|
+
|
|
5
29
|
### [2.1.3](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.2...v2.1.3) (2026-05-21)
|
|
6
30
|
|
|
7
31
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 📏 支持递归深度限制
|
|
10
10
|
- 🎯 支持指定源文件后缀(png, jpg, gif 等)
|
|
11
11
|
- 🎨 支持多种目标格式(webp, jpg, png, avif, tiff)
|
|
12
|
-
- 📤 输出到 `
|
|
12
|
+
- 📤 输出到 `<目标格式>/` 子目录(如 `webp/`, `jpg/`)
|
|
13
13
|
- 🔢 同名不同后缀文件自动编号(`_01`, `_02`)
|
|
14
14
|
- 🖱️ Windows 右键菜单集成
|
|
15
15
|
- ⚡ 基于 [sharp](https://sharp.pixel.glass/) 高性能图片处理
|
|
@@ -101,7 +101,7 @@ cis uninstall-menu
|
|
|
101
101
|
|
|
102
102
|
## 文件命名规范
|
|
103
103
|
|
|
104
|
-
转换后的文件输出到
|
|
104
|
+
转换后的文件输出到 **`<目标格式>/`** 子目录下(例如转换为 webp 则输出到 `webp/`,转换为 jpg 则输出到 `jpg/`),命名规则如下:
|
|
105
105
|
|
|
106
106
|
### 同名不同后缀 → 自动编号
|
|
107
107
|
|
|
@@ -109,34 +109,36 @@ cis uninstall-menu
|
|
|
109
109
|
|
|
110
110
|
```
|
|
111
111
|
源目录: photo.png + photo.jpg + photo.gif
|
|
112
|
-
输出:
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
输出: webp/photo_01.webp
|
|
113
|
+
webp/photo_02.webp
|
|
114
|
+
webp/photo_03.webp
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
### 不同名或不同文件 → 直接覆盖
|
|
118
118
|
|
|
119
119
|
```
|
|
120
120
|
源目录: banner.png + logo.jpg
|
|
121
|
-
输出:
|
|
122
|
-
|
|
121
|
+
输出: webp/banner.webp
|
|
122
|
+
webp/logo.webp
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
### 同格式转换 → 直接覆盖
|
|
126
126
|
|
|
127
127
|
```
|
|
128
128
|
源目录: photo.webp
|
|
129
|
-
输出:
|
|
129
|
+
输出: webp/photo.webp (直接复制,无双重后缀)
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
### 输出目录结构
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
📁 原目录/
|
|
136
|
-
├── 📁
|
|
136
|
+
├── 📁 webp/ ← 转换后的 webp 文件
|
|
137
137
|
│ ├── photo.webp
|
|
138
|
-
│ ├── banner.
|
|
139
|
-
│ └── photo_01.
|
|
138
|
+
│ ├── banner.webp
|
|
139
|
+
│ └── photo_01.webp
|
|
140
|
+
├── 📁 jpg/ ← 转换后的 jpg 文件
|
|
141
|
+
│ └── ...
|
|
140
142
|
├── photo.png
|
|
141
143
|
├── banner.jpg
|
|
142
144
|
└── logo.gif
|
|
@@ -193,14 +195,14 @@ cis -f ./avatar.png -t webp
|
|
|
193
195
|
### 示例 6:多选文件/目录批量转换
|
|
194
196
|
|
|
195
197
|
```bash
|
|
196
|
-
# 多选文件(空格分隔),每个文件的输出在其所在目录的
|
|
198
|
+
# 多选文件(空格分隔),每个文件的输出在其所在目录的 <目标格式>/
|
|
197
199
|
cis ./photo1.png ./photo2.jpg ./folder3
|
|
198
200
|
|
|
199
201
|
# 多选多个目录
|
|
200
202
|
cis ./images ./icons ./logos
|
|
201
203
|
```
|
|
202
204
|
|
|
203
|
-
> 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `
|
|
205
|
+
> 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `<目标格式>/` 子目录**中(如 `webp/`, `jpg/`),互不干扰。
|
|
204
206
|
|
|
205
207
|
---
|
|
206
208
|
|
package/dist/index.js
CHANGED
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
const fs = __importStar(require("fs"));
|
|
41
|
-
const path = __importStar(require("path"));
|
|
42
|
-
const os = __importStar(require("os"));
|
|
43
|
-
const child_process_1 = require("child_process");
|
|
44
|
-
const sharp_1 = __importDefault(require("sharp"));
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import sharp from 'sharp';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
45
10
|
// 支持的输入/输出格式
|
|
46
11
|
const SUPPORTED_INPUT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
|
|
47
12
|
const SUPPORTED_OUTPUT_FORMATS = ['webp', 'jpg', 'jpeg', 'png', 'avif', 'tiff', 'tif'];
|
|
@@ -58,7 +23,7 @@ function requireWindows() {
|
|
|
58
23
|
}
|
|
59
24
|
function regDelete(key) {
|
|
60
25
|
try {
|
|
61
|
-
|
|
26
|
+
execSync(`reg delete "${key}" /f`, { stdio: 'ignore' });
|
|
62
27
|
}
|
|
63
28
|
catch {
|
|
64
29
|
// 忽略不存在的键
|
|
@@ -69,11 +34,11 @@ function installContextMenu() {
|
|
|
69
34
|
// ── 查找 cis.cmd 路径 ──
|
|
70
35
|
let cisCmd = '';
|
|
71
36
|
try {
|
|
72
|
-
cisCmd =
|
|
37
|
+
cisCmd = execSync('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
|
|
73
38
|
}
|
|
74
39
|
catch {
|
|
75
40
|
try {
|
|
76
|
-
cisCmd =
|
|
41
|
+
cisCmd = execSync('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
|
|
77
42
|
}
|
|
78
43
|
catch {
|
|
79
44
|
console.error('❌ 找不到 cis 命令,请先执行 npm link 或 npm install -g change-image-suffix');
|
|
@@ -176,21 +141,21 @@ endlocal
|
|
|
176
141
|
else {
|
|
177
142
|
cmd = `"${cisCmd}" -t ${fmt.verb} ${menu.arg}`;
|
|
178
143
|
}
|
|
179
|
-
|
|
180
|
-
|
|
144
|
+
execSync(`reg add "${shellKey}" /ve /d "${fmt.label}" /f`, { stdio: 'ignore' });
|
|
145
|
+
execSync(`reg add "${shellKey}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
|
|
181
146
|
// 直接调用 bat,不需要 cmd /c,Windows 会自动追加文件路径
|
|
182
|
-
|
|
147
|
+
execSync(`reg add "${shellKey}\\command" /ve /d "${cmd}" /f`, { stdio: 'ignore' });
|
|
183
148
|
}
|
|
184
149
|
}
|
|
185
150
|
// 2. 注册主菜单项(使用 ExtendedSubCommandsKey 关联各自的子菜单)
|
|
186
151
|
for (const menu of menuBases) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
152
|
+
execSync(`reg add "${menu.base}" /ve /d "🖼 转换图片 (cis)" /f`, { stdio: 'ignore' });
|
|
153
|
+
execSync(`reg add "${menu.base}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
|
|
154
|
+
execSync(`reg add "${menu.base}" /v ExtendedSubCommandsKey /d "${menu.subMenu}" /f`, { stdio: 'ignore' });
|
|
190
155
|
// 使用 bat 的菜单(文件右键和目录右键):设置 command 接收文件路径 %1
|
|
191
156
|
// Windows 会将父命令收到的 %1 自动传递给子命令
|
|
192
157
|
if (menu.useBat) {
|
|
193
|
-
|
|
158
|
+
execSync(`reg add "${menu.base}\\command" /ve /d "cmd /c echo %1 > nul" /f`, { stdio: 'ignore' });
|
|
194
159
|
// 注意:不添加 AppliesTo 限制,让菜单始终显示
|
|
195
160
|
// bat 脚本会检查文件扩展名,自动忽略非图片文件
|
|
196
161
|
}
|
|
@@ -203,7 +168,7 @@ endlocal
|
|
|
203
168
|
console.log(' 📁 文件夹空白处/图标右键 → 悬停展开格式子菜单');
|
|
204
169
|
console.log(' 🖼 图片文件上右键 → 悬停展开格式子菜单');
|
|
205
170
|
console.log(' ⚠️ 非图片文件右键 → 菜单显示但不处理');
|
|
206
|
-
console.log(` 📂 输出目录:
|
|
171
|
+
console.log(` 📂 输出目录: <原目录>/<目标格式>/`);
|
|
207
172
|
console.log('\n💡 提示:如需卸载,执行 cis uninstall-menu');
|
|
208
173
|
}
|
|
209
174
|
function uninstallContextMenu() {
|
|
@@ -216,7 +181,7 @@ function uninstallContextMenu() {
|
|
|
216
181
|
];
|
|
217
182
|
for (const key of mainKeys) {
|
|
218
183
|
try {
|
|
219
|
-
|
|
184
|
+
execSync(`reg delete "${key}" /f`, { stdio: 'ignore' });
|
|
220
185
|
}
|
|
221
186
|
catch { /* ignore */ }
|
|
222
187
|
}
|
|
@@ -228,22 +193,32 @@ function uninstallContextMenu() {
|
|
|
228
193
|
];
|
|
229
194
|
for (const root of subMenuRoots) {
|
|
230
195
|
try {
|
|
231
|
-
|
|
196
|
+
execSync(`reg delete "${root}" /f`, { stdio: 'ignore' });
|
|
232
197
|
}
|
|
233
198
|
catch { /* ignore */ }
|
|
234
199
|
}
|
|
235
|
-
//
|
|
200
|
+
// 删除批处理文件、图标和版本标记
|
|
236
201
|
const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
|
|
237
202
|
const batPath = path.join(appDataDir, 'cis_file.bat');
|
|
238
203
|
try {
|
|
239
204
|
fs.unlinkSync(batPath);
|
|
240
205
|
}
|
|
241
206
|
catch { /* ignore */ }
|
|
207
|
+
const iconPath = path.join(appDataDir, 'icon.ico');
|
|
208
|
+
try {
|
|
209
|
+
fs.unlinkSync(iconPath);
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
242
212
|
const versionFile = path.join(appDataDir, 'version.json');
|
|
243
213
|
try {
|
|
244
214
|
fs.unlinkSync(versionFile);
|
|
245
215
|
}
|
|
246
216
|
catch { /* ignore */ }
|
|
217
|
+
// 尝试删除目录(仅当为空时),失败也不影响
|
|
218
|
+
try {
|
|
219
|
+
fs.rmdirSync(appDataDir);
|
|
220
|
+
}
|
|
221
|
+
catch { /* ignore */ }
|
|
247
222
|
console.log('✅ 右键菜单已卸载');
|
|
248
223
|
}
|
|
249
224
|
// 自动检测版本变化并更新右键菜单(解决 npm update 不触发 postinstall 的问题)
|
|
@@ -252,20 +227,23 @@ function autoUpdateContextMenu() {
|
|
|
252
227
|
return;
|
|
253
228
|
const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
|
|
254
229
|
const versionFile = path.join(appDataDir, 'version.json');
|
|
230
|
+
// 如果从未安装过右键菜单,跳过自动更新(postinstall 负责首次安装)
|
|
231
|
+
if (!fs.existsSync(versionFile))
|
|
232
|
+
return;
|
|
255
233
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
256
234
|
const currentVersion = pkg.version;
|
|
257
235
|
let installedVersion = '';
|
|
258
236
|
try {
|
|
259
|
-
|
|
260
|
-
installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
|
|
261
|
-
}
|
|
237
|
+
installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
|
|
262
238
|
}
|
|
263
239
|
catch { /* ignore */ }
|
|
264
240
|
if (installedVersion !== currentVersion) {
|
|
265
241
|
try {
|
|
266
242
|
installContextMenu();
|
|
267
243
|
}
|
|
268
|
-
catch {
|
|
244
|
+
catch {
|
|
245
|
+
console.warn('⚠️ 右键菜单自动更新失败,请手动执行 cis install-menu');
|
|
246
|
+
}
|
|
269
247
|
}
|
|
270
248
|
}
|
|
271
249
|
// ─────────────────────────────────────────
|
|
@@ -294,27 +272,40 @@ function parseArgs() {
|
|
|
294
272
|
continue;
|
|
295
273
|
}
|
|
296
274
|
if (arg === '-d' || arg === '--depth') {
|
|
297
|
-
if (i + 1 < args.length) {
|
|
298
|
-
|
|
299
|
-
if (isNaN(
|
|
275
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
276
|
+
const val = parseInt(args[++i], 10);
|
|
277
|
+
if (isNaN(val) || val < 1) {
|
|
300
278
|
console.error('❌ 深度必须是正整数');
|
|
301
279
|
process.exit(1);
|
|
302
280
|
}
|
|
281
|
+
options.maxDepth = val;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.error('❌ -d/--depth 需要指定一个正整数参数');
|
|
285
|
+
process.exit(1);
|
|
303
286
|
}
|
|
304
287
|
i++;
|
|
305
288
|
continue;
|
|
306
289
|
}
|
|
307
290
|
if (arg === '-e' || arg === '--extensions') {
|
|
308
|
-
if (i + 1 < args.length) {
|
|
291
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
309
292
|
options.extensions = args[++i].split(',').map(e => e.trim().toLowerCase().replace(/^\./, ''));
|
|
310
293
|
}
|
|
294
|
+
else {
|
|
295
|
+
console.error('❌ -e/--extensions 需要指定后缀参数');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
311
298
|
i++;
|
|
312
299
|
continue;
|
|
313
300
|
}
|
|
314
301
|
if (arg === '-t' || arg === '--to') {
|
|
315
|
-
if (i + 1 < args.length) {
|
|
302
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
316
303
|
options.targetFormat = args[++i].trim().toLowerCase().replace(/^\./, '');
|
|
317
304
|
}
|
|
305
|
+
else {
|
|
306
|
+
console.error('❌ -t/--to 需要指定目标格式');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
318
309
|
i++;
|
|
319
310
|
continue;
|
|
320
311
|
}
|
|
@@ -343,10 +334,15 @@ function parseArgs() {
|
|
|
343
334
|
}
|
|
344
335
|
if (arg === '-f' || arg === '--file') {
|
|
345
336
|
// 收集 -f 后的所有文件
|
|
337
|
+
const start = i + 1;
|
|
346
338
|
while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
347
339
|
i++;
|
|
348
340
|
filesFromFlag.push(path.resolve(args[i]));
|
|
349
341
|
}
|
|
342
|
+
if (start > i) {
|
|
343
|
+
console.error('❌ -f/--file 需要指定至少一个文件路径');
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
350
346
|
i++;
|
|
351
347
|
continue;
|
|
352
348
|
}
|
|
@@ -434,14 +430,16 @@ function printHelp() {
|
|
|
434
430
|
// ─────────────────────────────────────────
|
|
435
431
|
// 图片处理
|
|
436
432
|
// ─────────────────────────────────────────
|
|
437
|
-
function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth) {
|
|
433
|
+
function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth, excludeDirName) {
|
|
438
434
|
const files = [];
|
|
439
435
|
try {
|
|
440
436
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
441
437
|
for (const entry of entries) {
|
|
442
438
|
const fullPath = path.join(dir, entry.name);
|
|
443
439
|
if (entry.isDirectory() && recursive && currentDepth < maxDepth) {
|
|
444
|
-
|
|
440
|
+
if (excludeDirName && entry.name === excludeDirName)
|
|
441
|
+
continue;
|
|
442
|
+
files.push(...getAllFiles(fullPath, extensions, recursive, currentDepth + 1, maxDepth, excludeDirName));
|
|
445
443
|
}
|
|
446
444
|
else if (entry.isFile()) {
|
|
447
445
|
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
@@ -462,7 +460,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
462
460
|
const basename = path.basename(inputPath, ext);
|
|
463
461
|
const targetExt = targetFormat;
|
|
464
462
|
let coreName = basename;
|
|
465
|
-
const outputDir = path.join(dir,
|
|
463
|
+
const outputDir = path.join(dir, targetFormat);
|
|
466
464
|
// 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
|
|
467
465
|
// 如 photo.png 和 photo.jpg 会被判定为同名冲突
|
|
468
466
|
const hasNameConflict = allInputFiles.some(f => {
|
|
@@ -471,7 +469,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
471
469
|
const fDir = path.dirname(f);
|
|
472
470
|
const fExt = path.extname(f);
|
|
473
471
|
const fBasename = path.basename(f, fExt);
|
|
474
|
-
return fDir === dir && fBasename === basename && fExt.toLowerCase() !== ext.toLowerCase();
|
|
472
|
+
return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase() && fExt.toLowerCase() !== ext.toLowerCase();
|
|
475
473
|
});
|
|
476
474
|
if (hasNameConflict) {
|
|
477
475
|
// 找所有同basename的文件的序号
|
|
@@ -481,7 +479,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
481
479
|
const fDir = path.dirname(f);
|
|
482
480
|
const fExt = path.extname(f);
|
|
483
481
|
const fBasename = path.basename(f, fExt);
|
|
484
|
-
return fDir === dir && fBasename === basename;
|
|
482
|
+
return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase();
|
|
485
483
|
});
|
|
486
484
|
// 当前文件在所有同名文件中的索引(从1开始)
|
|
487
485
|
const sortedMatches = [...allBasenameMatches, inputPath].sort();
|
|
@@ -493,23 +491,24 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
493
491
|
return path.join(outputDir, filename);
|
|
494
492
|
}
|
|
495
493
|
async function convertImage(inputPath, targetFormat, allInputFiles) {
|
|
494
|
+
const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
|
|
495
|
+
const srcExt = path.extname(inputPath).slice(1).toLowerCase();
|
|
496
|
+
const fmt = targetFormat.toLowerCase();
|
|
497
|
+
// 先验证格式,避免在无效路径上创建目录
|
|
498
|
+
if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
|
|
499
|
+
return { success: false, outputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
|
|
500
|
+
}
|
|
496
501
|
try {
|
|
497
|
-
const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
|
|
498
502
|
const outputDir = path.dirname(outputPath);
|
|
499
503
|
if (!fs.existsSync(outputDir)) {
|
|
500
504
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
501
505
|
}
|
|
502
|
-
const srcExt = path.extname(inputPath).slice(1).toLowerCase();
|
|
503
|
-
const fmt = targetFormat.toLowerCase();
|
|
504
506
|
// 同格式直接复制,避免重新编码导致质量损失
|
|
505
507
|
if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
|
|
506
508
|
fs.copyFileSync(inputPath, outputPath);
|
|
507
509
|
return { success: true, outputPath };
|
|
508
510
|
}
|
|
509
|
-
|
|
510
|
-
return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
|
|
511
|
-
}
|
|
512
|
-
const image = (0, sharp_1.default)(inputPath);
|
|
511
|
+
const image = sharp(inputPath);
|
|
513
512
|
switch (fmt) {
|
|
514
513
|
case 'webp':
|
|
515
514
|
await image.webp({ quality: 90 }).toFile(outputPath);
|
|
@@ -534,7 +533,7 @@ async function convertImage(inputPath, targetFormat, allInputFiles) {
|
|
|
534
533
|
catch (err) {
|
|
535
534
|
return {
|
|
536
535
|
success: false,
|
|
537
|
-
outputPath
|
|
536
|
+
outputPath,
|
|
538
537
|
error: err instanceof Error ? err.message : '未知错误'
|
|
539
538
|
};
|
|
540
539
|
}
|
|
@@ -565,16 +564,18 @@ async function main() {
|
|
|
565
564
|
console.log('----------------------------------------\n');
|
|
566
565
|
let totalSuccess = 0;
|
|
567
566
|
let totalFail = 0;
|
|
567
|
+
const failures = [];
|
|
568
568
|
for (const filePath of files) {
|
|
569
569
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
570
570
|
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
571
571
|
console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
|
|
572
572
|
totalFail++;
|
|
573
|
+
failures.push(filePath);
|
|
573
574
|
continue;
|
|
574
575
|
}
|
|
575
576
|
console.log(` 📄 文件: ${filePath}`);
|
|
576
577
|
process.stdout.write(` 处理中: ${path.basename(filePath)} ... `);
|
|
577
|
-
const result = await convertImage(filePath, options.targetFormat,
|
|
578
|
+
const result = await convertImage(filePath, options.targetFormat, files);
|
|
578
579
|
if (result.success) {
|
|
579
580
|
console.log(`✅ -> ${path.relative(path.dirname(filePath), result.outputPath)}`);
|
|
580
581
|
totalSuccess++;
|
|
@@ -582,6 +583,13 @@ async function main() {
|
|
|
582
583
|
else {
|
|
583
584
|
console.log(`❌ 失败 (${result.error})`);
|
|
584
585
|
totalFail++;
|
|
586
|
+
failures.push(filePath);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (failures.length > 0) {
|
|
590
|
+
console.log('\n❌ 失败的文件:');
|
|
591
|
+
for (const f of failures) {
|
|
592
|
+
console.log(` - ${f}`);
|
|
585
593
|
}
|
|
586
594
|
}
|
|
587
595
|
return { success: totalSuccess, fail: totalFail };
|
|
@@ -593,11 +601,13 @@ async function main() {
|
|
|
593
601
|
console.log('----------------------------------------\n');
|
|
594
602
|
let totalSuccess = 0;
|
|
595
603
|
let totalFail = 0;
|
|
604
|
+
const failures = [];
|
|
596
605
|
for (const inputPath of dirs) {
|
|
597
606
|
const stat = fs.existsSync(inputPath) ? fs.statSync(inputPath) : null;
|
|
598
607
|
if (!stat) {
|
|
599
608
|
console.log(` ⚠️ 跳过(不存在): ${inputPath}`);
|
|
600
609
|
totalFail++;
|
|
610
|
+
failures.push(inputPath);
|
|
601
611
|
continue;
|
|
602
612
|
}
|
|
603
613
|
if (stat.isFile()) {
|
|
@@ -605,11 +615,12 @@ async function main() {
|
|
|
605
615
|
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
606
616
|
console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
|
|
607
617
|
totalFail++;
|
|
618
|
+
failures.push(inputPath);
|
|
608
619
|
continue;
|
|
609
620
|
}
|
|
610
621
|
console.log(` 📄 文件: ${inputPath}`);
|
|
611
622
|
process.stdout.write(` 处理中: ${path.basename(inputPath)} ... `);
|
|
612
|
-
const result = await convertImage(inputPath, options.targetFormat,
|
|
623
|
+
const result = await convertImage(inputPath, options.targetFormat, dirs);
|
|
613
624
|
if (result.success) {
|
|
614
625
|
console.log(`✅ -> ${path.relative(path.dirname(inputPath), result.outputPath)}`);
|
|
615
626
|
totalSuccess++;
|
|
@@ -617,10 +628,11 @@ async function main() {
|
|
|
617
628
|
else {
|
|
618
629
|
console.log(`❌ 失败 (${result.error})`);
|
|
619
630
|
totalFail++;
|
|
631
|
+
failures.push(inputPath);
|
|
620
632
|
}
|
|
621
633
|
}
|
|
622
634
|
else {
|
|
623
|
-
const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth);
|
|
635
|
+
const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
|
|
624
636
|
console.log(` 📁 目录: ${inputPath} (${files.length} 个文件)`);
|
|
625
637
|
if (files.length === 0) {
|
|
626
638
|
console.log(' ✅ 没有找到图片文件');
|
|
@@ -636,10 +648,17 @@ async function main() {
|
|
|
636
648
|
else {
|
|
637
649
|
console.log(`❌ (${result.error})`);
|
|
638
650
|
totalFail++;
|
|
651
|
+
failures.push(file);
|
|
639
652
|
}
|
|
640
653
|
}
|
|
641
654
|
}
|
|
642
655
|
}
|
|
656
|
+
if (failures.length > 0) {
|
|
657
|
+
console.log('\n❌ 失败的文件:');
|
|
658
|
+
for (const f of failures) {
|
|
659
|
+
console.log(` - ${f}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
643
662
|
return { success: totalSuccess, fail: totalFail };
|
|
644
663
|
}
|
|
645
664
|
// ─── 混合模式:同时有文件和目录 ───
|
|
@@ -673,7 +692,7 @@ async function main() {
|
|
|
673
692
|
console.log(`📄 后缀: ${options.extensions.join(', ')}`);
|
|
674
693
|
console.log(`🎯 目标格式: ${options.targetFormat}`);
|
|
675
694
|
console.log('\n----------------------------------------\n');
|
|
676
|
-
const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth);
|
|
695
|
+
const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
|
|
677
696
|
if (files.length === 0) {
|
|
678
697
|
console.log('✅ 没有找到需要转换的图片文件。');
|
|
679
698
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "change-image-suffix",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "批量转换图片格式的CLI工具,支持递归搜索、深度限制、指定后缀、Windows右键菜单等功能",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"files": [
|
|
@@ -9,8 +10,8 @@
|
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE",
|
|
11
12
|
"CHANGELOG.md",
|
|
12
|
-
"scripts/postinstall.
|
|
13
|
-
"scripts/preuninstall.
|
|
13
|
+
"scripts/postinstall.cjs",
|
|
14
|
+
"scripts/preuninstall.cjs"
|
|
14
15
|
],
|
|
15
16
|
"bin": {
|
|
16
17
|
"change-image-suffix": "./dist/index.js",
|
|
@@ -24,13 +25,13 @@
|
|
|
24
25
|
"clean": "rimraf dist",
|
|
25
26
|
"build": "tsc",
|
|
26
27
|
"dev": "tsc --watch",
|
|
27
|
-
"postinstall": "node scripts/postinstall.
|
|
28
|
-
"preuninstall": "node scripts/preuninstall.
|
|
28
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
29
|
+
"preuninstall": "node scripts/preuninstall.cjs",
|
|
29
30
|
"prepublishOnly": "npm run clean && npm run build",
|
|
30
|
-
"release": "node scripts/release.
|
|
31
|
-
"release:patch": "node scripts/release.
|
|
32
|
-
"release:minor": "node scripts/release.
|
|
33
|
-
"release:major": "node scripts/release.
|
|
31
|
+
"release": "node scripts/release.cjs",
|
|
32
|
+
"release:patch": "node scripts/release.cjs --patch",
|
|
33
|
+
"release:minor": "node scripts/release.cjs --minor",
|
|
34
|
+
"release:major": "node scripts/release.cjs --major",
|
|
34
35
|
"lint": "tsc --noEmit",
|
|
35
36
|
"typecheck": "tsc --noEmit"
|
|
36
37
|
},
|
|
@@ -56,19 +57,19 @@
|
|
|
56
57
|
},
|
|
57
58
|
"homepage": "https://github.com/GuoSirius/change-image-suffix#readme",
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"sharp": "^0.
|
|
60
|
+
"sharp": "^0.34.5"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@commitlint/cli": "^21.0.1",
|
|
63
64
|
"@commitlint/config-conventional": "^21.0.1",
|
|
64
|
-
"@types/node": "^
|
|
65
|
+
"@types/node": "^25.9.1",
|
|
65
66
|
"enquirer": "^2.4.1",
|
|
66
67
|
"husky": "^9.1.7",
|
|
67
68
|
"rimraf": "^6.1.3",
|
|
68
69
|
"standard-version": "^9.5.0",
|
|
69
|
-
"typescript": "^
|
|
70
|
+
"typescript": "^6.0.3"
|
|
70
71
|
},
|
|
71
72
|
"engines": {
|
|
72
|
-
"node": ">=
|
|
73
|
+
"node": ">=24.0.0"
|
|
73
74
|
}
|
|
74
75
|
}
|
|
File without changes
|
|
File without changes
|