change-image-suffix 2.0.2 → 2.1.1
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 +30 -0
- package/README.md +12 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -174
- package/package.json +8 -4
- package/scripts/postinstall.js +14 -0
- package/scripts/preuninstall.js +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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.1](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.0...v2.1.1) (2026-05-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* unify format constants, remove dead code, sync docs with source ([c1e7700](https://github.com/GuoSirius/change-image-suffix/commit/c1e7700ee5f5a0142c98f5a1e95bc6453317a4a9))
|
|
11
|
+
|
|
12
|
+
## [2.1.0](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.2...v2.1.0) (2026-05-21)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* auto context-menu lifecycle hooks, enable declarations, add dev script ([ab44e37](https://github.com/GuoSirius/change-image-suffix/commit/ab44e374040fbfa6715edacb8fb249eeaf746821))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* rewrite bat script, remove gif/heif/jp2, same-format copy, quality 90 ([085a589](https://github.com/GuoSirius/change-image-suffix/commit/085a589f96682626834ac9715f7f228d52bfe89d))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Chores
|
|
26
|
+
|
|
27
|
+
* include .claude/ for cross-device project settings ([c863337](https://github.com/GuoSirius/change-image-suffix/commit/c863337e926826a1799ea0ff4bc6e1f0790afee0))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* add CLAUDE.md for project onboarding on other devices ([c7f06bc](https://github.com/GuoSirius/change-image-suffix/commit/c7f06bc9c670b31e3e9af9329cbce96b5d3ed065))
|
|
33
|
+
* add project memory for context-menu fix and codebase context ([e8fb432](https://github.com/GuoSirius/change-image-suffix/commit/e8fb43252cf1288d503c24737b84b2f687e19968))
|
|
34
|
+
|
|
5
35
|
### [2.0.2](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.1...v2.0.2) (2026-05-19)
|
|
6
36
|
|
|
7
37
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- 🔄 支持递归搜索子目录
|
|
9
9
|
- 📏 支持递归深度限制
|
|
10
10
|
- 🎯 支持指定源文件后缀(png, jpg, gif 等)
|
|
11
|
-
- 🎨 支持多种目标格式(webp, jpg, png, avif,
|
|
11
|
+
- 🎨 支持多种目标格式(webp, jpg, png, avif, tiff)
|
|
12
12
|
- 📤 输出到 `output/` 子目录
|
|
13
13
|
- 🔢 同名不同后缀文件自动编号(`_01`, `_02`)
|
|
14
14
|
- 🖱️ Windows 右键菜单集成
|
|
@@ -68,10 +68,7 @@ cis uninstall-menu
|
|
|
68
68
|
├── 📷 JPG
|
|
69
69
|
├── 🖼 PNG
|
|
70
70
|
├── 📺 AVIF
|
|
71
|
-
|
|
72
|
-
├── 📋 TIFF
|
|
73
|
-
├── 🍎 HEIF
|
|
74
|
-
└── 📐 JPEG2000
|
|
71
|
+
└── 📋 TIFF
|
|
75
72
|
```
|
|
76
73
|
|
|
77
74
|
**适用场景:**
|
|
@@ -94,7 +91,7 @@ cis uninstall-menu
|
|
|
94
91
|
| `-p, --path <dir>` | 指定工作目录 | 当前目录 |
|
|
95
92
|
| `-r, --recursive` | 递归搜索子目录 | 否 |
|
|
96
93
|
| `-d, --depth <n>` | 递归深度限制 | 无限制 |
|
|
97
|
-
| `-e, --extensions` | 指定源后缀,逗号分隔 | png,jpg,jpeg,gif,bmp,tiff,webp |
|
|
94
|
+
| `-e, --extensions` | 指定源后缀,逗号分隔 | png,jpg,jpeg,gif,bmp,tiff,tif,webp,avif |
|
|
98
95
|
| `-t, --to <format>` | 目标格式 | webp |
|
|
99
96
|
| `-f, --file <file>` | 指定单个文件转换 | - |
|
|
100
97
|
| `-h, --help` | 显示帮助 | - |
|
|
@@ -152,7 +149,7 @@ cis uninstall-menu
|
|
|
152
149
|
| 类型 | 格式 |
|
|
153
150
|
|------|------|
|
|
154
151
|
| **输入** | png, jpg, jpeg, gif, bmp, tiff, webp, avif |
|
|
155
|
-
| **输出** | webp, jpg, png, avif,
|
|
152
|
+
| **输出** | webp, jpg, png, avif, tiff |
|
|
156
153
|
|
|
157
154
|
---
|
|
158
155
|
|
|
@@ -282,14 +279,21 @@ npm run release:major # 重大变更
|
|
|
282
279
|
## 项目结构
|
|
283
280
|
|
|
284
281
|
```
|
|
282
|
+
├── .claude/ # Claude Code 项目记忆与设置
|
|
283
|
+
│ ├── MEMORY.md
|
|
284
|
+
│ ├── memory/
|
|
285
|
+
│ └── settings.local.json
|
|
285
286
|
├── .github/workflows/ # GitHub Actions 工作流
|
|
286
287
|
│ └── release.yml # 自动发布工作流
|
|
287
288
|
├── .husky/ # Git 钩子
|
|
288
289
|
│ └── commit-msg # 提交信息校验钩子
|
|
289
290
|
├── scripts/ # 脚本目录
|
|
290
|
-
│
|
|
291
|
+
│ ├── release.js # 一键发布脚本
|
|
292
|
+
│ ├── postinstall.js # npm postinstall 钩子(自动注册右键菜单)
|
|
293
|
+
│ └── preuninstall.js # npm preuninstall 钩子(自动清理右键菜单)
|
|
291
294
|
├── src/ # 源代码
|
|
292
295
|
│ └── index.ts # 主入口
|
|
296
|
+
├── CLAUDE.md # 项目上下文文档
|
|
293
297
|
├── .commitlintrc.json # commitlint 配置
|
|
294
298
|
├── CHANGELOG.md # 变更日志
|
|
295
299
|
└── package.json # 项目配置
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -42,8 +42,10 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const os = __importStar(require("os"));
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
44
|
const sharp_1 = __importDefault(require("sharp"));
|
|
45
|
-
//
|
|
46
|
-
const
|
|
45
|
+
// 支持的输入/输出格式
|
|
46
|
+
const SUPPORTED_INPUT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
|
|
47
|
+
const SUPPORTED_OUTPUT_FORMATS = ['webp', 'jpg', 'jpeg', 'png', 'avif', 'tiff', 'tif'];
|
|
48
|
+
const DEFAULT_EXTENSIONS = [...SUPPORTED_INPUT_EXTENSIONS];
|
|
47
49
|
const DEFAULT_TARGET_FORMAT = 'webp';
|
|
48
50
|
// ─────────────────────────────────────────
|
|
49
51
|
// 右键菜单管理(仅 Windows)
|
|
@@ -54,17 +56,6 @@ function requireWindows() {
|
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
/**
|
|
58
|
-
* 写注册表 key(调用 reg.exe,无需管理员权限写 HKCU)
|
|
59
|
-
*/
|
|
60
|
-
function regAdd(key, name, value, type = 'REG_SZ') {
|
|
61
|
-
const cmd = `reg add "${key}" /v "${name}" /t ${type} /d "${value}" /f`;
|
|
62
|
-
(0, child_process_1.execSync)(cmd, { stdio: 'ignore' });
|
|
63
|
-
}
|
|
64
|
-
function regAddDefault(key, value) {
|
|
65
|
-
const cmd = `reg add "${key}" /ve /d "${value}" /f`;
|
|
66
|
-
(0, child_process_1.execSync)(cmd, { stdio: 'ignore' });
|
|
67
|
-
}
|
|
68
59
|
function regDelete(key) {
|
|
69
60
|
try {
|
|
70
61
|
(0, child_process_1.execSync)(`reg delete "${key}" /f`, { stdio: 'ignore' });
|
|
@@ -75,17 +66,14 @@ function regDelete(key) {
|
|
|
75
66
|
}
|
|
76
67
|
function installContextMenu() {
|
|
77
68
|
requireWindows();
|
|
78
|
-
// ── 查找 cis.cmd
|
|
69
|
+
// ── 查找 cis.cmd 路径 ──
|
|
79
70
|
let cisCmd = '';
|
|
80
|
-
let nodeModulesDir = '';
|
|
81
71
|
try {
|
|
82
72
|
cisCmd = (0, child_process_1.execSync)('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
|
|
83
|
-
nodeModulesDir = path.dirname(cisCmd);
|
|
84
73
|
}
|
|
85
74
|
catch {
|
|
86
75
|
try {
|
|
87
76
|
cisCmd = (0, child_process_1.execSync)('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
|
|
88
|
-
nodeModulesDir = path.dirname(cisCmd);
|
|
89
77
|
}
|
|
90
78
|
catch {
|
|
91
79
|
console.error('❌ 找不到 cis 命令,请先执行 npm link 或 npm install -g change-image-suffix');
|
|
@@ -103,146 +91,60 @@ function installContextMenu() {
|
|
|
103
91
|
fs.copyFileSync(icoSource, icoTarget);
|
|
104
92
|
}
|
|
105
93
|
const iconPath = fs.existsSync(icoTarget) ? icoTarget : cisCmd;
|
|
106
|
-
// ── 辅助脚本路径定义(需要在 batContent 之前,因为 bat 中引用了 ps1Path)──
|
|
107
94
|
const batPath = path.join(appDataDir, 'cis_file.bat');
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
const cisGetfilesContent = `
|
|
111
|
-
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
112
|
-
Add-Type -AssemblyName UIAutomationClient
|
|
113
|
-
$files = @()
|
|
114
|
-
try {
|
|
115
|
-
$shell = New-Object -ComObject Shell.Application
|
|
116
|
-
$windows = $shell.Windows()
|
|
117
|
-
foreach ($win in $windows) {
|
|
118
|
-
if ($win -and $win.FullName -like "*explorer.exe") {
|
|
119
|
-
$selected = $win.Document.SelectedItems()
|
|
120
|
-
foreach ($item in $selected) {
|
|
121
|
-
if ($item -and $item.Path) {
|
|
122
|
-
$files += $item.Path
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch {}
|
|
128
|
-
if ($files.Count -gt 0) {
|
|
129
|
-
$files | ForEach-Object { $_ }
|
|
130
|
-
} else {
|
|
131
|
-
Write-Output "NO_FILES"
|
|
132
|
-
}
|
|
133
|
-
`;
|
|
134
|
-
fs.writeFileSync(ps1Path, cisGetfilesContent, 'utf8');
|
|
135
|
-
// ── bat 脚本:接收 Windows 传递的文件路径和格式参数 ──
|
|
136
|
-
// 根据 Windows ExtendedSubCommandsKey 机制:
|
|
137
|
-
// - 子命令的 command 参数(格式)在前
|
|
138
|
-
// - Windows 自动将父命令收到的文件路径追加在末尾
|
|
139
|
-
// - 最终执行: cmd /c "bat" "格式" "文件路径"
|
|
140
|
-
// 移除 AppliesTo 限制后,bat 需要过滤非图片文件
|
|
95
|
+
// ── bat 脚本:接收格式 + 文件/目录路径,直接调用 cis ──
|
|
96
|
+
// %1 = 格式, %2 %3 ... = Windows 传入的文件/目录路径
|
|
141
97
|
const batContent = `
|
|
142
98
|
@echo off
|
|
143
99
|
chcp 65001 >nul
|
|
144
100
|
setlocal enabledelayedexpansion
|
|
145
101
|
|
|
146
|
-
REM
|
|
147
|
-
set "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
REM Get cis.cmd path
|
|
151
|
-
for /f "delims=" %%c in ('where cis.cmd 2^>nul') do set "CIS_CMD=%%c"
|
|
152
|
-
|
|
153
|
-
REM Supported image extensions
|
|
154
|
-
set "SUPPORTED_EXT=.png;.jpg;.jpeg;.gif;.bmp;.tiff;.tif;.webp;.avif"
|
|
155
|
-
|
|
156
|
-
REM %1 = format (from subcommand), %2 = file path (from Windows)
|
|
157
|
-
if "%~1"=="" (
|
|
158
|
-
echo Error: No format specified.
|
|
159
|
-
timeout /t 2 >nul
|
|
160
|
-
goto :done
|
|
102
|
+
REM Find cis command in PATH
|
|
103
|
+
set "CIS_CMD="
|
|
104
|
+
for /f "delims=" %%c in ('where cis.cmd 2^>nul') do (
|
|
105
|
+
if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
|
|
161
106
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
set "fileList="
|
|
166
|
-
|
|
167
|
-
REM Windows passes file path as %2. If multiple files selected, use PowerShell.
|
|
168
|
-
REM Otherwise use direct argument.
|
|
169
|
-
if "%~2"=="" (
|
|
170
|
-
REM Multi-file via PowerShell (with retries for reliability)
|
|
171
|
-
for /f "delims=" %%i in ('powershell -ExecutionPolicy Bypass -File "!SCRIPT_DIR!\\cis_getfiles.ps1"') do (
|
|
172
|
-
if not "%%i"=="NO_FILES" (
|
|
173
|
-
call :add_if_image "%%i"
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
) else (
|
|
177
|
-
REM Single file or multiple via %2 (space-separated)
|
|
178
|
-
REM Split space-separated paths
|
|
179
|
-
for %%F in (%~2) do (
|
|
180
|
-
call :add_if_image "%%F"
|
|
107
|
+
if "!CIS_CMD!"=="" (
|
|
108
|
+
for /f "delims=" %%c in ('where cis 2^>nul') do (
|
|
109
|
+
if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
|
|
181
110
|
)
|
|
182
111
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
112
|
+
if "!CIS_CMD!"=="" (
|
|
113
|
+
echo [change-image-suffix] cis command not found. Run: npm install -g change-image-suffix
|
|
114
|
+
pause
|
|
115
|
+
exit /b 1
|
|
187
116
|
)
|
|
188
|
-
goto :done
|
|
189
117
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
118
|
+
if "%~1"=="" (
|
|
119
|
+
echo [change-image-suffix] Error: No format specified.
|
|
120
|
+
pause
|
|
121
|
+
exit /b 1
|
|
122
|
+
)
|
|
195
123
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if "!ext!"=="" exit /b
|
|
124
|
+
set "format=%~1"
|
|
125
|
+
shift
|
|
199
126
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
call set "ext_lower=%%ext_lower:I=i%%
|
|
211
|
-
call set "ext_lower=%%ext_lower:J=j%%
|
|
212
|
-
call set "ext_lower=%%ext_lower:K=k%%
|
|
213
|
-
call set "ext_lower=%%ext_lower:L=l%%
|
|
214
|
-
call set "ext_lower=%%ext_lower:M=m%%
|
|
215
|
-
call set "ext_lower=%%ext_lower:N=n%%
|
|
216
|
-
call set "ext_lower=%%ext_lower:O=o%%
|
|
217
|
-
call set "ext_lower=%%ext_lower:P=p%%
|
|
218
|
-
call set "ext_lower=%%ext_lower:Q=q%%
|
|
219
|
-
call set "ext_lower=%%ext_lower:R=r%%
|
|
220
|
-
call set "ext_lower=%%ext_lower:S=s%%
|
|
221
|
-
call set "ext_lower=%%ext_lower:T=t%%
|
|
222
|
-
call set "ext_lower=%%ext_lower:U=u%%
|
|
223
|
-
call set "ext_lower=%%ext_lower:V=v%%
|
|
224
|
-
call set "ext_lower=%%ext_lower:W=w%%
|
|
225
|
-
call set "ext_lower=%%ext_lower:X=x%%
|
|
226
|
-
call set "ext_lower=%%ext_lower:Y=y%%
|
|
227
|
-
call set "ext_lower=%%ext_lower:Z=z%%"
|
|
127
|
+
set "args="
|
|
128
|
+
:parse
|
|
129
|
+
if "%~1"=="" goto :run
|
|
130
|
+
if exist "%~1\\*" (
|
|
131
|
+
set "args=!args! -p "%~1""
|
|
132
|
+
) else (
|
|
133
|
+
set "args=!args! -f "%~1""
|
|
134
|
+
)
|
|
135
|
+
shift
|
|
136
|
+
goto :parse
|
|
228
137
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
138
|
+
:run
|
|
139
|
+
if "!args!"=="" (
|
|
140
|
+
echo [change-image-suffix] No files or directories to process.
|
|
141
|
+
pause
|
|
142
|
+
exit /b 1
|
|
233
143
|
)
|
|
234
|
-
exit /b
|
|
235
144
|
|
|
236
|
-
|
|
145
|
+
"!CIS_CMD!" -t !format! !args!
|
|
146
|
+
if !errorlevel! neq 0 pause
|
|
237
147
|
endlocal
|
|
238
|
-
exit
|
|
239
|
-
|
|
240
|
-
exit
|
|
241
|
-
|
|
242
|
-
exit
|
|
243
|
-
|
|
244
|
-
exit
|
|
245
|
-
|
|
246
148
|
`;
|
|
247
149
|
fs.writeFileSync(batPath, batContent, 'utf8');
|
|
248
150
|
// ── 格式列表(webp 排第一,其他按常见程度排序)──
|
|
@@ -251,15 +153,10 @@ exit
|
|
|
251
153
|
{ verb: 'jpg', label: '📷 JPG' },
|
|
252
154
|
{ verb: 'png', label: '🖼 PNG' },
|
|
253
155
|
{ verb: 'avif', label: '📺 AVIF' },
|
|
254
|
-
{ verb: 'gif', label: '🎞 GIF' },
|
|
255
156
|
{ verb: 'tiff', label: '📋 TIFF' },
|
|
256
|
-
{ verb: 'heif', label: '🍎 HEIF' },
|
|
257
|
-
{ verb: 'jp2', label: '📐 JPEG2000' },
|
|
258
157
|
];
|
|
259
|
-
// ── 使用 ExtendedSubCommandsKey
|
|
260
|
-
//
|
|
261
|
-
// 注意:文件右键和目录右键都使用 bat + PowerShell 获取选中文件
|
|
262
|
-
// 这样混合选择时也能处理所有选中项
|
|
158
|
+
// ── 使用 ExtendedSubCommandsKey 方式 ──
|
|
159
|
+
// 文件右键和目录右键均使用 bat 脚本,混合选择时自动分类文件和目录
|
|
263
160
|
const menuBases = [
|
|
264
161
|
{ base: 'HKCU\\Software\\Classes\\Directory\\Background\\shell\\cis', subMenu: 'Directory\\ContextMenus\\cis', arg: '-p "%V"' },
|
|
265
162
|
{ base: 'HKCU\\Software\\Classes\\Directory\\shell\\cis', subMenu: 'Directory\\ContextMenus\\cis_dir', useBat: true },
|
|
@@ -331,18 +228,13 @@ function uninstallContextMenu() {
|
|
|
331
228
|
}
|
|
332
229
|
catch { /* ignore */ }
|
|
333
230
|
}
|
|
334
|
-
//
|
|
231
|
+
// 删除批处理文件
|
|
335
232
|
const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
|
|
336
233
|
const batPath = path.join(appDataDir, 'cis_file.bat');
|
|
337
|
-
const ps1Path = path.join(appDataDir, 'cis_getfiles.ps1');
|
|
338
234
|
try {
|
|
339
235
|
fs.unlinkSync(batPath);
|
|
340
236
|
}
|
|
341
237
|
catch { /* ignore */ }
|
|
342
|
-
try {
|
|
343
|
-
fs.unlinkSync(ps1Path);
|
|
344
|
-
}
|
|
345
|
-
catch { /* ignore */ }
|
|
346
238
|
console.log('✅ 右键菜单已卸载');
|
|
347
239
|
}
|
|
348
240
|
// ─────────────────────────────────────────
|
|
@@ -400,7 +292,8 @@ function parseArgs() {
|
|
|
400
292
|
process.exit(0);
|
|
401
293
|
}
|
|
402
294
|
if (arg === '-v' || arg === '--version') {
|
|
403
|
-
|
|
295
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
296
|
+
console.log(`change-image-suffix v${pkg.version}`);
|
|
404
297
|
process.exit(0);
|
|
405
298
|
}
|
|
406
299
|
if (arg === '-p' || arg === '--path') {
|
|
@@ -488,12 +381,12 @@ function printHelp() {
|
|
|
488
381
|
cis uninstall-menu # 从 Windows 右键菜单移除
|
|
489
382
|
|
|
490
383
|
选项:
|
|
491
|
-
-f, --file <file>
|
|
384
|
+
-f, --file <file> 转换指定文件(可多个,空格分隔)
|
|
492
385
|
-p, --path <dir> 指定工作目录(默认: 当前目录)
|
|
493
386
|
-r, --recursive 递归搜索子目录
|
|
494
387
|
-d, --depth <n> 递归深度限制(需要 -r 选项)
|
|
495
|
-
-e, --extensions <ext>
|
|
496
|
-
-t, --to <format>
|
|
388
|
+
-e, --extensions <ext> 指定源后缀,逗号分隔(不含点号)
|
|
389
|
+
-t, --to <format> 目标格式: webp, jpg/jpeg, png, avif, tiff/tif(默认: webp)
|
|
497
390
|
-h, --help 显示帮助信息
|
|
498
391
|
-v, --version 显示版本信息
|
|
499
392
|
|
|
@@ -536,9 +429,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
536
429
|
const dir = path.dirname(inputPath);
|
|
537
430
|
const ext = path.extname(inputPath);
|
|
538
431
|
const basename = path.basename(inputPath, ext);
|
|
539
|
-
const originalExt = ext.slice(1).toLowerCase();
|
|
540
432
|
const targetExt = targetFormat;
|
|
541
|
-
// 源格式与目标格式相同时,直接覆盖
|
|
542
433
|
let coreName = basename;
|
|
543
434
|
const outputDir = path.join(dir, 'output');
|
|
544
435
|
// 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
|
|
@@ -573,34 +464,39 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
|
|
|
573
464
|
async function convertImage(inputPath, targetFormat, allInputFiles) {
|
|
574
465
|
try {
|
|
575
466
|
const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
|
|
576
|
-
// 确保 output 目录存在
|
|
577
467
|
const outputDir = path.dirname(outputPath);
|
|
578
468
|
if (!fs.existsSync(outputDir)) {
|
|
579
469
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
580
470
|
}
|
|
471
|
+
const srcExt = path.extname(inputPath).slice(1).toLowerCase();
|
|
472
|
+
const fmt = targetFormat.toLowerCase();
|
|
473
|
+
// 同格式直接复制,避免重新编码导致质量损失
|
|
474
|
+
if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
|
|
475
|
+
fs.copyFileSync(inputPath, outputPath);
|
|
476
|
+
return { success: true, outputPath };
|
|
477
|
+
}
|
|
478
|
+
if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
|
|
479
|
+
return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
|
|
480
|
+
}
|
|
581
481
|
const image = (0, sharp_1.default)(inputPath);
|
|
582
|
-
switch (
|
|
482
|
+
switch (fmt) {
|
|
583
483
|
case 'webp':
|
|
584
|
-
await image.webp({ quality:
|
|
484
|
+
await image.webp({ quality: 90 }).toFile(outputPath);
|
|
585
485
|
break;
|
|
586
486
|
case 'jpg':
|
|
587
487
|
case 'jpeg':
|
|
588
|
-
await image.jpeg({ quality:
|
|
488
|
+
await image.jpeg({ quality: 90 }).toFile(outputPath);
|
|
589
489
|
break;
|
|
590
490
|
case 'png':
|
|
591
|
-
await image.png({
|
|
592
|
-
break;
|
|
593
|
-
case 'gif':
|
|
594
|
-
await image.gif().toFile(outputPath);
|
|
491
|
+
await image.png({ compressionLevel: 6 }).toFile(outputPath);
|
|
595
492
|
break;
|
|
596
493
|
case 'tiff':
|
|
597
494
|
case 'tif':
|
|
598
|
-
await image.tiff({ quality:
|
|
495
|
+
await image.tiff({ quality: 90 }).toFile(outputPath);
|
|
599
496
|
break;
|
|
600
497
|
case 'avif':
|
|
601
|
-
await image.avif({ quality:
|
|
498
|
+
await image.avif({ quality: 90 }).toFile(outputPath);
|
|
602
499
|
break;
|
|
603
|
-
default: await image.toFormat(targetFormat).toFile(outputPath);
|
|
604
500
|
}
|
|
605
501
|
return { success: true, outputPath };
|
|
606
502
|
}
|
|
@@ -634,12 +530,11 @@ async function main() {
|
|
|
634
530
|
console.log(`🎯 目标格式: ${options.targetFormat}`);
|
|
635
531
|
console.log(`📦 待处理: ${files.length} 个文件\n`);
|
|
636
532
|
console.log('----------------------------------------\n');
|
|
637
|
-
const supportedExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
|
|
638
533
|
let totalSuccess = 0;
|
|
639
534
|
let totalFail = 0;
|
|
640
535
|
for (const filePath of files) {
|
|
641
536
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
642
|
-
if (!
|
|
537
|
+
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
643
538
|
console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
|
|
644
539
|
totalFail++;
|
|
645
540
|
continue;
|
|
@@ -674,8 +569,7 @@ async function main() {
|
|
|
674
569
|
}
|
|
675
570
|
if (stat.isFile()) {
|
|
676
571
|
const ext = path.extname(inputPath).slice(1).toLowerCase();
|
|
677
|
-
|
|
678
|
-
if (!supportedExts.includes(ext)) {
|
|
572
|
+
if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
|
|
679
573
|
console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
|
|
680
574
|
totalFail++;
|
|
681
575
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "change-image-suffix",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "批量转换图片格式的CLI工具,支持递归搜索、深度限制、指定后缀、Windows右键菜单等功能",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"assets/",
|
|
9
9
|
"README.md",
|
|
10
10
|
"LICENSE",
|
|
11
|
-
"CHANGELOG.md"
|
|
11
|
+
"CHANGELOG.md",
|
|
12
|
+
"scripts/postinstall.js",
|
|
13
|
+
"scripts/preuninstall.js"
|
|
12
14
|
],
|
|
13
15
|
"bin": {
|
|
14
16
|
"change-image-suffix": "./dist/index.js",
|
|
@@ -21,13 +23,15 @@
|
|
|
21
23
|
"scripts": {
|
|
22
24
|
"clean": "rimraf dist",
|
|
23
25
|
"build": "tsc",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"postinstall": "node scripts/postinstall.js",
|
|
28
|
+
"preuninstall": "node scripts/preuninstall.js",
|
|
24
29
|
"prepublishOnly": "npm run clean && npm run build",
|
|
25
30
|
"release": "node scripts/release.js",
|
|
26
31
|
"release:patch": "node scripts/release.js --patch",
|
|
27
32
|
"release:minor": "node scripts/release.js --minor",
|
|
28
33
|
"release:major": "node scripts/release.js --major",
|
|
29
|
-
"lint": "
|
|
30
|
-
"lint:fix": "echo \"Linting fix not configured yet\"",
|
|
34
|
+
"lint": "tsc --noEmit",
|
|
31
35
|
"typecheck": "tsc --noEmit"
|
|
32
36
|
},
|
|
33
37
|
"keywords": [
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// npm lifecycle hook: runs after npm install/update
|
|
2
|
+
// Auto-registers Windows context menu on global install
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
if (os.platform() === 'win32' && process.env.npm_config_global === 'true') {
|
|
8
|
+
try {
|
|
9
|
+
const indexJs = path.join(__dirname, '..', 'dist', 'index.js');
|
|
10
|
+
execSync(`node "${indexJs}" install-menu`, { stdio: 'inherit' });
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.warn('⚠️ Context menu auto-register failed. Run "cis install-menu" manually.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// npm lifecycle hook: runs before npm uninstall
|
|
2
|
+
// Cleans up Windows context menu to avoid leftover registry entries
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
if (os.platform() === 'win32') {
|
|
8
|
+
try {
|
|
9
|
+
const indexJs = path.join(__dirname, '..', 'dist', 'index.js');
|
|
10
|
+
execSync(`node "${indexJs}" uninstall-menu`, { stdio: 'inherit' });
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// Ignore cleanup errors — the menu will be orphaned but harmless
|
|
13
|
+
}
|
|
14
|
+
}
|