xw-devtool-cli 1.0.22 → 1.0.24

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
@@ -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 数据生成**:
@@ -27,6 +28,7 @@
27
28
  - **UUID**:生成 UUID v4。
28
29
  - **中文转拼音**:将汉字转换为不带声调的拼音。
29
30
  - **颜色转换**:Hex <-> RGB 互转。
31
+ - **颜色预览**:输入颜色并在终端显示颜色条,便于快速视觉确认,同时自动复制 Hex 到剪贴板。
30
32
  - **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
31
33
  - **哈希计算**:支持 MD5, SHA1, SHA256, SHA512, SM3 算法。
32
34
  - **二维码生成**:终端直接显示二维码,支持保存为 PNG 图片(带时间戳文件名)。
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.
@@ -26,6 +27,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
26
27
  - **UUID**: Generate UUID v4.
27
28
  - **Pinyin**: Convert Chinese characters to Pinyin (without tone).
28
29
  - **Color Converter**: Hex <-> RGB.
30
+ - **Color Preview**: Enter a color and display a color bar in terminal for quick visual confirmation; Hex is auto-copied to clipboard.
29
31
  - **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
30
32
  - **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
31
33
  - **QR Code**: Display QR codes in terminal, save as PNG.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -0,0 +1,72 @@
1
+ import inquirer from 'inquirer';
2
+ import tinycolor from 'tinycolor2';
3
+ import { copy } from '../utils/clipboard.js';
4
+ import i18next from '../i18n.js';
5
+ // No width/height selection per user request; fixed size preview
6
+
7
+ function blend(fg, bg, alpha) {
8
+ const a = Math.max(0, Math.min(1, alpha ?? 1));
9
+ return {
10
+ r: Math.round(a * fg.r + (1 - a) * bg.r),
11
+ g: Math.round(a * fg.g + (1 - a) * bg.g),
12
+ b: Math.round(a * fg.b + (1 - a) * bg.b),
13
+ };
14
+ }
15
+
16
+ function printColorBar(rgb, width = 50, height = 2, label = '', alpha = 1) {
17
+ const reset = '\x1b[0m';
18
+ const block = ' ';
19
+ if (label) {
20
+ console.log(label);
21
+ }
22
+ const useChecker = alpha < 1;
23
+ const bg1 = { r: 255, g: 255, b: 255 };
24
+ const bg2 = { r: 204, g: 204, b: 204 };
25
+ for (let row = 0; row < height; row++) {
26
+ let line = '';
27
+ for (let col = 0; col < width; col++) {
28
+ const checker = ((Math.floor(row / 1) + Math.floor(col / 2)) % 2 === 0) ? bg1 : bg2;
29
+ const blended = useChecker ? blend(rgb, checker, alpha) : rgb;
30
+ const bg = `\x1b[48;2;${blended.r};${blended.g};${blended.b}m`;
31
+ line += `${bg}${block}`;
32
+ }
33
+ line += reset;
34
+ console.log(line);
35
+ }
36
+ }
37
+
38
+ function isAllowedColorFormat(val) {
39
+ if (!val || typeof val !== 'string') return false;
40
+ const s = val.trim();
41
+ const hex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
42
+ const rgb = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
43
+ const rgba = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(?:0|1|0?\.\d+)\s*\)$/i;
44
+ const hsl = /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i;
45
+ const hsla = /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(?:0|1|0?\.\d+)\s*\)$/i;
46
+ return hex.test(s) || rgb.test(s) || rgba.test(s) || hsl.test(s) || hsla.test(s);
47
+ }
48
+
49
+ export async function colorPreviewHandler() {
50
+ const { input } = await inquirer.prompt([
51
+ {
52
+ type: 'input',
53
+ name: 'input',
54
+ message: i18next.t('colorPreview.input'),
55
+ validate: (val) => isAllowedColorFormat(val) || i18next.t('colorPreview.invalid')
56
+ }
57
+ ]);
58
+
59
+ const color = tinycolor(input);
60
+ const rgb = color.toRgb();
61
+ const alpha = typeof rgb.a === 'number' ? rgb.a : 1;
62
+ const hex = alpha < 1 ? color.toHex8String().toUpperCase() : color.toHexString().toUpperCase();
63
+
64
+ const width = 50;
65
+ const height = 2;
66
+
67
+ const label = `${i18next.t('colorPreview.preview')} ${hex} ${color.toRgbString()} ${color.toHslString()}`;
68
+ printColorBar(rgb, width, height, label, alpha);
69
+
70
+ await copy(hex);
71
+ console.log(i18next.t('colorPreview.copied', { value: hex }));
72
+ }
@@ -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,12 +8,14 @@ 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';
14
15
  import { uuidHandler } from './commands/uuid.js';
15
16
  import { pinyinHandler } from './commands/pinyin.js';
16
17
  import { colorHandler } from './commands/color.js';
18
+ import { colorPreviewHandler } from './commands/colorPreview.js';
17
19
  import { variableFormatHandler } from './commands/variableFormat.js';
18
20
  import { jsonFormatHandler } from './commands/jsonFormat.js';
19
21
  import { hashingHandler } from './commands/hashing.js';
@@ -46,6 +48,7 @@ function getFeatures() {
46
48
  // Image Tools
47
49
  { name: i18next.t('menu.features.imgBase64'), value: 'imgBase64' },
48
50
  { name: i18next.t('menu.features.imgConvert'), value: 'imgConvert' },
51
+ { name: i18next.t('menu.features.imgSplit'), value: 'imgSplit' },
49
52
  { name: i18next.t('menu.features.dominantColor'), value: 'dominantColor' },
50
53
  { name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
51
54
  { name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
@@ -63,6 +66,7 @@ function getFeatures() {
63
66
  { name: i18next.t('menu.features.timeFormat'), value: 'timeFormat' },
64
67
  { name: i18next.t('menu.features.timeCalc'), value: 'timeCalc' },
65
68
  { name: i18next.t('menu.features.color'), value: 'color' },
69
+ { name: i18next.t('menu.features.colorPreview'), value: 'colorPreview' },
66
70
  { name: i18next.t('menu.features.uuid'), value: 'uuid' },
67
71
  { name: i18next.t('menu.features.hashing'), value: 'hashing' },
68
72
 
@@ -148,6 +152,7 @@ async function showMenu() {
148
152
  const selectedFeature = features[index];
149
153
 
150
154
  try {
155
+ // console.log('Handling action:', selectedFeature.value);
151
156
  await handleAction(selectedFeature.value);
152
157
  } catch (error) {
153
158
  console.error('Error:', error.message);
@@ -173,6 +178,9 @@ async function handleAction(action) {
173
178
  case 'imgConvert':
174
179
  await imgConvertHandler();
175
180
  break;
181
+ case 'imgSplit':
182
+ await imgSplitHandler();
183
+ break;
176
184
  case 'dominantColor':
177
185
  await dominantColorHandler();
178
186
  break;
@@ -194,6 +202,9 @@ async function handleAction(action) {
194
202
  case 'color':
195
203
  await colorHandler();
196
204
  break;
205
+ case 'colorPreview':
206
+ await colorPreviewHandler();
207
+ break;
197
208
  case 'variableFormat':
198
209
  await variableFormatHandler();
199
210
  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',
@@ -21,6 +22,7 @@ export default {
21
22
  timeFormat: 'Time Format / Timestamp',
22
23
  timeCalc: 'Time Calculation (Diff/Offset)',
23
24
  color: 'Color Converter (Hex <-> RGB)',
25
+ colorPreview: 'Color Preview',
24
26
  uuid: 'Get UUID',
25
27
  hashing: 'Hash Calculator (MD5/SHA/SM3)',
26
28
  mock: 'Mock Text',
@@ -36,6 +38,12 @@ export default {
36
38
  saved: 'Language saved. Please restart the tool or continue using the menu.',
37
39
  backToMenu: 'Back to Menu'
38
40
  },
41
+ colorPreview: {
42
+ input: 'Enter color (Hex/RGB/HSL):',
43
+ invalid: 'Invalid color format',
44
+ preview: 'Preview:',
45
+ copied: 'Copied to clipboard: {{value}}'
46
+ },
39
47
  url: {
40
48
  title: 'URL Encode/Decode',
41
49
  encode: 'Encode',
@@ -43,5 +51,15 @@ export default {
43
51
  inputPrompt: 'Enter URL to {{mode}}:',
44
52
  result: 'Result:',
45
53
  error: 'Error processing URL:'
54
+ },
55
+ imgSplit: {
56
+ mode: 'Select split mode',
57
+ modeGrid: 'Grid Split (Equal parts)',
58
+ modeCustom: 'Custom Split Lines',
59
+ rows: 'Number of rows:',
60
+ cols: 'Number of columns:',
61
+ hLines: 'Horizontal split lines (e.g. 10 20 30 or 10% 20%):',
62
+ vLines: 'Vertical split lines (e.g. 10 20 30 or 10% 20%):',
63
+ success: 'Successfully split image into {{count}} parts. Saved to {{path}}'
46
64
  }
47
65
  };
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: '二维码生成器',
@@ -21,6 +22,7 @@ export default {
21
22
  timeFormat: '时间格式化 / 时间戳',
22
23
  timeCalc: '时间计算 (差值/偏移)',
23
24
  color: '颜色转换 (Hex <-> RGB)',
25
+ colorPreview: '颜色预览',
24
26
  uuid: '生成 UUID',
25
27
  hashing: '哈希计算 (MD5/SHA/SM3)',
26
28
  mock: 'Mock 文本生成',
@@ -36,6 +38,12 @@ export default {
36
38
  saved: '语言已保存。请继续使用。',
37
39
  backToMenu: '返回主菜单'
38
40
  },
41
+ colorPreview: {
42
+ input: '输入颜色 (Hex/RGB/HSL):',
43
+ invalid: '颜色格式无效',
44
+ preview: '预览:',
45
+ copied: '已复制到剪贴板: {{value}}'
46
+ },
39
47
  url: {
40
48
  title: 'URL 编码/解码',
41
49
  encode: '编码',
@@ -43,5 +51,15 @@ export default {
43
51
  inputPrompt: '请输入要{{mode}}的 URL:',
44
52
  result: '结果:',
45
53
  error: '处理 URL 时出错:'
54
+ },
55
+ imgSplit: {
56
+ mode: '选择分割模式',
57
+ modeGrid: '网格分割 (等分)',
58
+ modeCustom: '自定义分割线',
59
+ rows: '行数:',
60
+ cols: '列数:',
61
+ hLines: '水平分割线 (例如 10 20 30 或 10% 20%):',
62
+ vLines: '垂直分割线 (例如 10 20 30 或 10% 20%):',
63
+ success: '成功将图片分割为 {{count}} 份。保存至 {{path}}'
46
64
  }
47
65
  };