getwebp 1.0.0 → 1.1.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/README.md +212 -196
- package/dist/index.js +243 -83
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,318 +1,334 @@
|
|
|
1
1
|
# GetWebP CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Batch-convert JPG, PNG, BMP, WebP, HEIC, HEIF, and AVIF images to optimized WebP format from the command line.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
**Website:** [getwebp.com](https://getwebp.com) | **Pricing:** [getwebp.com/pricing](https://getwebp.com/pricing) | **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
|
|
6
11
|
|
|
7
12
|
---
|
|
8
13
|
|
|
9
|
-
##
|
|
14
|
+
## Feature Highlights
|
|
15
|
+
|
|
16
|
+
- **Batch conversion** -- process entire directories of images in one command
|
|
17
|
+
- **Parallel processing** -- concurrent workers scale to your CPU cores (Starter/Pro)
|
|
18
|
+
- **Recursive scanning** -- traverse subdirectories with `-r`
|
|
19
|
+
- **CI/CD ready** -- structured `--json` output with machine-parseable error codes
|
|
20
|
+
- **Offline-first licensing** -- JWT-based activation works without a persistent connection
|
|
21
|
+
- **Cross-platform** -- native binaries for macOS (ARM + Intel), Linux x64, and Windows x64
|
|
22
|
+
|
|
23
|
+
---
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Download the binary for your platform (see Installation below)
|
|
29
|
+
# 2. Convert a single image
|
|
30
|
+
getwebp convert photo.jpg
|
|
31
|
+
|
|
32
|
+
# 3. Convert an entire directory
|
|
33
|
+
getwebp convert ./images -o ./output
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Output:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
✓ photo.jpg
|
|
40
|
+
Done: 1 succeeded, 0 failed
|
|
41
|
+
Avg saved: 34.2%
|
|
42
|
+
```
|
|
12
43
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### Download Binary (Recommended)
|
|
49
|
+
|
|
50
|
+
Download the binary for your platform from the [Releases page](https://getwebp.com/download):
|
|
51
|
+
|
|
52
|
+
| Platform | Binary |
|
|
53
|
+
|---|---|
|
|
54
|
+
| macOS Apple Silicon (M1/M2/M3/M4) | `getwebp-macos-arm64` |
|
|
16
55
|
| macOS Intel | `getwebp-macos-x64` |
|
|
17
56
|
| Linux x64 | `getwebp-linux-x64` |
|
|
18
57
|
| Windows x64 | `getwebp-windows-x64.exe` |
|
|
19
58
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### macOS / Linux 安装
|
|
59
|
+
#### macOS / Linux
|
|
23
60
|
|
|
24
61
|
```bash
|
|
25
|
-
#
|
|
26
|
-
mv getwebp-macos-arm64 getwebp
|
|
27
|
-
# mv getwebp-macos-x64 getwebp # Intel Mac
|
|
28
|
-
# mv getwebp-linux-x64 getwebp # Linux
|
|
29
|
-
|
|
62
|
+
# Rename and make executable
|
|
63
|
+
mv getwebp-macos-arm64 getwebp # adjust filename for your platform
|
|
30
64
|
chmod +x getwebp
|
|
31
|
-
|
|
32
|
-
# 移动到 PATH 目录(需要 sudo)
|
|
33
65
|
sudo mv getwebp /usr/local/bin/
|
|
34
66
|
|
|
35
|
-
#
|
|
67
|
+
# Verify
|
|
36
68
|
getwebp --version
|
|
37
69
|
```
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
#### Windows
|
|
40
72
|
|
|
41
|
-
|
|
73
|
+
Rename `getwebp-windows-x64.exe` to `getwebp.exe` and place it in a directory on your `PATH` (e.g. `C:\Windows\System32`), or add its directory to the system `PATH` environment variable.
|
|
42
74
|
|
|
43
|
-
### macOS Gatekeeper
|
|
75
|
+
### macOS Gatekeeper
|
|
44
76
|
|
|
45
|
-
macOS
|
|
77
|
+
macOS may block unsigned binaries. Remove the quarantine attribute:
|
|
46
78
|
|
|
47
79
|
```bash
|
|
48
80
|
xattr -cr getwebp
|
|
49
81
|
```
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## 安全验证
|
|
54
|
-
|
|
55
|
-
每个 Release 都附带 SHA256 校验和文件(`*.sha256`),建议安装前验证:
|
|
83
|
+
### Verify Download (SHA256)
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
Each release includes SHA256 checksum files (`*.sha256`):
|
|
58
86
|
|
|
59
87
|
```bash
|
|
60
|
-
#
|
|
61
|
-
curl -LO https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-macos-arm64
|
|
62
|
-
curl -LO https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-macos-arm64.sha256
|
|
63
|
-
|
|
64
|
-
# 验证
|
|
88
|
+
# macOS / Linux
|
|
65
89
|
shasum -a 256 -c getwebp-macos-arm64.sha256
|
|
66
|
-
#
|
|
90
|
+
# Expected: getwebp-macos-arm64: OK
|
|
67
91
|
```
|
|
68
92
|
|
|
69
|
-
### Windows (PowerShell)
|
|
70
|
-
|
|
71
93
|
```powershell
|
|
72
|
-
#
|
|
73
|
-
Invoke-WebRequest -Uri "https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-win-x64.exe" -OutFile getwebp.exe
|
|
74
|
-
Invoke-WebRequest -Uri "https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-win-x64.exe.sha256" -OutFile getwebp.exe.sha256
|
|
75
|
-
|
|
76
|
-
# 验证
|
|
94
|
+
# Windows (PowerShell)
|
|
77
95
|
$expected = (Get-Content getwebp.exe.sha256).Split(' ')[0]
|
|
78
96
|
$actual = (Get-FileHash getwebp.exe -Algorithm SHA256).Hash.ToLower()
|
|
79
|
-
if ($expected -eq $actual) { Write-Host "
|
|
97
|
+
if ($expected -eq $actual) { Write-Host "Verified OK" } else { Write-Host "MISMATCH" }
|
|
80
98
|
```
|
|
81
99
|
|
|
82
100
|
---
|
|
83
101
|
|
|
84
|
-
##
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
### Quick Reference
|
|
85
105
|
|
|
86
|
-
|
|
106
|
+
| Command | Description |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `getwebp convert <path>` | Convert images to WebP |
|
|
109
|
+
| `getwebp auth <key>` | Activate a license key |
|
|
110
|
+
| `getwebp status` | Show license and version info |
|
|
111
|
+
| `getwebp logout` | Unbind license from this device |
|
|
112
|
+
| `getwebp --version` | Print CLI version |
|
|
113
|
+
| `getwebp --help` | Show help |
|
|
114
|
+
|
|
115
|
+
### `getwebp convert`
|
|
116
|
+
|
|
117
|
+
Convert one or more images to WebP format. Original files are never modified or deleted.
|
|
87
118
|
|
|
88
119
|
```
|
|
89
120
|
getwebp convert [path] [options]
|
|
90
121
|
```
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
**Options:**
|
|
93
124
|
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
| `-i, --input <path>` |
|
|
97
|
-
| `-o, --output <path>` |
|
|
98
|
-
| `-q, --quality <
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
| `--dry-run` |
|
|
102
|
-
| `--skip-existing` |
|
|
103
|
-
| `--json` | JSON
|
|
125
|
+
| Option | Description | Default |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `-i, --input <path>` | Input file or directory | -- |
|
|
128
|
+
| `-o, --output <path>` | Output directory | Same directory as source |
|
|
129
|
+
| `-q, --quality <n>` | WebP quality, 1--100 | `80` |
|
|
130
|
+
| `-r, --recursive` | Process subdirectories | off |
|
|
131
|
+
| `--concurrency <n>` | Parallel workers, max 32 (Starter/Pro) | CPU cores - 1 |
|
|
132
|
+
| `--dry-run` | Preview files without converting | off |
|
|
133
|
+
| `--skip-existing` | Skip if `.webp` already exists | off |
|
|
134
|
+
| `--json` | JSON output for CI / AI agents | off |
|
|
104
135
|
|
|
105
|
-
|
|
136
|
+
**Examples:**
|
|
106
137
|
|
|
107
138
|
```bash
|
|
108
|
-
#
|
|
139
|
+
# Convert a single image
|
|
109
140
|
getwebp convert photo.jpg
|
|
110
141
|
|
|
111
|
-
#
|
|
112
|
-
getwebp convert ./images
|
|
113
|
-
|
|
114
|
-
# 指定输入目录和输出目录
|
|
142
|
+
# Convert a directory with custom output
|
|
115
143
|
getwebp convert -i ./src/images -o ./dist/images
|
|
116
144
|
|
|
117
|
-
#
|
|
118
|
-
getwebp convert ./images -q
|
|
145
|
+
# High quality, skip already-converted files
|
|
146
|
+
getwebp convert ./images -q 95 --skip-existing
|
|
119
147
|
|
|
120
|
-
#
|
|
121
|
-
getwebp convert ./images
|
|
122
|
-
|
|
123
|
-
# 跳过已存在的 WebP 文件
|
|
124
|
-
getwebp convert ./images --skip-existing
|
|
125
|
-
|
|
126
|
-
# 递归处理子目录中的图片(Starter/Pro)
|
|
127
|
-
getwebp convert ./images --recursive
|
|
148
|
+
# Recursive scan of subdirectories
|
|
149
|
+
getwebp convert ./images -r
|
|
128
150
|
|
|
129
|
-
#
|
|
151
|
+
# Parallel processing with 8 workers (Starter/Pro)
|
|
130
152
|
getwebp convert ./images --concurrency 8
|
|
131
153
|
|
|
132
|
-
#
|
|
133
|
-
getwebp convert ./images --
|
|
154
|
+
# Preview what would be converted (no writes)
|
|
155
|
+
getwebp convert ./images --dry-run
|
|
134
156
|
|
|
135
|
-
#
|
|
136
|
-
getwebp convert
|
|
157
|
+
# JSON output for CI/CD pipelines
|
|
158
|
+
getwebp convert ./images --json
|
|
137
159
|
```
|
|
138
160
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
### `getwebp auth` — 激活 Pro 授权
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
getwebp auth <license-key>
|
|
145
|
-
```
|
|
161
|
+
### `getwebp auth`
|
|
146
162
|
|
|
147
|
-
|
|
163
|
+
Activate a license key to unlock Starter or Pro features.
|
|
148
164
|
|
|
149
165
|
```bash
|
|
150
|
-
# 使用许可证 Key 激活 Pro
|
|
151
166
|
getwebp auth XXXX-XXXX-XXXX-XXXX
|
|
167
|
+
```
|
|
152
168
|
|
|
153
|
-
|
|
169
|
+
```
|
|
170
|
+
Verifying license...
|
|
171
|
+
✓ Activated! Starter plan unlocked.
|
|
154
172
|
```
|
|
155
173
|
|
|
156
|
-
|
|
174
|
+
Purchase a license at [getwebp.com/pricing](https://getwebp.com/pricing).
|
|
157
175
|
|
|
158
|
-
|
|
176
|
+
### `getwebp status`
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
Display current license info, plan, expiry date, and device usage.
|
|
161
179
|
|
|
162
180
|
```bash
|
|
163
|
-
# 查看当前激活状态、许可证信息和到期时间
|
|
164
181
|
getwebp status
|
|
165
|
-
|
|
166
|
-
# 以 JSON 格式输出(适合脚本解析)
|
|
167
|
-
getwebp status --json
|
|
168
182
|
```
|
|
169
183
|
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
### `getwebp logout` — 退出登录
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
# 清除本地存储的许可证信息
|
|
176
|
-
getwebp logout
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## JSON 输出格式参考
|
|
182
|
-
|
|
183
|
-
所有命令均支持 `--json` 参数,适用于 AI Agent 和 CI/CD 脚本。
|
|
184
|
-
|
|
185
|
-
**成功响应**(`success: true`):
|
|
186
|
-
```json
|
|
187
|
-
{ "success": true, "data": { ... } }
|
|
188
184
|
```
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
Version : 1.0.1
|
|
186
|
+
Mode : Starter
|
|
187
|
+
License : xxxx-xxxx-xxxx-A1B2
|
|
188
|
+
Expires : 2027-04-01
|
|
189
|
+
Devices : 1 / 3 used
|
|
193
190
|
```
|
|
194
191
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|------------|------|-----------|
|
|
199
|
-
| `missing_input` | 未指定输入路径 | — |
|
|
200
|
-
| `network_unreachable` | 无法连接服务器 | ✅ |
|
|
201
|
-
| `not_activated` | 未激活 Pro 许可证 | — |
|
|
202
|
-
| `device_not_found` | 设备未找到(可能已解绑) | — |
|
|
203
|
-
| `invalid_token` | Token 无效或已过期 | — |
|
|
204
|
-
| `unknown_error` | 未知错误 | ✅ |
|
|
205
|
-
|
|
206
|
-
**convert 成功示例:**
|
|
207
|
-
```json
|
|
208
|
-
{ "status": "success", "total": 3, "success": 3, "failed": 0, "results": [...] }
|
|
192
|
+
```bash
|
|
193
|
+
# Machine-readable output
|
|
194
|
+
getwebp status --json
|
|
209
195
|
```
|
|
210
196
|
|
|
211
|
-
**convert 错误示例(缺少路径):**
|
|
212
197
|
```json
|
|
213
|
-
{
|
|
198
|
+
{
|
|
199
|
+
"success": true,
|
|
200
|
+
"data": {
|
|
201
|
+
"version": "1.0.1",
|
|
202
|
+
"mode": "starter",
|
|
203
|
+
"licenseKeySuffix": "A1B2",
|
|
204
|
+
"expiresAt": "2027-04-01T00:00:00.000Z",
|
|
205
|
+
"devicesUsed": 1,
|
|
206
|
+
"devicesLimit": 3
|
|
207
|
+
}
|
|
208
|
+
}
|
|
214
209
|
```
|
|
215
210
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## 退出码
|
|
211
|
+
### `getwebp logout`
|
|
221
212
|
|
|
222
|
-
|
|
223
|
-
|--------|------|
|
|
224
|
-
| `0` | 成功 |
|
|
225
|
-
| `1` | 失败(详见 stderr 或 JSON 的 `error` 字段) |
|
|
213
|
+
Unbind the license from this device, freeing a device slot. Requires network access.
|
|
226
214
|
|
|
227
|
-
|
|
215
|
+
```bash
|
|
216
|
+
getwebp logout # interactive confirmation
|
|
217
|
+
getwebp logout --force # skip confirmation (CI-safe)
|
|
218
|
+
```
|
|
228
219
|
|
|
229
220
|
---
|
|
230
221
|
|
|
231
|
-
##
|
|
222
|
+
## JSON Output
|
|
232
223
|
|
|
233
|
-
|
|
234
|
-
|------|:----:|:---:|
|
|
235
|
-
| 每次最多处理张数 | 10 张 | 无限制 |
|
|
236
|
-
| 处理速度 | 每张等待 3 秒 | 无限制 |
|
|
237
|
-
| 并发处理 | ❌ | ✅(最多 4 并发) |
|
|
238
|
-
| 递归目录(`-r`) | ❌ | ✅ |
|
|
239
|
-
| 自定义质量(`-q`) | ✅ | ✅ |
|
|
240
|
-
| JSON 输出(`--json`) | ✅ | ✅ |
|
|
241
|
-
|
|
242
|
-
升级 Pro:[https://getwebp.com/pricing](https://getwebp.com/pricing)
|
|
243
|
-
|
|
244
|
-
---
|
|
224
|
+
All commands support `--json` for structured output on stdout. Human-readable messages go to stderr.
|
|
245
225
|
|
|
246
|
-
|
|
226
|
+
**Success:**
|
|
247
227
|
|
|
248
|
-
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"success": true,
|
|
231
|
+
"status": "success",
|
|
232
|
+
"total": 3,
|
|
233
|
+
"successCount": 3,
|
|
234
|
+
"failedCount": 0,
|
|
235
|
+
"results": [
|
|
236
|
+
{ "file": "photo.jpg", "status": "success", "originalSize": 204800, "newSize": 133120, "savedRatio": 0.35, "saved": "35.0%" }
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
249
240
|
|
|
250
|
-
|
|
241
|
+
**Error:**
|
|
251
242
|
|
|
252
|
-
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"success": false,
|
|
246
|
+
"status": "error",
|
|
247
|
+
"error": "missing_input",
|
|
248
|
+
"message": "Please specify an input path"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
253
251
|
|
|
254
|
-
**
|
|
252
|
+
**Error codes:**
|
|
255
253
|
|
|
256
|
-
|
|
254
|
+
| Code | Meaning | Retryable |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| `missing_input` | No input path specified | No |
|
|
257
|
+
| `network_unreachable` | Cannot reach API server | Yes |
|
|
258
|
+
| `not_activated` | No active license on device | No |
|
|
259
|
+
| `device_not_found` | Device already unbound | No |
|
|
260
|
+
| `invalid_token` | Token expired or revoked | No |
|
|
261
|
+
| `unknown_error` | Unclassified error | Yes |
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
xattr -cr getwebp
|
|
260
|
-
```
|
|
263
|
+
> **Note:** `getwebp logout --json` automatically skips the interactive confirmation prompt. You can also use `--force` to skip it in non-JSON mode.
|
|
261
264
|
|
|
262
265
|
---
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
A:在 [getwebp.com/pricing](https://getwebp.com/pricing) 购买许可证后,运行:
|
|
267
|
+
## Exit Codes
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
| Code | Meaning |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `0` | All files processed successfully |
|
|
272
|
+
| `1` | General error |
|
|
273
|
+
| `2` | Partial failure (some files failed) |
|
|
274
|
+
| `3` | License / auth error |
|
|
275
|
+
| `4` | Network error |
|
|
271
276
|
|
|
272
|
-
|
|
277
|
+
See [docs/exit-codes.md](./docs/exit-codes.md) for detailed descriptions and retry guidance.
|
|
273
278
|
|
|
274
279
|
---
|
|
275
280
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
A:Free 模式每次最多转换 10 张图片,且每张图片处理前会等待 3 秒。不支持并发处理和递归子目录。升级 Pro 可解锁全部功能。
|
|
281
|
+
## Free vs Starter vs Pro
|
|
279
282
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
| Feature | Free | Starter | Pro |
|
|
284
|
+
|---|:---:|:---:|:---:|
|
|
285
|
+
| Files per run | 10 | Unlimited | Unlimited |
|
|
286
|
+
| Processing speed | 3s delay per file | Instant | Instant |
|
|
287
|
+
| Parallel workers | 1 (serial) | Auto (CPU cores - 1) | Auto (CPU cores - 1) |
|
|
288
|
+
| Custom `--concurrency` | -- | Up to 32 | Up to 32 |
|
|
289
|
+
| Recursive `-r` | Yes | Yes | Yes |
|
|
290
|
+
| Custom quality `-q` | Yes | Yes | Yes |
|
|
291
|
+
| JSON output `--json` | Yes | Yes | Yes |
|
|
292
|
+
| `--dry-run` / `--skip-existing` | Yes | Yes | Yes |
|
|
283
293
|
|
|
284
|
-
|
|
294
|
+
Upgrade at [getwebp.com/pricing](https://getwebp.com/pricing), then activate:
|
|
285
295
|
|
|
286
296
|
```bash
|
|
287
|
-
getwebp
|
|
297
|
+
getwebp auth <your-license-key>
|
|
288
298
|
```
|
|
289
299
|
|
|
290
|
-
配合 Pro 许可证,可以在 CI 环境中批量处理大量图片,无数量和速度限制。
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
**Q:支持退款吗?**
|
|
295
|
-
|
|
296
|
-
A:购买后如有问题,请联系 [getwebp.com](https://getwebp.com/contact) 客服,我们提供合理的退款支持。
|
|
297
|
-
|
|
298
300
|
---
|
|
299
301
|
|
|
300
|
-
|
|
302
|
+
## Supported Formats
|
|
301
303
|
|
|
302
|
-
|
|
304
|
+
| Input | Output |
|
|
305
|
+
|---|---|
|
|
306
|
+
| JPG / JPEG | WebP |
|
|
307
|
+
| PNG | WebP |
|
|
308
|
+
| BMP | WebP |
|
|
309
|
+
| WebP | WebP (re-encoded) |
|
|
310
|
+
| HEIC | WebP |
|
|
311
|
+
| HEIF | WebP |
|
|
312
|
+
| AVIF | WebP |
|
|
303
313
|
|
|
304
314
|
---
|
|
305
315
|
|
|
306
|
-
|
|
316
|
+
## Global Options
|
|
307
317
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
318
|
+
| Option | Description |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `--verbose` | Enable verbose output |
|
|
321
|
+
| `--debug` | Enable debug output (implies `--verbose`) |
|
|
322
|
+
| `--json` | JSON output (available on all commands) |
|
|
323
|
+
| `--version`, `-v` | Print version |
|
|
324
|
+
| `--help` | Show help |
|
|
311
325
|
|
|
312
326
|
---
|
|
313
327
|
|
|
314
|
-
##
|
|
315
|
-
|
|
316
|
-
当前版本:**2.1.0**
|
|
328
|
+
## Links
|
|
317
329
|
|
|
318
|
-
|
|
330
|
+
- **Website:** [getwebp.com](https://getwebp.com)
|
|
331
|
+
- **Pricing:** [getwebp.com/pricing](https://getwebp.com/pricing)
|
|
332
|
+
- **Download:** [getwebp.com/download](https://getwebp.com/download)
|
|
333
|
+
- **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
|
|
334
|
+
- **Dashboard:** [getwebp.com/dashboard](https://getwebp.com/dashboard) (manage devices and licenses)
|
package/dist/index.js
CHANGED
|
@@ -9,29 +9,130 @@ import jwt from "jsonwebtoken";
|
|
|
9
9
|
|
|
10
10
|
// src/core/config.ts
|
|
11
11
|
import Conf from "conf";
|
|
12
|
-
import
|
|
12
|
+
import crypto2 from "node:crypto";
|
|
13
|
+
import { unlinkSync } from "node:fs";
|
|
14
|
+
import os2 from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
// src/utils/debug.ts
|
|
18
|
+
var _verbose = false;
|
|
19
|
+
var _debug = false;
|
|
20
|
+
var _sink = (msg) => process.stderr.write(msg);
|
|
21
|
+
function setVerbose(v) {
|
|
22
|
+
_verbose = v;
|
|
23
|
+
}
|
|
24
|
+
function setDebug(v) {
|
|
25
|
+
_debug = v;
|
|
26
|
+
if (v) _verbose = true;
|
|
27
|
+
}
|
|
28
|
+
function debugLog(...args) {
|
|
29
|
+
if (_debug) _sink("[debug] " + args.join(" ") + "\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/core/machine-id.ts
|
|
33
|
+
import { execSync, exec } from "node:child_process";
|
|
13
34
|
import crypto from "node:crypto";
|
|
14
35
|
import os from "node:os";
|
|
36
|
+
function getRawId() {
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
try {
|
|
39
|
+
if (platform === "darwin") {
|
|
40
|
+
const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8" });
|
|
41
|
+
const match = out.match(/IOPlatformUUID[^=]+=\s*"([^"]+)"/);
|
|
42
|
+
if (match) return match[1].toLowerCase();
|
|
43
|
+
} else if (platform === "linux") {
|
|
44
|
+
const out = execSync("( cat /var/lib/dbus/machine-id /etc/machine-id 2>/dev/null || hostname ) | head -n 1", {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
shell: "/bin/sh"
|
|
47
|
+
});
|
|
48
|
+
return out.trim();
|
|
49
|
+
} else if (platform === "win32") {
|
|
50
|
+
const out = execSync(
|
|
51
|
+
"REG QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
|
|
52
|
+
{ encoding: "utf8" }
|
|
53
|
+
);
|
|
54
|
+
const match = out.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
|
|
55
|
+
if (match) return match[1].trim();
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
return `${os.hostname()}:${os.userInfo().username}`;
|
|
60
|
+
}
|
|
61
|
+
function hashId(raw) {
|
|
62
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
function machineIdSync() {
|
|
65
|
+
return hashId(getRawId());
|
|
66
|
+
}
|
|
67
|
+
function machineId() {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const platform = process.platform;
|
|
70
|
+
if (platform === "darwin") {
|
|
71
|
+
exec("ioreg -rd1 -c IOPlatformExpertDevice", (err, stdout) => {
|
|
72
|
+
if (!err) {
|
|
73
|
+
const match = stdout.match(/IOPlatformUUID[^=]+=\s*"([^"]+)"/);
|
|
74
|
+
if (match) return resolve(hashId(match[1].toLowerCase()));
|
|
75
|
+
}
|
|
76
|
+
resolve(hashId(getRawId()));
|
|
77
|
+
});
|
|
78
|
+
} else if (platform === "linux") {
|
|
79
|
+
exec("( cat /var/lib/dbus/machine-id /etc/machine-id 2>/dev/null || hostname ) | head -n 1", { shell: "/bin/sh" }, (err, stdout) => {
|
|
80
|
+
resolve(hashId(err ? getRawId() : stdout.trim()));
|
|
81
|
+
});
|
|
82
|
+
} else if (platform === "win32") {
|
|
83
|
+
exec("REG QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", (err, stdout) => {
|
|
84
|
+
if (!err) {
|
|
85
|
+
const match = stdout.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
|
|
86
|
+
if (match) return resolve(hashId(match[1].trim()));
|
|
87
|
+
}
|
|
88
|
+
resolve(hashId(getRawId()));
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
resolve(hashId(getRawId()));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/config.ts
|
|
15
97
|
function getMachineKey() {
|
|
16
98
|
try {
|
|
17
99
|
const id = machineIdSync();
|
|
18
|
-
return
|
|
100
|
+
return crypto2.createHash("sha256").update(id).digest("hex");
|
|
19
101
|
} catch {
|
|
20
|
-
|
|
21
|
-
"warn: Could not read machine ID, falling back to hostname
|
|
102
|
+
debugLog(
|
|
103
|
+
"warn: Could not read machine ID, falling back to hostname.",
|
|
104
|
+
"Token may become invalid if hostname changes."
|
|
22
105
|
);
|
|
23
|
-
return
|
|
106
|
+
return crypto2.createHash("sha256").update(`${os2.hostname()}:${os2.userInfo().username}`).digest("hex");
|
|
24
107
|
}
|
|
25
108
|
}
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
109
|
+
var _store = null;
|
|
110
|
+
function getStore() {
|
|
111
|
+
if (!_store) {
|
|
112
|
+
const confOpts = {
|
|
113
|
+
projectName: "getwebp",
|
|
114
|
+
encryptionKey: getMachineKey(),
|
|
115
|
+
...process.env.GETWEBP_CONFIG_DIR ? { cwd: process.env.GETWEBP_CONFIG_DIR } : {}
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
_store = new Conf(confOpts);
|
|
119
|
+
} catch {
|
|
120
|
+
debugLog("warn: Corrupt config file detected, clearing and reinitialising.");
|
|
121
|
+
const configDir = process.env.GETWEBP_CONFIG_DIR ?? (process.platform === "darwin" ? path.join(os2.homedir(), "Library", "Preferences", "getwebp-nodejs") : path.join(os2.homedir(), ".config", "getwebp"));
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(path.join(configDir, "config.json"));
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
_store = new Conf(confOpts);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return _store;
|
|
130
|
+
}
|
|
31
131
|
function getConfig() {
|
|
32
|
-
return
|
|
132
|
+
return getStore().store;
|
|
33
133
|
}
|
|
34
134
|
function saveConfig(data) {
|
|
135
|
+
const store = getStore();
|
|
35
136
|
for (const [key, value] of Object.entries(data)) {
|
|
36
137
|
if (value === void 0) {
|
|
37
138
|
store.delete(key);
|
|
@@ -42,23 +143,23 @@ function saveConfig(data) {
|
|
|
42
143
|
}
|
|
43
144
|
|
|
44
145
|
// src/core/device.ts
|
|
45
|
-
import
|
|
46
|
-
import crypto2 from "node:crypto";
|
|
146
|
+
import crypto3 from "node:crypto";
|
|
47
147
|
async function getDeviceId() {
|
|
48
148
|
const id = await machineId();
|
|
49
|
-
return
|
|
149
|
+
return crypto3.createHash("sha256").update(id).digest("hex");
|
|
50
150
|
}
|
|
51
151
|
|
|
52
152
|
// src/core/constants.ts
|
|
53
|
-
import
|
|
153
|
+
import os3 from "node:os";
|
|
54
154
|
var FREE_TIER = {
|
|
55
155
|
FILE_LIMIT: 10,
|
|
56
156
|
DELAY_MS: 3e3
|
|
57
157
|
};
|
|
58
158
|
var DEFAULTS = {
|
|
59
159
|
QUALITY: 80,
|
|
60
|
-
CONCURRENCY: Math.max(1,
|
|
160
|
+
CONCURRENCY: Math.max(1, os3.cpus().length - 1)
|
|
61
161
|
};
|
|
162
|
+
var AUTO_QUALITY_SENTINEL = -1;
|
|
62
163
|
var NETWORK = {
|
|
63
164
|
HEARTBEAT_TIMEOUT_MS: 3e3,
|
|
64
165
|
API_TIMEOUT_MS: 5e3,
|
|
@@ -126,7 +227,7 @@ async function activate(licenseKey) {
|
|
|
126
227
|
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
127
228
|
}
|
|
128
229
|
}
|
|
129
|
-
async function checkLicense() {
|
|
230
|
+
async function checkLicense(warn) {
|
|
130
231
|
const config = getConfig();
|
|
131
232
|
if (!config?.token) return { valid: false, plan: "free" };
|
|
132
233
|
try {
|
|
@@ -145,7 +246,8 @@ async function checkLicense() {
|
|
|
145
246
|
return { valid: false, plan: "free", expired: true };
|
|
146
247
|
}
|
|
147
248
|
if (err instanceof Error && err.message.includes("JWT_PUBLIC_KEY")) {
|
|
148
|
-
process.stderr.write(
|
|
249
|
+
const w = warn ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
250
|
+
w("warn: JWT public key not found. License validation skipped.");
|
|
149
251
|
}
|
|
150
252
|
return { valid: false, plan: "free" };
|
|
151
253
|
}
|
|
@@ -212,44 +314,55 @@ function resolveExitCode(results) {
|
|
|
212
314
|
}
|
|
213
315
|
|
|
214
316
|
// src/core/processor.ts
|
|
215
|
-
import { encode as encodeWebp } from "@jsquash/webp";
|
|
216
|
-
import { unlinkSync, constants as fsConstants } from "node:fs";
|
|
317
|
+
import { encode as encodeWebp, decode as decodeWebp2 } from "@jsquash/webp";
|
|
318
|
+
import { unlinkSync as unlinkSync2, constants as fsConstants } from "node:fs";
|
|
217
319
|
import fs3 from "node:fs/promises";
|
|
218
|
-
import
|
|
320
|
+
import path4 from "node:path";
|
|
219
321
|
import pLimit from "p-limit";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
var _verbose = false;
|
|
223
|
-
var _debug = false;
|
|
224
|
-
function setVerbose(v) {
|
|
225
|
-
_verbose = v;
|
|
226
|
-
}
|
|
227
|
-
function setDebug(v) {
|
|
228
|
-
_debug = v;
|
|
229
|
-
if (v) _verbose = true;
|
|
230
|
-
}
|
|
231
|
-
function debugLog(...args) {
|
|
232
|
-
if (_debug) process.stderr.write("[debug] " + args.join(" ") + "\n");
|
|
233
|
-
}
|
|
322
|
+
import { applySizeGuard } from "@getwebp/core/size-guard";
|
|
323
|
+
import { findOptimalQuality } from "@getwebp/core/auto-quality";
|
|
234
324
|
|
|
235
325
|
// src/core/codecs.ts
|
|
236
326
|
import { decode as decodeJpeg } from "@jsquash/jpeg";
|
|
237
327
|
import { decode as decodePng } from "@jsquash/png";
|
|
238
328
|
import { decode as decodeWebp } from "@jsquash/webp";
|
|
329
|
+
import { decode as decodeAvifWasm } from "@jsquash/avif";
|
|
239
330
|
import Bmp from "bmp-js";
|
|
240
331
|
import fs from "node:fs/promises";
|
|
241
|
-
import
|
|
332
|
+
import path2 from "node:path";
|
|
333
|
+
async function decodeHeic(buffer) {
|
|
334
|
+
const heicDecode = (await import("heic-decode")).default;
|
|
335
|
+
const { width, height, data } = await heicDecode({ buffer: new Uint8Array(buffer) });
|
|
336
|
+
return new ImageData(
|
|
337
|
+
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
|
338
|
+
width,
|
|
339
|
+
height
|
|
340
|
+
);
|
|
341
|
+
}
|
|
242
342
|
var decoders = {
|
|
243
343
|
".jpg": decodeJpeg,
|
|
244
344
|
".jpeg": decodeJpeg,
|
|
245
345
|
".png": decodePng,
|
|
246
|
-
".webp": decodeWebp
|
|
346
|
+
".webp": decodeWebp,
|
|
347
|
+
".avif": async (buf) => {
|
|
348
|
+
const result = await decodeAvifWasm(buf);
|
|
349
|
+
if (!result) throw new Error("AVIF decode returned null");
|
|
350
|
+
return result;
|
|
351
|
+
},
|
|
352
|
+
".heic": decodeHeic,
|
|
353
|
+
".heif": decodeHeic
|
|
247
354
|
};
|
|
248
355
|
var MAGIC_VALIDATORS = {
|
|
249
356
|
".jpg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
|
|
250
357
|
".jpeg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
|
|
251
358
|
".png": (b) => b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71,
|
|
252
359
|
".webp": (b) => b.length > 11 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80,
|
|
360
|
+
".heic": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 99 || // "heic"
|
|
361
|
+
b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 102 || // "heif"
|
|
362
|
+
b[8] === 109 && b[9] === 105 && b[10] === 102 && b[11] === 49),
|
|
363
|
+
".heif": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 99 || b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 102 || b[8] === 109 && b[9] === 105 && b[10] === 102 && b[11] === 49),
|
|
364
|
+
".avif": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 97 && b[9] === 118 && b[10] === 105 && b[11] === 102 || // "avif"
|
|
365
|
+
b[8] === 97 && b[9] === 118 && b[10] === 105 && b[11] === 115),
|
|
253
366
|
".bmp": (b) => b[0] === 66 && b[1] === 77
|
|
254
367
|
};
|
|
255
368
|
var BMP_MAX_DIMENSION = 65535;
|
|
@@ -272,7 +385,7 @@ async function decodeBmp(buffer) {
|
|
|
272
385
|
return new ImageData(new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), bmp.width, bmp.height);
|
|
273
386
|
}
|
|
274
387
|
async function decodeImage(filePath) {
|
|
275
|
-
const ext =
|
|
388
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
276
389
|
const buffer = await fs.readFile(filePath);
|
|
277
390
|
if (buffer.byteLength === 0) {
|
|
278
391
|
throw new Error(`File is empty (0 bytes): ${filePath}`);
|
|
@@ -303,13 +416,13 @@ async function decodeImage(filePath) {
|
|
|
303
416
|
|
|
304
417
|
// src/core/file-scanner.ts
|
|
305
418
|
import fs2 from "node:fs/promises";
|
|
306
|
-
import
|
|
307
|
-
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp"]);
|
|
308
|
-
|
|
419
|
+
import path3 from "node:path";
|
|
420
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp", ".heic", ".heif", ".avif"]);
|
|
421
|
+
var defaultWarn = (msg) => process.stderr.write(msg + "\n");
|
|
422
|
+
async function collectImageFiles(input, recursive, warn = defaultWarn) {
|
|
309
423
|
const lstat = await fs2.lstat(input);
|
|
310
424
|
if (lstat.isSymbolicLink()) {
|
|
311
|
-
|
|
312
|
-
`);
|
|
425
|
+
warn(`Warning: ${input} is a symlink, skipping`);
|
|
313
426
|
return [];
|
|
314
427
|
}
|
|
315
428
|
if (lstat.isFile()) return [input];
|
|
@@ -317,18 +430,17 @@ async function collectImageFiles(input, recursive) {
|
|
|
317
430
|
try {
|
|
318
431
|
entries = await fs2.readdir(input, { withFileTypes: true });
|
|
319
432
|
} catch {
|
|
320
|
-
|
|
321
|
-
`);
|
|
433
|
+
warn(`Warning: cannot read directory ${input}, skipping`);
|
|
322
434
|
return [];
|
|
323
435
|
}
|
|
324
436
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
325
437
|
const results = [];
|
|
326
438
|
const subdirPromises = [];
|
|
327
439
|
for (const entry of entries) {
|
|
328
|
-
const fullPath =
|
|
440
|
+
const fullPath = path3.join(input, entry.name);
|
|
329
441
|
if (entry.isDirectory() && recursive && !entry.isSymbolicLink()) {
|
|
330
|
-
subdirPromises.push(collectImageFiles(fullPath, recursive));
|
|
331
|
-
} else if (entry.isFile() && !entry.isSymbolicLink() && IMAGE_EXTS.has(
|
|
442
|
+
subdirPromises.push(collectImageFiles(fullPath, recursive, warn));
|
|
443
|
+
} else if (entry.isFile() && !entry.isSymbolicLink() && IMAGE_EXTS.has(path3.extname(entry.name).toLowerCase())) {
|
|
332
444
|
results.push(fullPath);
|
|
333
445
|
}
|
|
334
446
|
}
|
|
@@ -340,10 +452,11 @@ async function collectImageFiles(input, recursive) {
|
|
|
340
452
|
// src/core/wasm-init.ts
|
|
341
453
|
import { readFileSync } from "node:fs";
|
|
342
454
|
import { createRequire } from "node:module";
|
|
343
|
-
import { init as initPngCodec } from "@jsquash/png/decode";
|
|
344
|
-
import { init as initJpegDec } from "@jsquash/jpeg/decode";
|
|
345
|
-
import { init as initWebpDec } from "@jsquash/webp/decode";
|
|
346
|
-
import { init as initWebpEnc } from "@jsquash/webp/encode";
|
|
455
|
+
import { init as initPngCodec } from "@jsquash/png/decode.js";
|
|
456
|
+
import { init as initJpegDec } from "@jsquash/jpeg/decode.js";
|
|
457
|
+
import { init as initWebpDec } from "@jsquash/webp/decode.js";
|
|
458
|
+
import { init as initWebpEnc } from "@jsquash/webp/encode.js";
|
|
459
|
+
import { init as initAvifDec } from "@jsquash/avif/decode.js";
|
|
347
460
|
var _require = createRequire(import.meta.url);
|
|
348
461
|
var initialized = false;
|
|
349
462
|
async function initWasm() {
|
|
@@ -357,9 +470,15 @@ async function initWasm() {
|
|
|
357
470
|
await initWebpDec(webpDecWasm);
|
|
358
471
|
const webpEncWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/webp/codec/enc/webp_enc.wasm")));
|
|
359
472
|
await initWebpEnc(webpEncWasm);
|
|
473
|
+
const avifDecWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/avif/codec/dec/avif_dec.wasm")));
|
|
474
|
+
await initAvifDec(avifDecWasm);
|
|
360
475
|
}
|
|
361
476
|
|
|
362
477
|
// src/core/processor.ts
|
|
478
|
+
var HEIC_AVIF_EXTS = /* @__PURE__ */ new Set([".heic", ".heif", ".avif"]);
|
|
479
|
+
function isHeicOrAvif(filePath) {
|
|
480
|
+
return HEIC_AVIF_EXTS.has(path4.extname(filePath).toLowerCase());
|
|
481
|
+
}
|
|
363
482
|
async function processImages(options) {
|
|
364
483
|
const { input, output, quality, plan, out } = options;
|
|
365
484
|
const isFree = plan === "free";
|
|
@@ -379,7 +498,7 @@ async function processImages(options) {
|
|
|
379
498
|
}
|
|
380
499
|
await initWasm();
|
|
381
500
|
const recursive = options.recursive ?? false;
|
|
382
|
-
const files = await collectImageFiles(input, recursive);
|
|
501
|
+
const files = await collectImageFiles(input, recursive, (msg) => out.warn(msg));
|
|
383
502
|
debugLog("collected", files.length, "files");
|
|
384
503
|
if (isFree && files.length > 0) {
|
|
385
504
|
out.warn("Free plan: max 10 files, 3s delay between each.");
|
|
@@ -389,23 +508,23 @@ async function processImages(options) {
|
|
|
389
508
|
const skipped = isFree ? Math.max(0, files.length - FREE_TIER.FILE_LIMIT) : 0;
|
|
390
509
|
const outputPaths = /* @__PURE__ */ new Map();
|
|
391
510
|
for (const file of targets) {
|
|
392
|
-
const dir = output ??
|
|
393
|
-
const outName =
|
|
394
|
-
const outPath =
|
|
511
|
+
const dir = output ?? path4.dirname(file);
|
|
512
|
+
const outName = path4.basename(file, path4.extname(file)) + ".webp";
|
|
513
|
+
const outPath = path4.resolve(dir, outName);
|
|
395
514
|
const existing = outputPaths.get(outPath) ?? [];
|
|
396
515
|
existing.push(file);
|
|
397
516
|
outputPaths.set(outPath, existing);
|
|
398
517
|
}
|
|
399
518
|
for (const [outPath, sources] of outputPaths) {
|
|
400
519
|
if (sources.length > 1) {
|
|
401
|
-
out.warn(`Conflict: ${sources.join(", ")} all map to ${
|
|
520
|
+
out.warn(`Conflict: ${sources.join(", ")} all map to ${path4.basename(outPath)} \u2014 only the last processed will survive`);
|
|
402
521
|
}
|
|
403
522
|
}
|
|
404
523
|
if (!output && !dryRun) {
|
|
405
524
|
out.warn("No --output specified: converted files will be written next to source files.");
|
|
406
525
|
}
|
|
407
526
|
if (dryRun) {
|
|
408
|
-
const webpCount = targets.filter((f) =>
|
|
527
|
+
const webpCount = targets.filter((f) => path4.extname(f).toLowerCase() === ".webp").length;
|
|
409
528
|
const nonWebpCount = targets.length - webpCount;
|
|
410
529
|
out.warn("Dry run \u2014 no files will be converted:");
|
|
411
530
|
for (const file of targets) {
|
|
@@ -420,19 +539,18 @@ async function processImages(options) {
|
|
|
420
539
|
}
|
|
421
540
|
const results = [];
|
|
422
541
|
const writingFiles = /* @__PURE__ */ new Set();
|
|
542
|
+
const shouldRegisterSigint = options.registerSigint ?? true;
|
|
423
543
|
const sigintHandler = () => {
|
|
424
544
|
for (const f of writingFiles) {
|
|
425
545
|
try {
|
|
426
|
-
|
|
546
|
+
unlinkSync2(f);
|
|
427
547
|
} catch {
|
|
428
548
|
}
|
|
429
549
|
}
|
|
430
|
-
|
|
431
|
-
Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.
|
|
432
|
-
`);
|
|
550
|
+
out.warn(`Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.`);
|
|
433
551
|
process.exit(130);
|
|
434
552
|
};
|
|
435
|
-
process.once("SIGINT", sigintHandler);
|
|
553
|
+
if (shouldRegisterSigint) process.once("SIGINT", sigintHandler);
|
|
436
554
|
try {
|
|
437
555
|
if (isFree) {
|
|
438
556
|
for (let i = 0; i < targets.length; i++) {
|
|
@@ -445,7 +563,7 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
|
|
|
445
563
|
await sleep(FREE_TIER.DELAY_MS);
|
|
446
564
|
}
|
|
447
565
|
debugLog("converting", file);
|
|
448
|
-
const result = await convertFile(file, output ??
|
|
566
|
+
const result = await convertFile(file, output ?? path4.dirname(file), quality, out, skipExisting, writingFiles);
|
|
449
567
|
debugLog("done", file, result.status);
|
|
450
568
|
results.push(result);
|
|
451
569
|
}
|
|
@@ -453,11 +571,13 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
|
|
|
453
571
|
const MAX_CONCURRENCY = 32;
|
|
454
572
|
const rawConcurrency = Number(options.concurrency ?? DEFAULTS.CONCURRENCY);
|
|
455
573
|
const concurrency = Number.isNaN(rawConcurrency) || rawConcurrency < 1 ? DEFAULTS.CONCURRENCY : Math.min(Math.floor(rawConcurrency), MAX_CONCURRENCY);
|
|
456
|
-
const
|
|
457
|
-
const
|
|
574
|
+
const hasHeicOrAvif = targets.some(isHeicOrAvif);
|
|
575
|
+
const effectiveConcurrency = hasHeicOrAvif ? 1 : concurrency;
|
|
576
|
+
const limit = pLimit(effectiveConcurrency);
|
|
577
|
+
const uniqueDirs = new Set(targets.map((file) => output ?? path4.dirname(file)));
|
|
458
578
|
await Promise.all([...uniqueDirs].map((dir) => fs3.mkdir(dir, { recursive: true })));
|
|
459
579
|
const tasks = targets.map((file) => {
|
|
460
|
-
const outputDir = output ??
|
|
580
|
+
const outputDir = output ?? path4.dirname(file);
|
|
461
581
|
return limit(async () => {
|
|
462
582
|
debugLog("converting", file);
|
|
463
583
|
const result = await convertFile(file, outputDir, quality, out, skipExisting, writingFiles);
|
|
@@ -468,15 +588,15 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
|
|
|
468
588
|
results.push(...await Promise.all(tasks));
|
|
469
589
|
}
|
|
470
590
|
} finally {
|
|
471
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
591
|
+
if (shouldRegisterSigint) process.removeListener("SIGINT", sigintHandler);
|
|
472
592
|
}
|
|
473
593
|
out.summary(results, { skipped, totalFound: files.length, plan });
|
|
474
594
|
return results;
|
|
475
595
|
}
|
|
476
596
|
async function convertFile(filePath, outputDir, quality, out, skipExisting, writingFiles) {
|
|
477
|
-
const outName =
|
|
478
|
-
const outPath =
|
|
479
|
-
if (
|
|
597
|
+
const outName = path4.basename(filePath, path4.extname(filePath)) + ".webp";
|
|
598
|
+
const outPath = path4.join(outputDir, outName);
|
|
599
|
+
if (path4.extname(filePath).toLowerCase() === ".webp" && path4.normalize(outPath) === path4.normalize(filePath)) {
|
|
480
600
|
return { file: filePath, status: "skipped", reason: "existing" };
|
|
481
601
|
}
|
|
482
602
|
if (skipExisting) {
|
|
@@ -498,29 +618,54 @@ async function convertFile(filePath, outputDir, quality, out, skipExisting, writ
|
|
|
498
618
|
return { file: filePath, status: "error", error: `Decode failed: ${msg}` };
|
|
499
619
|
}
|
|
500
620
|
let webpBuffer;
|
|
621
|
+
let finalQuality;
|
|
622
|
+
let qualityMode;
|
|
623
|
+
let ssimScore;
|
|
501
624
|
try {
|
|
502
|
-
|
|
625
|
+
if (quality === AUTO_QUALITY_SENTINEL) {
|
|
626
|
+
const codec = {
|
|
627
|
+
encode: (img, q) => encodeWebp(img, { quality: q }),
|
|
628
|
+
decode: (buf) => decodeWebp2(buf)
|
|
629
|
+
};
|
|
630
|
+
const result = await findOptimalQuality(imageData, inputStat.size, codec);
|
|
631
|
+
finalQuality = result.quality;
|
|
632
|
+
ssimScore = result.ssim;
|
|
633
|
+
qualityMode = "auto";
|
|
634
|
+
webpBuffer = result.buffer ?? await encodeWebp(imageData, { quality: finalQuality });
|
|
635
|
+
} else {
|
|
636
|
+
finalQuality = quality;
|
|
637
|
+
qualityMode = "fixed";
|
|
638
|
+
webpBuffer = await encodeWebp(imageData, { quality });
|
|
639
|
+
}
|
|
503
640
|
} catch (err) {
|
|
504
641
|
spinner.fail();
|
|
505
642
|
const msg = err instanceof Error ? err.message : String(err);
|
|
506
643
|
return { file: filePath, status: "error", error: `Encode failed: ${msg}` };
|
|
507
644
|
}
|
|
645
|
+
const originalBuffer = (await fs3.readFile(filePath)).buffer;
|
|
646
|
+
const isWebP = path4.extname(filePath).toLowerCase() === ".webp";
|
|
647
|
+
const guard = applySizeGuard({ originalBuffer, encodedBuffer: webpBuffer, isWebP });
|
|
648
|
+
const finalBuffer = guard.buffer;
|
|
508
649
|
const tmpPath = outPath + ".tmp";
|
|
509
650
|
if (process.platform === "win32" && tmpPath.length >= 260) {
|
|
510
651
|
spinner.fail();
|
|
511
652
|
return { file: filePath, status: "error", error: `Output path too long for Windows (${tmpPath.length} chars, max 260). Use a shorter output directory or enable LongPathsEnabled in Windows registry.` };
|
|
512
653
|
}
|
|
513
654
|
writingFiles.add(tmpPath);
|
|
514
|
-
await fs3.writeFile(tmpPath, new Uint8Array(
|
|
655
|
+
await fs3.writeFile(tmpPath, new Uint8Array(finalBuffer));
|
|
515
656
|
await fs3.rename(tmpPath, outPath);
|
|
516
657
|
writingFiles.delete(tmpPath);
|
|
517
|
-
const
|
|
518
|
-
|
|
658
|
+
const newSize = finalBuffer.byteLength;
|
|
659
|
+
const savedRatio = 1 - newSize / inputStat.size;
|
|
660
|
+
spinner.succeed({ quality: finalQuality, qualityMode });
|
|
519
661
|
return {
|
|
520
662
|
file: filePath,
|
|
521
663
|
originalSize: inputStat.size,
|
|
522
|
-
newSize
|
|
664
|
+
newSize,
|
|
523
665
|
savedRatio,
|
|
666
|
+
quality: finalQuality,
|
|
667
|
+
qualityMode,
|
|
668
|
+
ssim: ssimScore,
|
|
524
669
|
status: "success"
|
|
525
670
|
};
|
|
526
671
|
} catch (err) {
|
|
@@ -541,7 +686,10 @@ var HumanOutput = class {
|
|
|
541
686
|
startFile(file) {
|
|
542
687
|
const spinner = ora(`Processing ${chalk.cyan(file)}`).start();
|
|
543
688
|
return {
|
|
544
|
-
succeed: () =>
|
|
689
|
+
succeed: (result) => {
|
|
690
|
+
const qualityInfo = result?.quality !== void 0 ? ` ${chalk.dim(`q=${result.quality}${result.qualityMode === "auto" ? " (auto)" : ""}`)}` : "";
|
|
691
|
+
spinner.succeed(chalk.green(`\u2713 ${file}`) + qualityInfo);
|
|
692
|
+
},
|
|
545
693
|
fail: () => spinner.fail(chalk.red(`\u2717 ${file}`))
|
|
546
694
|
};
|
|
547
695
|
}
|
|
@@ -665,7 +813,7 @@ var earlyFlags = {
|
|
|
665
813
|
|
|
666
814
|
// src/commands/convert.ts
|
|
667
815
|
function convertCommand(cli2) {
|
|
668
|
-
cli2.command("[path]", "Convert images
|
|
816
|
+
cli2.command("[path]", "Convert images to WebP format. Original files are never modified or deleted.").option("-i, --input <path>", "Input file or directory path (e.g. ./images)").option("-o, --output <path>", "Output directory for converted files (default: write .webp next to source files)").option("-q, --quality <number>", "WebP quality 1\u2013100 (default: 80)", { default: void 0 }).option("--json", "Output results as JSON \u2014 useful for CI pipelines and AI agents").option("--concurrency <number>", "Number of parallel workers (default: auto, paid plans only)", { default: void 0 }).option("-r, --recursive", "Recursively process images in subdirectories").option("--dry-run", "Preview which files would be converted without actually converting them").option("--skip-existing", "Skip conversion if a .webp file already exists at the output path").option("--no-auto-quality", "Disable auto quality, use fixed quality=80").action((argPath, options) => {
|
|
669
817
|
runAction(async () => {
|
|
670
818
|
const targetPath = argPath || options.input;
|
|
671
819
|
if (!targetPath) {
|
|
@@ -680,8 +828,20 @@ function convertCommand(cli2) {
|
|
|
680
828
|
}
|
|
681
829
|
const out = createOutput(options.json ?? false);
|
|
682
830
|
const licenseStatus = await checkLicense();
|
|
683
|
-
|
|
684
|
-
|
|
831
|
+
if (!licenseStatus.valid && licenseStatus.expired) {
|
|
832
|
+
out.warn("Your license has expired. Please renew to continue using Pro features.");
|
|
833
|
+
} else if (!licenseStatus.valid && !licenseStatus.expired) {
|
|
834
|
+
out.warn("Free mode: conversion is limited. Activate a license for full access.");
|
|
835
|
+
}
|
|
836
|
+
let quality;
|
|
837
|
+
if (options.quality !== void 0) {
|
|
838
|
+
const rawQuality = Number(options.quality);
|
|
839
|
+
quality = Number.isNaN(rawQuality) ? 80 : Math.max(1, Math.min(100, rawQuality));
|
|
840
|
+
} else if (options.autoQuality === false) {
|
|
841
|
+
quality = 80;
|
|
842
|
+
} else {
|
|
843
|
+
quality = AUTO_QUALITY_SENTINEL;
|
|
844
|
+
}
|
|
685
845
|
const results = await processImages({
|
|
686
846
|
input: targetPath,
|
|
687
847
|
output: options.output,
|
|
@@ -734,7 +894,7 @@ function authCommand(cli2) {
|
|
|
734
894
|
}
|
|
735
895
|
|
|
736
896
|
// src/version.ts
|
|
737
|
-
var VERSION = true ? "1.
|
|
897
|
+
var VERSION = true ? "1.1.0" : await getDevVersion();
|
|
738
898
|
|
|
739
899
|
// src/commands/status.ts
|
|
740
900
|
function statusCommand(cli2) {
|
|
@@ -853,7 +1013,7 @@ function logoutCommand(cli2) {
|
|
|
853
1013
|
);
|
|
854
1014
|
await safeExit(1);
|
|
855
1015
|
} else if (result.error === "not_activated") {
|
|
856
|
-
process.stderr.write("No active license found on this device.\n");
|
|
1016
|
+
process.stderr.write("No active Pro license found on this device.\n");
|
|
857
1017
|
await safeExit(1);
|
|
858
1018
|
} else {
|
|
859
1019
|
process.stderr.write(`\u2717 Logout failed: ${result.error}
|
|
@@ -878,7 +1038,7 @@ function errorMessage(error) {
|
|
|
878
1038
|
case "network_unreachable":
|
|
879
1039
|
return "Cannot reach server. Retry later or unbind via dashboard.";
|
|
880
1040
|
case "not_activated":
|
|
881
|
-
return "No active license on this device.";
|
|
1041
|
+
return "No active Pro license on this device.";
|
|
882
1042
|
case "device_not_found":
|
|
883
1043
|
return "Device not found. It may have already been unbound.";
|
|
884
1044
|
case "invalid_token":
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getwebp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Convert images to WebP
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Convert images to WebP from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"getwebp": "dist/index.js"
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "bun run src/index.ts",
|
|
17
|
-
"build": "CLI_VERSION=$(node -p \"require('./package.json').version\") bun build src/index.ts --compile --minify --define \"CLI_VERSION='$CLI_VERSION'\" --outfile release/getwebp",
|
|
17
|
+
"build": "export CLI_VERSION=$(node -p \"require('./package.json').version\") && bun build src/index.ts --compile --minify --define \"CLI_VERSION='$CLI_VERSION'\" --outfile release/getwebp",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"test": "bun test --timeout 60000 tests/isolated/ && bun test --timeout 60000 tests/",
|
|
20
|
+
"test:e2e": "bun test --timeout 60000 tests/e2e/",
|
|
21
|
+
"test:e2e:binary": "USE_BINARY=1 bun test --timeout 60000 tests/e2e/",
|
|
20
22
|
"test:watch": "bun test --timeout 60000 --watch"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
25
|
+
"@getwebp/core": "*",
|
|
23
26
|
"@jsquash/jpeg": "^1.5.0",
|
|
24
27
|
"@jsquash/png": "^3.1.0",
|
|
25
28
|
"@jsquash/webp": "^1.4.0",
|
|
@@ -30,7 +33,9 @@
|
|
|
30
33
|
"jsonwebtoken": "^9.0.2",
|
|
31
34
|
"node-machine-id": "^1.1.12",
|
|
32
35
|
"ora": "^8.1.0",
|
|
33
|
-
"p-limit": "^6.2.0"
|
|
36
|
+
"p-limit": "^6.2.0",
|
|
37
|
+
"@jsquash/avif": "^2.1.1",
|
|
38
|
+
"heic-decode": "^2.1.0"
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
41
|
"@types/jsonwebtoken": "^9.0.7",
|