xw-devtool-cli 1.0.22 → 1.0.23
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 +1 -0
- package/README_EN.md +1 -0
- package/package.json +1 -1
- package/src/commands/imgSplit.js +173 -0
- package/src/index.js +6 -0
- package/src/locales/en.js +11 -0
- package/src/locales/zh.js +11 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- **图片工具**:
|
|
13
13
|
- **图片格式转换**:支持 PNG、JPG、WebP、ICO 格式互转,可调整压缩质量。生成 ICO 时支持自定义尺寸(逗号分隔),留空则默认生成 `256` 尺寸。输出文件名默认带时间戳,避免覆盖。
|
|
14
14
|
- **图片 ↔ Base64**:支持图片转 Base64 字符串,以及 Base64 还原为图片文件。
|
|
15
|
+
- **图片分割**:支持网格等分(自定义行/列数)或自定义分割线(像素/百分比),自动生成分割后的图片文件。
|
|
15
16
|
- **图片主色识别**:提取图片主色调(Hex/RGB/HSL/HSV),默认复制 Hex 到剪贴板,支持保存详细信息到文件。
|
|
16
17
|
- **占位图生成**:快速生成指定尺寸、颜色、文字的占位图片 (Placeholder Image)。
|
|
17
18
|
- **Mock 数据生成**:
|
package/README_EN.md
CHANGED
|
@@ -12,6 +12,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
12
12
|
- **Image Tools**:
|
|
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
|
+
- **Image Splitter**: Split images into grid (rows/cols) or custom split lines (pixels/percentage).
|
|
15
16
|
- **Placeholder Image**: Quickly generate placeholder images with custom size, color, and text.
|
|
16
17
|
- **Mock Data**:
|
|
17
18
|
- Generate: Lorem Ipsum, Chinese characters, ID cards, Emails, URLs, Order IDs, Phone numbers.
|
package/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import i18next from '../i18n.js';
|
|
6
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
7
|
+
import { selectFile } from '../utils/fileDialog.js';
|
|
8
|
+
|
|
9
|
+
async function pickInputFile() {
|
|
10
|
+
const method = await selectFromMenu('Image Splitter - Input Method', [
|
|
11
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
12
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
13
|
+
]);
|
|
14
|
+
let filePath = '';
|
|
15
|
+
if (method === 'dialog') {
|
|
16
|
+
filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.webp|All Files|*.*') || '';
|
|
17
|
+
if (!filePath) {
|
|
18
|
+
console.log('File dialog not available or canceled. Please enter path manually.');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!filePath) {
|
|
22
|
+
const ans = await inquirer.prompt([
|
|
23
|
+
{
|
|
24
|
+
type: 'input',
|
|
25
|
+
name: 'filePath',
|
|
26
|
+
message: 'Enter image file path:',
|
|
27
|
+
validate: (input) => (fs.existsSync(input) ? true : 'File does not exist.')
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
filePath = ans.filePath;
|
|
31
|
+
}
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseSplitLines(input, totalSize) {
|
|
36
|
+
if (!input || !input.trim()) return [0, totalSize];
|
|
37
|
+
|
|
38
|
+
const tokens = input.trim().split(/[\s,]+/);
|
|
39
|
+
const points = tokens.map(t => {
|
|
40
|
+
t = t.trim();
|
|
41
|
+
let val = 0;
|
|
42
|
+
if (t.endsWith('px')) {
|
|
43
|
+
val = parseFloat(t.replace('px', ''));
|
|
44
|
+
} else if (t.endsWith('%')) {
|
|
45
|
+
val = (parseFloat(t.replace('%', '')) / 100) * totalSize;
|
|
46
|
+
} else {
|
|
47
|
+
// Default to percentage as per requirement
|
|
48
|
+
val = (parseFloat(t) / 100) * totalSize;
|
|
49
|
+
}
|
|
50
|
+
return Math.round(val);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Filter valid points
|
|
54
|
+
const validPoints = points.filter(p => p > 0 && p < totalSize);
|
|
55
|
+
|
|
56
|
+
// Add start and end
|
|
57
|
+
const allPoints = [0, ...validPoints, totalSize];
|
|
58
|
+
|
|
59
|
+
// Sort and unique
|
|
60
|
+
return [...new Set(allPoints)].sort((a, b) => a - b);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function imgSplitHandler() {
|
|
64
|
+
console.log('\nStarting Image Splitter...');
|
|
65
|
+
const inputPath = await pickInputFile();
|
|
66
|
+
|
|
67
|
+
if (!inputPath) {
|
|
68
|
+
console.log('No file selected.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const image = sharp(inputPath);
|
|
73
|
+
const metadata = await image.metadata();
|
|
74
|
+
const { width, height, format } = metadata;
|
|
75
|
+
|
|
76
|
+
console.log(`Image: ${inputPath} (${width}x${height})`);
|
|
77
|
+
|
|
78
|
+
const mode = await selectFromMenu(i18next.t('imgSplit.mode'), [
|
|
79
|
+
{ name: i18next.t('imgSplit.modeGrid'), value: 'grid' },
|
|
80
|
+
{ name: i18next.t('imgSplit.modeCustom'), value: 'custom' }
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
let xPoints = [];
|
|
84
|
+
let yPoints = [];
|
|
85
|
+
|
|
86
|
+
if (mode === 'grid') {
|
|
87
|
+
const { rows, cols } = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'number',
|
|
90
|
+
name: 'rows',
|
|
91
|
+
message: i18next.t('imgSplit.rows'),
|
|
92
|
+
default: 2,
|
|
93
|
+
validate: n => n > 0 || 'Must be > 0'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'number',
|
|
97
|
+
name: 'cols',
|
|
98
|
+
message: i18next.t('imgSplit.cols'),
|
|
99
|
+
default: 2,
|
|
100
|
+
validate: n => n > 0 || 'Must be > 0'
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
// Generate points for grid
|
|
105
|
+
for (let i = 0; i <= cols; i++) {
|
|
106
|
+
xPoints.push(Math.round((i * width) / cols));
|
|
107
|
+
}
|
|
108
|
+
for (let i = 0; i <= rows; i++) {
|
|
109
|
+
yPoints.push(Math.round((i * height) / rows));
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const { hLines, vLines } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'hLines',
|
|
116
|
+
message: i18next.t('imgSplit.hLines'),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'vLines',
|
|
121
|
+
message: i18next.t('imgSplit.vLines'),
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
// hLines are horizontal lines, so they split the Y axis (height)
|
|
126
|
+
// vLines are vertical lines, so they split the X axis (width)
|
|
127
|
+
yPoints = parseSplitLines(hLines, height);
|
|
128
|
+
xPoints = parseSplitLines(vLines, width);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create output directory
|
|
132
|
+
const dir = path.dirname(inputPath);
|
|
133
|
+
const base = path.basename(inputPath, path.extname(inputPath));
|
|
134
|
+
const ts = Date.now();
|
|
135
|
+
const outDir = path.join(dir, `${base}_split_${ts}`);
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(outDir)) {
|
|
138
|
+
fs.mkdirSync(outDir);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let count = 0;
|
|
142
|
+
// Iterate regions
|
|
143
|
+
// xPoints: [0, x1, x2, ..., width]
|
|
144
|
+
// yPoints: [0, y1, y2, ..., height]
|
|
145
|
+
|
|
146
|
+
const tasks = [];
|
|
147
|
+
|
|
148
|
+
for (let y = 0; y < yPoints.length - 1; y++) {
|
|
149
|
+
for (let x = 0; x < xPoints.length - 1; x++) {
|
|
150
|
+
const left = xPoints[x];
|
|
151
|
+
const top = yPoints[y];
|
|
152
|
+
const regionWidth = xPoints[x+1] - xPoints[x];
|
|
153
|
+
const regionHeight = yPoints[y+1] - yPoints[y];
|
|
154
|
+
|
|
155
|
+
if (regionWidth <= 0 || regionHeight <= 0) continue;
|
|
156
|
+
|
|
157
|
+
const outName = `${base}_${y}_${x}.${format}`;
|
|
158
|
+
const outPath = path.join(outDir, outName);
|
|
159
|
+
|
|
160
|
+
tasks.push(
|
|
161
|
+
image
|
|
162
|
+
.clone()
|
|
163
|
+
.extract({ left, top, width: regionWidth, height: regionHeight })
|
|
164
|
+
.toFile(outPath)
|
|
165
|
+
);
|
|
166
|
+
count++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await Promise.all(tasks);
|
|
171
|
+
|
|
172
|
+
console.log(i18next.t('imgSplit.success', { count, path: outDir }));
|
|
173
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { base64Handler } from './commands/base64.js';
|
|
|
8
8
|
import { unicodeHandler } from './commands/unicode.js';
|
|
9
9
|
import { imgBase64Handler } from './commands/imgBase64.js';
|
|
10
10
|
import { imgConvertHandler } from './commands/imgConvert.js';
|
|
11
|
+
import { imgSplitHandler } from './commands/imgSplit.js';
|
|
11
12
|
import { timeFormatHandler } from './commands/timeFormat.js';
|
|
12
13
|
import { timeCalcHandler } from './commands/timeCalc.js';
|
|
13
14
|
import { mockHandler } from './commands/mock.js';
|
|
@@ -46,6 +47,7 @@ function getFeatures() {
|
|
|
46
47
|
// Image Tools
|
|
47
48
|
{ name: i18next.t('menu.features.imgBase64'), value: 'imgBase64' },
|
|
48
49
|
{ name: i18next.t('menu.features.imgConvert'), value: 'imgConvert' },
|
|
50
|
+
{ name: i18next.t('menu.features.imgSplit'), value: 'imgSplit' },
|
|
49
51
|
{ name: i18next.t('menu.features.dominantColor'), value: 'dominantColor' },
|
|
50
52
|
{ name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
|
|
51
53
|
{ name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
|
|
@@ -148,6 +150,7 @@ async function showMenu() {
|
|
|
148
150
|
const selectedFeature = features[index];
|
|
149
151
|
|
|
150
152
|
try {
|
|
153
|
+
// console.log('Handling action:', selectedFeature.value);
|
|
151
154
|
await handleAction(selectedFeature.value);
|
|
152
155
|
} catch (error) {
|
|
153
156
|
console.error('Error:', error.message);
|
|
@@ -173,6 +176,9 @@ async function handleAction(action) {
|
|
|
173
176
|
case 'imgConvert':
|
|
174
177
|
await imgConvertHandler();
|
|
175
178
|
break;
|
|
179
|
+
case 'imgSplit':
|
|
180
|
+
await imgSplitHandler();
|
|
181
|
+
break;
|
|
176
182
|
case 'dominantColor':
|
|
177
183
|
await dominantColorHandler();
|
|
178
184
|
break;
|
package/src/locales/en.js
CHANGED
|
@@ -8,6 +8,7 @@ export default {
|
|
|
8
8
|
features: {
|
|
9
9
|
imgBase64: 'Image <-> Base64',
|
|
10
10
|
imgConvert: 'Image Format Convert',
|
|
11
|
+
imgSplit: 'Image Splitter',
|
|
11
12
|
dominantColor: 'Image Dominant Color',
|
|
12
13
|
placeholderImg: 'Placeholder Image Generator',
|
|
13
14
|
qrcode: 'QR Code Generator',
|
|
@@ -43,5 +44,15 @@ export default {
|
|
|
43
44
|
inputPrompt: 'Enter URL to {{mode}}:',
|
|
44
45
|
result: 'Result:',
|
|
45
46
|
error: 'Error processing URL:'
|
|
47
|
+
},
|
|
48
|
+
imgSplit: {
|
|
49
|
+
mode: 'Select split mode',
|
|
50
|
+
modeGrid: 'Grid Split (Equal parts)',
|
|
51
|
+
modeCustom: 'Custom Split Lines',
|
|
52
|
+
rows: 'Number of rows:',
|
|
53
|
+
cols: 'Number of columns:',
|
|
54
|
+
hLines: 'Horizontal split lines (e.g. 10 20 30 or 10% 20%):',
|
|
55
|
+
vLines: 'Vertical split lines (e.g. 10 20 30 or 10% 20%):',
|
|
56
|
+
success: 'Successfully split image into {{count}} parts. Saved to {{path}}'
|
|
46
57
|
}
|
|
47
58
|
};
|
package/src/locales/zh.js
CHANGED
|
@@ -8,6 +8,7 @@ export default {
|
|
|
8
8
|
features: {
|
|
9
9
|
imgBase64: '图片 <-> Base64',
|
|
10
10
|
imgConvert: '图片格式转换',
|
|
11
|
+
imgSplit: '图片分割工具',
|
|
11
12
|
dominantColor: '图片主色识别',
|
|
12
13
|
placeholderImg: '占位图生成器',
|
|
13
14
|
qrcode: '二维码生成器',
|
|
@@ -43,5 +44,15 @@ export default {
|
|
|
43
44
|
inputPrompt: '请输入要{{mode}}的 URL:',
|
|
44
45
|
result: '结果:',
|
|
45
46
|
error: '处理 URL 时出错:'
|
|
47
|
+
},
|
|
48
|
+
imgSplit: {
|
|
49
|
+
mode: '选择分割模式',
|
|
50
|
+
modeGrid: '网格分割 (等分)',
|
|
51
|
+
modeCustom: '自定义分割线',
|
|
52
|
+
rows: '行数:',
|
|
53
|
+
cols: '列数:',
|
|
54
|
+
hLines: '水平分割线 (例如 10 20 30 或 10% 20%):',
|
|
55
|
+
vLines: '垂直分割线 (例如 10 20 30 或 10% 20%):',
|
|
56
|
+
success: '成功将图片分割为 {{count}} 份。保存至 {{path}}'
|
|
46
57
|
}
|
|
47
58
|
};
|