xw-devtool-cli 1.0.41 → 1.0.43

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  一个基于 Node.js 的开发者命令行工具箱,旨在提供开箱即用的常用开发工具,帮助开发者快速处理日常任务。
6
6
 
7
- 主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
7
+ 主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、二维码识别、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
8
8
 
9
9
  ## ✨ 功能特性
10
10
 
@@ -13,6 +13,7 @@
13
13
  - **图片格式转换**:支持 PNG、JPG、WebP、ICO 格式互转,可调整压缩质量。生成 ICO 时支持自定义尺寸(逗号分隔),留空则默认生成 `256` 尺寸。输出文件名默认带时间戳,避免覆盖。
14
14
  - **图片 ↔ Base64**:支持图片转 Base64 字符串,以及 Base64 还原为图片文件。
15
15
  - **图片分割**:支持网格等分(自定义行/列数)或自定义分割线(像素/百分比),自动生成分割后的图片文件。
16
+ - **图片拼接**:支持“选择目录”或“选择多张图片”两种输入方式;在对话框中可一次多选多张图片,可选**水平拼接**或**垂直拼接**,输出支持选择指定目录(对话框或手动输入),默认输出文件名带时间戳并自动复制输出路径。
16
17
  - **图片主色识别**:提取图片主色调(Hex/RGB/HSL/HSV),默认复制 Hex 到剪贴板,支持保存详细信息到文件。
17
18
  - **颜色吸取**:从图片中吸取单像素颜色或区域平均颜色,支持 px/% 坐标输入,结果显示颜色条并自动复制 Hex/Hex8。
18
19
  - **屏幕取色**:实时显示鼠标所在位置的颜色值(Hex/RGB)。
@@ -45,6 +46,7 @@
45
46
  - **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
46
47
  - **哈希计算**:支持 MD5, SHA1, SHA256, SHA512, SM3 算法。
47
48
  - **二维码生成**:终端直接显示二维码,支持保存为 PNG 图片(带时间戳文件名)。
49
+ - **二维码识别**:支持选择二维码图片进行识别,识别结果自动复制到剪贴板。
48
50
  - **特殊符号大全**:包含常用符号、箭头、数学符号、货币、希腊字母等 170+ 个符号,支持一键复制。
49
51
  - **Emoji 输入**:支持分类查看和选择常用 Emoji,一键复制到剪贴板。
50
52
  - **HTML 实体工具**:支持 HTML 实体编码与解码 (如 `&` <-> `&amp;`)。
@@ -207,6 +209,13 @@ x. 设置 / 语言 (Settings)
207
209
  - 输入文本或 URL(支持直接回车读取剪贴板)。
208
210
  - 终端直接显示二维码预览。
209
211
  - 可选保存为 PNG 图片,默认文件名包含时间戳。
212
+
213
+ ### 二维码识别
214
+ - 选择 `qrcodeDecode`(菜单对应数字/字母)进入。
215
+ - 支持两种图片输入方式:
216
+ - **选择文件(对话框)**
217
+ - **手动输入文件路径**
218
+ - 自动识别二维码内容并复制到剪贴板。
210
219
 
211
220
  ### 5. URL 编解码
212
221
  - 选择 `5` 进入。
@@ -345,7 +354,7 @@ x. 设置 / 语言 (Settings)
345
354
 
346
355
  A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
347
356
 
348
- Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, etc. All results are automatically copied to the clipboard, greatly improving development efficiency.
357
+ Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
349
358
 
350
359
  ### ✨ Features
351
360
 
@@ -380,6 +389,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
380
389
  - **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
381
390
  - **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
382
391
  - **QR Code**: Display QR codes in terminal, save as PNG.
392
+ - **QR Code Reader**: Select an image with QR code and decode content, then copy the result to clipboard automatically.
383
393
  - **Special Symbols**: 170+ symbols, arrows, math symbols, etc.
384
394
  - **Emoji Picker**: Browse and copy emojis.
385
395
  - **HTML Entity**: Encode/Decode HTML entities.
@@ -493,6 +503,13 @@ You can enter a feature key directly, or type a keyword (e.g. `json`, `image`, `
493
503
  - Input text or URL.
494
504
  - Preview in terminal.
495
505
  - Option to save as PNG.
506
+
507
+ #### QR Code Reader
508
+ - Select `qrcodeDecode` (menu key).
509
+ - Image input methods:
510
+ - **Select file (dialog)**
511
+ - **Enter file path manually**
512
+ - Decode content and copy it to clipboard automatically.
496
513
 
497
514
  #### 5. URL Encode/Decode
498
515
  - Select `5`.
package/README_EN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
6
6
 
7
- Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
7
+ Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
8
8
 
9
9
  ## ✨ Features
10
10
 
@@ -13,6 +13,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
13
13
  - **Format Conversion**: Convert between PNG, JPG, WebP, with adjustable compression quality.
14
14
  - **Image ↔ Base64**: Convert images to Base64 strings and vice versa.
15
15
  - **Image Splitter**: Split images into grid (rows/cols) or custom split lines (pixels/percentage).
16
+ - **Image Merger**: Supports two input modes (folder or multiple images); the dialog mode supports selecting multiple images at once. You can choose **horizontal** or **vertical** merge, and choose a specific output directory (dialog or manual input). Default output filename includes timestamp, and output path is copied to clipboard.
16
17
  - **Placeholder Image**: Quickly generate placeholder images with custom size, color, and text.
17
18
  - **Color Picker**: Pick a single pixel color or area average from an image; supports px/% coordinates; shows preview bar and auto-copies Hex/Hex8.
18
19
  - **Screen Picker**: Pick color directly from the screen at the mouse position; move the cursor and press Enter to sample; supports alpha preview.
@@ -39,6 +40,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
39
40
  - **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
40
41
  - **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
41
42
  - **QR Code**: Display QR codes in terminal, save as PNG.
43
+ - **QR Code Reader**: Select a QR code image and recognize the content, then copy the result to clipboard.
42
44
  - **Special Symbols**: 170+ symbols, arrows, math symbols, etc.
43
45
  - **Emoji Picker**: Browse and copy emojis.
44
46
  - **HTML Entity**: Encode/Decode HTML entities.
@@ -159,6 +161,13 @@ s. Settings (Language)
159
161
  - Preview in terminal.
160
162
  - Option to save as PNG.
161
163
 
164
+ ### QR Code Reader
165
+ - Select `qrcodeDecode` (menu key).
166
+ - Supports two input methods:
167
+ - **Select file (dialog)**
168
+ - **Enter file path manually**
169
+ - Recognized content is copied to clipboard automatically.
170
+
162
171
  ### 5. URL Encode/Decode
163
172
  - Select `5`.
164
173
  - Select `Encode` or `Decode`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -42,6 +42,8 @@
42
42
  "png",
43
43
  "placeholder-image",
44
44
  "qrcode",
45
+ "qr-code-reader",
46
+ "qr-decoder",
45
47
  "json-format",
46
48
  "json-formatter",
47
49
  "variable-case",
@@ -54,6 +56,9 @@
54
56
  "screen-mark",
55
57
  "pixel-distance",
56
58
  "image-compress",
59
+ "image-merge",
60
+ "merge-images",
61
+ "image-stitch",
57
62
  "png-compress",
58
63
  "jpg-compress",
59
64
  "webp-compress",
@@ -84,6 +89,7 @@
84
89
  "he": "^1.2.0",
85
90
  "i18next": "^25.7.3",
86
91
  "inquirer": "^13.1.0",
92
+ "jsqr": "^1.4.0",
87
93
  "lorem-ipsum": "^2.0.8",
88
94
  "pinyin": "^4.0.0",
89
95
  "qrcode": "^1.5.4",
@@ -0,0 +1,285 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import inquirer from 'inquirer';
4
+ import sharp from 'sharp';
5
+ import i18next from '../i18n.js';
6
+ import { selectFromMenu } from '../utils/menu.js';
7
+ import { selectFiles, selectFolder } from '../utils/fileDialog.js';
8
+ import { copy } from '../utils/clipboard.js';
9
+ import { defaultFileName } from '../utils/output.js';
10
+
11
+ const SUPPORTED_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tif', '.tiff', '.gif']);
12
+
13
+ function isImageFile(filePath) {
14
+ const ext = path.extname(filePath).toLowerCase();
15
+ return SUPPORTED_EXT.has(ext);
16
+ }
17
+
18
+ function listImagesInDir(dir) {
19
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
20
+ return entries
21
+ .filter((entry) => entry.isFile() && isImageFile(entry.name))
22
+ .map((entry) => path.join(dir, entry.name))
23
+ .sort((a, b) => path.basename(a).localeCompare(path.basename(b), undefined, { numeric: true, sensitivity: 'base' }));
24
+ }
25
+
26
+ function parseManualImagePaths(input) {
27
+ return Array.from(new Set(
28
+ input
29
+ .split(/[\r\n,]+/)
30
+ .map((p) => p.trim())
31
+ .filter(Boolean)
32
+ ));
33
+ }
34
+
35
+ async function pickFolderPath() {
36
+ const method = await selectFromMenu(
37
+ i18next.t('imgMerge.inputMethod'),
38
+ [
39
+ { name: i18next.t('imgMerge.folderDialog'), value: 'dialog' },
40
+ { name: i18next.t('imgMerge.folderManual'), value: 'manual' }
41
+ ],
42
+ true,
43
+ i18next.t('common.back')
44
+ );
45
+ if (method === '__BACK__') return null;
46
+
47
+ if (method === 'dialog') {
48
+ const selected = selectFolder();
49
+ if (!selected) {
50
+ console.log(i18next.t('imgMerge.dialogFail'));
51
+ return await pickFolderPath();
52
+ }
53
+ return selected;
54
+ }
55
+
56
+ const { folderPath } = await inquirer.prompt([
57
+ {
58
+ type: 'input',
59
+ name: 'folderPath',
60
+ message: i18next.t('imgMerge.enterFolderPath'),
61
+ validate: (input) => {
62
+ if (!fs.existsSync(input)) return i18next.t('imgMerge.notExist');
63
+ if (!fs.statSync(input).isDirectory()) return i18next.t('imgMerge.notDirectory');
64
+ return true;
65
+ }
66
+ }
67
+ ]);
68
+ return folderPath;
69
+ }
70
+
71
+ async function pickMultiImageFiles() {
72
+ const method = await selectFromMenu(
73
+ i18next.t('imgMerge.inputMethod'),
74
+ [
75
+ { name: i18next.t('imgMerge.multiDialog'), value: 'dialog' },
76
+ { name: i18next.t('imgMerge.multiManual'), value: 'manual' }
77
+ ],
78
+ true,
79
+ i18next.t('common.back')
80
+ );
81
+ if (method === '__BACK__') return null;
82
+
83
+ if (method === 'dialog') {
84
+ const files = selectFiles('Image Files|*.png;*.jpg;*.jpeg;*.webp;*.bmp;*.tif;*.tiff;*.gif|All Files|*.*');
85
+ if (!files || files.length === 0) {
86
+ console.log(i18next.t('imgMerge.dialogFail'));
87
+ return await pickMultiImageFiles();
88
+ }
89
+ return Array.from(new Set(files));
90
+ }
91
+
92
+ const { filesInput } = await inquirer.prompt([
93
+ {
94
+ type: 'input',
95
+ name: 'filesInput',
96
+ message: i18next.t('imgMerge.enterImagePaths')
97
+ }
98
+ ]);
99
+ return parseManualImagePaths(filesInput);
100
+ }
101
+
102
+ async function pickOutputPath(baseDir) {
103
+ const outputMethod = await selectFromMenu(
104
+ i18next.t('imgMerge.selectOutput'),
105
+ [
106
+ { name: i18next.t('imgMerge.outputDefault'), value: 'default' },
107
+ { name: i18next.t('imgMerge.outputFolderDialog'), value: 'folderDialog' },
108
+ { name: i18next.t('imgMerge.outputFolderManual'), value: 'folderManual' },
109
+ { name: i18next.t('imgMerge.outputFileManual'), value: 'fileManual' }
110
+ ],
111
+ true,
112
+ i18next.t('common.back')
113
+ );
114
+ if (outputMethod === '__BACK__') return null;
115
+
116
+ if (outputMethod === 'default') {
117
+ return path.join(baseDir, defaultFileName('img-merge', 'png'));
118
+ }
119
+
120
+ if (outputMethod === 'folderDialog') {
121
+ const selected = selectFolder();
122
+ if (!selected) {
123
+ console.log(i18next.t('imgMerge.dialogFail'));
124
+ return await pickOutputPath(baseDir);
125
+ }
126
+ return path.join(selected, defaultFileName('img-merge', 'png'));
127
+ }
128
+
129
+ if (outputMethod === 'folderManual') {
130
+ const { outputDir } = await inquirer.prompt([
131
+ {
132
+ type: 'input',
133
+ name: 'outputDir',
134
+ message: i18next.t('imgMerge.enterOutputDir'),
135
+ default: baseDir,
136
+ validate: (input) => {
137
+ const dir = input.trim();
138
+ if (!dir) return i18next.t('imgMerge.outputPathRequired');
139
+ if (fs.existsSync(dir) && !fs.statSync(dir).isDirectory()) {
140
+ return i18next.t('imgMerge.notDirectory');
141
+ }
142
+ return true;
143
+ }
144
+ }
145
+ ]);
146
+ return path.join(outputDir.trim(), defaultFileName('img-merge', 'png'));
147
+ }
148
+
149
+ const defaultPath = path.join(baseDir, defaultFileName('img-merge', 'png'));
150
+ const { outputPath } = await inquirer.prompt([
151
+ {
152
+ type: 'input',
153
+ name: 'outputPath',
154
+ message: i18next.t('imgMerge.outputPathPrompt'),
155
+ default: defaultPath,
156
+ validate: (input) => input.trim() ? true : i18next.t('imgMerge.outputPathRequired')
157
+ }
158
+ ]);
159
+ return outputPath.trim();
160
+ }
161
+
162
+ function validateImageFiles(files) {
163
+ if (!files || files.length < 2) {
164
+ return i18next.t('imgMerge.needAtLeastTwo');
165
+ }
166
+ for (const filePath of files) {
167
+ if (!fs.existsSync(filePath)) {
168
+ return `${i18next.t('imgMerge.notExist')}: ${filePath}`;
169
+ }
170
+ if (!fs.statSync(filePath).isFile()) {
171
+ return `${i18next.t('imgMerge.notFile')}: ${filePath}`;
172
+ }
173
+ if (!isImageFile(filePath)) {
174
+ return `${i18next.t('imgMerge.notImage')}: ${filePath}`;
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+
180
+ async function mergeImages(files, direction, outputPath) {
181
+ const metas = await Promise.all(files.map(async (filePath) => {
182
+ const image = sharp(filePath).rotate();
183
+ const metadata = await image.metadata();
184
+ const buffer = await image.toBuffer();
185
+ return { filePath, metadata, buffer };
186
+ }));
187
+
188
+ const widths = metas.map((m) => m.metadata.width || 0);
189
+ const heights = metas.map((m) => m.metadata.height || 0);
190
+ if (widths.some((w) => w <= 0) || heights.some((h) => h <= 0)) {
191
+ throw new Error(i18next.t('imgMerge.invalidImageSize'));
192
+ }
193
+
194
+ const isHorizontal = direction === 'horizontal';
195
+ const canvasWidth = isHorizontal ? widths.reduce((sum, w) => sum + w, 0) : Math.max(...widths);
196
+ const canvasHeight = isHorizontal ? Math.max(...heights) : heights.reduce((sum, h) => sum + h, 0);
197
+
198
+ let offsetX = 0;
199
+ let offsetY = 0;
200
+ const composite = metas.map((meta, index) => {
201
+ const item = {
202
+ input: meta.buffer,
203
+ left: offsetX,
204
+ top: offsetY
205
+ };
206
+ if (isHorizontal) {
207
+ offsetX += widths[index];
208
+ } else {
209
+ offsetY += heights[index];
210
+ }
211
+ return item;
212
+ });
213
+
214
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
215
+
216
+ await sharp({
217
+ create: {
218
+ width: canvasWidth,
219
+ height: canvasHeight,
220
+ channels: 4,
221
+ background: { r: 255, g: 255, b: 255, alpha: 1 }
222
+ }
223
+ })
224
+ .composite(composite)
225
+ .png()
226
+ .toFile(outputPath);
227
+ }
228
+
229
+ export async function imgMergeHandler() {
230
+ const sourceType = await selectFromMenu(
231
+ i18next.t('imgMerge.selectSource'),
232
+ [
233
+ { name: i18next.t('imgMerge.sourceFolder'), value: 'folder' },
234
+ { name: i18next.t('imgMerge.sourceMulti'), value: 'multi' }
235
+ ],
236
+ true,
237
+ i18next.t('common.back')
238
+ );
239
+ if (sourceType === '__BACK__') return;
240
+
241
+ let files = [];
242
+ let baseDir = process.cwd();
243
+ if (sourceType === 'folder') {
244
+ const folderPath = await pickFolderPath();
245
+ if (!folderPath) return;
246
+ baseDir = folderPath;
247
+ files = listImagesInDir(folderPath);
248
+ } else {
249
+ const selectedFiles = await pickMultiImageFiles();
250
+ if (!selectedFiles) return;
251
+ files = selectedFiles;
252
+ if (files.length > 0) {
253
+ baseDir = path.dirname(files[0]);
254
+ }
255
+ }
256
+
257
+ const validationResult = validateImageFiles(files);
258
+ if (validationResult !== true) {
259
+ console.log(validationResult);
260
+ return;
261
+ }
262
+
263
+ const direction = await selectFromMenu(
264
+ i18next.t('imgMerge.selectDirection'),
265
+ [
266
+ { name: i18next.t('imgMerge.horizontal'), value: 'horizontal' },
267
+ { name: i18next.t('imgMerge.vertical'), value: 'vertical' }
268
+ ],
269
+ true,
270
+ i18next.t('common.back')
271
+ );
272
+ if (direction === '__BACK__') return;
273
+
274
+ const outputPath = await pickOutputPath(baseDir);
275
+ if (!outputPath) return;
276
+
277
+ try {
278
+ await mergeImages(files, direction, outputPath);
279
+ console.log(i18next.t('imgMerge.success', { outputPath }));
280
+ await copy(outputPath);
281
+ console.log(i18next.t('imgMerge.copied', { outputPath }));
282
+ } catch (error) {
283
+ console.error(i18next.t('imgMerge.error', { message: error.message }));
284
+ }
285
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'fs';
2
+ import inquirer from 'inquirer';
3
+ import jsQR from 'jsqr';
4
+ import sharp from 'sharp';
5
+ import i18next from '../i18n.js';
6
+ import { selectFile } from '../utils/fileDialog.js';
7
+ import { selectFromMenu } from '../utils/menu.js';
8
+ import { outputText } from '../utils/output.js';
9
+
10
+ async function getImagePath() {
11
+ const method = await selectFromMenu(
12
+ i18next.t('qrcodeDecode.selectInputMethod'),
13
+ [
14
+ { name: i18next.t('qrcodeDecode.inputDialog'), value: 'dialog' },
15
+ { name: i18next.t('qrcodeDecode.inputManual'), value: 'manual' }
16
+ ],
17
+ true,
18
+ i18next.t('common.back')
19
+ );
20
+ if (method === '__BACK__') {
21
+ return null;
22
+ }
23
+
24
+ let imagePath = '';
25
+ if (method === 'dialog') {
26
+ imagePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.webp|All Files|*.*') || '';
27
+ if (!imagePath) {
28
+ console.log(i18next.t('qrcodeDecode.dialogUnavailable'));
29
+ }
30
+ }
31
+
32
+ if (!imagePath) {
33
+ const answer = await inquirer.prompt([
34
+ {
35
+ type: 'input',
36
+ name: 'path',
37
+ message: i18next.t('qrcodeDecode.enterPath'),
38
+ validate: (value) => (fs.existsSync(value) ? true : i18next.t('qrcodeDecode.notExist'))
39
+ }
40
+ ]);
41
+ imagePath = answer.path;
42
+ }
43
+
44
+ return imagePath;
45
+ }
46
+
47
+ async function decodeQRCodeFromImage(imagePath) {
48
+ const { data, info } = await sharp(imagePath)
49
+ .rotate()
50
+ .ensureAlpha()
51
+ .raw()
52
+ .toBuffer({ resolveWithObject: true });
53
+ const pixelData = new Uint8ClampedArray(data);
54
+ const result = jsQR(pixelData, info.width, info.height, { inversionAttempts: 'attemptBoth' });
55
+ return result ? result.data : '';
56
+ }
57
+
58
+ export async function qrcodeDecodeHandler() {
59
+ const imagePath = await getImagePath();
60
+ if (!imagePath) {
61
+ return;
62
+ }
63
+
64
+ try {
65
+ const decoded = await decodeQRCodeFromImage(imagePath);
66
+ if (!decoded) {
67
+ console.log(i18next.t('qrcodeDecode.notFound'));
68
+ return;
69
+ }
70
+ await outputText(decoded, { showPreview: false, copyToClipboard: true });
71
+ console.log(i18next.t('qrcodeDecode.success'));
72
+ console.log(`${i18next.t('qrcodeDecode.result')}\n${decoded}\n`);
73
+ } catch (error) {
74
+ console.error(i18next.t('qrcodeDecode.decodeFail', { message: error.message }));
75
+ }
76
+ }
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ import { unicodeHandler } from './commands/unicode.js';
11
11
  import { imgBase64Handler } from './commands/imgBase64.js';
12
12
  import { imgConvertHandler } from './commands/imgConvert.js';
13
13
  import { imgSplitHandler } from './commands/imgSplit.js';
14
+ import { imgMergeHandler } from './commands/imgMerge.js';
14
15
  import { timeFormatHandler } from './commands/timeFormat.js';
15
16
  import { timeCalcHandler } from './commands/timeCalc.js';
16
17
  import { mockHandler } from './commands/mock.js';
@@ -22,6 +23,7 @@ import { variableFormatHandler } from './commands/variableFormat.js';
22
23
  import { jsonFormatHandler } from './commands/jsonFormat.js';
23
24
  import { hashingHandler } from './commands/hashing.js';
24
25
  import { qrcodeHandler } from './commands/qrcode.js';
26
+ import { qrcodeDecodeHandler } from './commands/qrcodeDecode.js';
25
27
  import { specialCharsHandler } from './commands/specialChars.js';
26
28
  import { emojiHandler } from './commands/emoji.js';
27
29
  import { htmlEntitiesHandler } from './commands/htmlEntities.js';
@@ -58,6 +60,7 @@ function getFeatures() {
58
60
  { name: i18next.t('menu.features.imgBase64'), value: 'imgBase64' },
59
61
  { name: i18next.t('menu.features.imgConvert'), value: 'imgConvert' },
60
62
  { name: i18next.t('menu.features.imgSplit'), value: 'imgSplit' },
63
+ { name: i18next.t('menu.features.imgMerge'), value: 'imgMerge' },
61
64
  { name: i18next.t('menu.features.dominantColor'), value: 'dominantColor' },
62
65
  { name: i18next.t('menu.features.colorPick'), value: 'colorPick' },
63
66
  { name: i18next.t('menu.features.crosshair'), value: 'crosshair' },
@@ -66,6 +69,7 @@ function getFeatures() {
66
69
  { name: i18next.t('menu.features.imgCompress'), value: 'imgCompress' },
67
70
  { name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
68
71
  { name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
72
+ { name: i18next.t('menu.features.qrcodeDecode'), value: 'qrcodeDecode' },
69
73
 
70
74
  // Encode/Decode & Formatting
71
75
  { name: i18next.t('menu.features.url'), value: 'url' },
@@ -282,6 +286,9 @@ async function handleAction(action) {
282
286
  case 'imgSplit':
283
287
  await imgSplitHandler();
284
288
  break;
289
+ case 'imgMerge':
290
+ await imgMergeHandler();
291
+ break;
285
292
  case 'dominantColor':
286
293
  await dominantColorHandler();
287
294
  break;
@@ -331,6 +338,9 @@ async function handleAction(action) {
331
338
  case 'qrcode':
332
339
  await qrcodeHandler();
333
340
  break;
341
+ case 'qrcodeDecode':
342
+ await qrcodeDecodeHandler();
343
+ break;
334
344
  case 'specialChars':
335
345
  await specialCharsHandler();
336
346
  break;
package/src/locales/en.js CHANGED
@@ -41,6 +41,7 @@ export default {
41
41
  imgBase64: 'Image <-> Base64',
42
42
  imgConvert: 'Image Format Convert',
43
43
  imgSplit: 'Image Splitter',
44
+ imgMerge: 'Image Merger',
44
45
  dominantColor: 'Image Dominant Color',
45
46
  colorPick: 'Color Picker',
46
47
  crosshair: 'Screen Crosshair',
@@ -49,6 +50,7 @@ export default {
49
50
  imgCompress: 'Image Compression',
50
51
  placeholderImg: 'Placeholder Image Generator',
51
52
  qrcode: 'QR Code Generator',
53
+ qrcodeDecode: 'QR Code Reader',
52
54
  url: 'URL Encode/Decode',
53
55
  baseConvert: 'Number Base Converter',
54
56
  base64: 'String Encode/Decode (Base64)',
@@ -109,6 +111,18 @@ export default {
109
111
  y2Prompt: 'Enter Y2 (px or %):',
110
112
  result: 'Picked Color'
111
113
  },
114
+ qrcodeDecode: {
115
+ selectInputMethod: 'Select QR image input method',
116
+ inputDialog: 'Select file (dialog)',
117
+ inputManual: 'Enter file path manually',
118
+ dialogUnavailable: 'File dialog is unavailable or canceled. Please enter the file path manually.',
119
+ enterPath: 'Enter QR code image path:',
120
+ notExist: 'File does not exist.',
121
+ notFound: 'No QR code detected. Please make sure the image is clear and contains a QR code.',
122
+ success: 'Recognition completed. Content copied to clipboard.',
123
+ result: 'Recognized content:',
124
+ decodeFail: 'Failed to decode QR code: {{message}}'
125
+ },
112
126
  url: {
113
127
  title: 'URL Encode/Decode',
114
128
  encode: 'Encode',
@@ -156,6 +170,42 @@ export default {
156
170
  vLines: 'Vertical split lines (e.g. 10 20 30 or 10% 20%):',
157
171
  success: 'Successfully split image into {{count}} parts. Saved to {{path}}'
158
172
  },
173
+ imgMerge: {
174
+ selectSource: 'Select image source',
175
+ sourceFolder: 'Use images from a folder',
176
+ sourceMulti: 'Select multiple images',
177
+ inputMethod: 'Select input method',
178
+ folderDialog: 'Select folder (dialog)',
179
+ folderManual: 'Enter folder path manually',
180
+ multiDialog: 'Select multiple images (dialog)',
181
+ multiManual: 'Enter multiple image paths manually',
182
+ continueAdd: 'Continue adding images?',
183
+ yes: 'Yes',
184
+ no: 'No',
185
+ enterFolderPath: 'Enter image folder path:',
186
+ enterImagePaths: 'Enter image paths (comma or newline separated):',
187
+ notExist: 'Path does not exist',
188
+ notDirectory: 'The path is not a folder',
189
+ notFile: 'The path is not a file',
190
+ notImage: 'The file is not a supported image format',
191
+ needAtLeastTwo: 'At least 2 images are required for merging.',
192
+ dialogFail: 'Dialog is unavailable or canceled. Please retry or use manual input.',
193
+ selectDirection: 'Select merge direction',
194
+ horizontal: 'Horizontal merge (left to right)',
195
+ vertical: 'Vertical merge (top to bottom)',
196
+ selectOutput: 'Select output mode',
197
+ outputDefault: 'Default output to input directory',
198
+ outputFolderDialog: 'Select output directory (dialog)',
199
+ outputFolderManual: 'Enter output directory manually',
200
+ outputFileManual: 'Enter full output file path manually',
201
+ enterOutputDir: 'Enter output directory path:',
202
+ outputPathPrompt: 'Enter output file path:',
203
+ outputPathRequired: 'Output file path cannot be empty.',
204
+ invalidImageSize: 'One or more images have invalid dimensions.',
205
+ success: 'Merge completed. Output saved to: {{outputPath}}',
206
+ copied: 'Output path copied to clipboard: {{outputPath}}',
207
+ error: 'Merge failed: {{message}}'
208
+ },
159
209
  imgCompress: {
160
210
  selectSource: 'Select input method',
161
211
  sourceFileDialog: 'Select file (dialog)',
package/src/locales/zh.js CHANGED
@@ -41,6 +41,7 @@ export default {
41
41
  imgBase64: '图片 <-> Base64',
42
42
  imgConvert: '图片格式转换',
43
43
  imgSplit: '图片分割工具',
44
+ imgMerge: '图片拼接工具',
44
45
  dominantColor: '图片主色识别',
45
46
  colorPick: '颜色吸取',
46
47
  crosshair: '全屏十字辅助线',
@@ -49,6 +50,7 @@ export default {
49
50
  imgCompress: '图片压缩',
50
51
  placeholderImg: '占位图生成器',
51
52
  qrcode: '二维码生成器',
53
+ qrcodeDecode: '二维码识别',
52
54
  url: 'URL 编码/解码',
53
55
  baseConvert: '进制转换工具',
54
56
  base64: '字符串 编码/解码 (Base64)',
@@ -109,6 +111,18 @@ export default {
109
111
  y2Prompt: '输入 Y2(px 或 %):',
110
112
  result: '吸取结果'
111
113
  },
114
+ qrcodeDecode: {
115
+ selectInputMethod: '选择二维码图片输入方式',
116
+ inputDialog: '选择文件(对话框)',
117
+ inputManual: '手动输入文件路径',
118
+ dialogUnavailable: '文件对话框不可用或已取消,请手动输入文件路径。',
119
+ enterPath: '请输入二维码图片路径:',
120
+ notExist: '文件不存在。',
121
+ notFound: '未识别到二维码,请确认图片清晰且包含二维码。',
122
+ success: '识别完成,内容已复制到剪贴板。',
123
+ result: '识别结果:',
124
+ decodeFail: '二维码识别失败: {{message}}'
125
+ },
112
126
  url: {
113
127
  title: 'URL 编码/解码',
114
128
  encode: '编码',
@@ -156,6 +170,42 @@ export default {
156
170
  vLines: '垂直分割线 (例如 10 20 30 或 10% 20%):',
157
171
  success: '成功将图片分割为 {{count}} 份。保存至 {{path}}'
158
172
  },
173
+ imgMerge: {
174
+ selectSource: '选择图片来源',
175
+ sourceFolder: '选择目录中的图片',
176
+ sourceMulti: '选择多张图片',
177
+ inputMethod: '选择输入方式',
178
+ folderDialog: '选择文件夹(对话框)',
179
+ folderManual: '手动输入文件夹路径',
180
+ multiDialog: '多选图片(对话框)',
181
+ multiManual: '手动输入多张图片路径',
182
+ continueAdd: '是否继续添加图片',
183
+ yes: '是',
184
+ no: '否',
185
+ enterFolderPath: '请输入图片目录路径:',
186
+ enterImagePaths: '请输入图片路径(可用逗号或换行分隔):',
187
+ notExist: '路径不存在',
188
+ notDirectory: '输入路径不是文件夹',
189
+ notFile: '输入路径不是文件',
190
+ notImage: '文件不是支持的图片格式',
191
+ needAtLeastTwo: '至少需要 2 张图片才能拼接。',
192
+ dialogFail: '对话框不可用或已取消,请重试或改用手动输入。',
193
+ selectDirection: '选择拼接方向',
194
+ horizontal: '水平拼接(从左到右)',
195
+ vertical: '垂直拼接(从上到下)',
196
+ selectOutput: '选择输出方式',
197
+ outputDefault: '默认输出到输入目录',
198
+ outputFolderDialog: '选择输出目录(对话框)',
199
+ outputFolderManual: '手动输入输出目录',
200
+ outputFileManual: '手动输入完整输出文件路径',
201
+ enterOutputDir: '请输入输出目录路径:',
202
+ outputPathPrompt: '请输入输出文件路径:',
203
+ outputPathRequired: '输出文件路径不能为空。',
204
+ invalidImageSize: '存在无法读取尺寸的图片文件。',
205
+ success: '拼接完成,已输出到:{{outputPath}}',
206
+ copied: '输出路径已复制到剪贴板:{{outputPath}}',
207
+ error: '拼接失败:{{message}}'
208
+ },
159
209
  imgCompress: {
160
210
  selectSource: '选择输入方式',
161
211
  sourceFileDialog: '选择文件(对话框)',
@@ -16,6 +16,21 @@ export function selectFile(filter = 'All Files|*.*') {
16
16
  }
17
17
  }
18
18
 
19
+ export function selectFiles(filter = 'All Files|*.*') {
20
+ if (!isWindows()) return null;
21
+ try {
22
+ const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $fd = New-Object System.Windows.Forms.OpenFileDialog; $fd.Filter='${filter.replace(/"/g, '\\"')}'; $fd.Multiselect=$true; if($fd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){ $json = ConvertTo-Json @($fd.FileNames) -Compress; $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); Write-Output ([Convert]::ToBase64String($bytes)) }"`;
23
+ const buf = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] });
24
+ const output = Buffer.isBuffer(buf) ? buf.toString('utf8').trim() : String(buf).trim();
25
+ if (!output) return null;
26
+ const parsed = JSON.parse(Buffer.from(output, 'base64').toString('utf8'));
27
+ if (!Array.isArray(parsed)) return null;
28
+ return parsed;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
19
34
  export function saveFile(defaultName = 'output.txt', filter = 'All Files|*.*') {
20
35
  if (!isWindows()) return null;
21
36
  try {