xw-devtool-cli 1.0.20 → 1.0.22
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 +8 -4
- package/package.json +3 -2
- package/src/commands/dominantColor.js +109 -0
- package/src/commands/imgConvert.js +37 -14
- package/src/index.js +5 -0
- package/src/locales/en.js +1 -0
- package/src/locales/zh.js +1 -0
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
- **Base64 工具**:支持字符串与 Base64 互转,支持从剪贴板、文件或手动输入读取。
|
|
12
12
|
- **图片工具**:
|
|
13
|
-
- **图片格式转换**:支持 PNG、JPG、WebP
|
|
13
|
+
- **图片格式转换**:支持 PNG、JPG、WebP、ICO 格式互转,可调整压缩质量。生成 ICO 时支持自定义尺寸(逗号分隔),留空则默认生成 `256` 尺寸。输出文件名默认带时间戳,避免覆盖。
|
|
14
14
|
- **图片 ↔ Base64**:支持图片转 Base64 字符串,以及 Base64 还原为图片文件。
|
|
15
|
+
- **图片主色识别**:提取图片主色调(Hex/RGB/HSL/HSV),默认复制 Hex 到剪贴板,支持保存详细信息到文件。
|
|
15
16
|
- **占位图生成**:快速生成指定尺寸、颜色、文字的占位图片 (Placeholder Image)。
|
|
16
17
|
- **Mock 数据生成**:
|
|
17
18
|
- 支持生成:英文段落 (Lorem Ipsum)、中文字符、中国居民身份证号、电子邮箱、URL、订单号、手机号、座机号。
|
|
@@ -79,7 +80,8 @@ xw-devtool --en # 英文启动 (Start in English)
|
|
|
79
80
|
1. 图片 ↔ Base64
|
|
80
81
|
2. 图片格式转换
|
|
81
82
|
3. 占位图生成
|
|
82
|
-
4.
|
|
83
|
+
4. 图片主色识别
|
|
84
|
+
5. 二维码生成
|
|
83
85
|
5. URL 编解码
|
|
84
86
|
6. 字符串 Base64 转换
|
|
85
87
|
7. Unicode 编解码
|
|
@@ -116,9 +118,10 @@ s. 设置 (语言)
|
|
|
116
118
|
### 2. 图片格式转换
|
|
117
119
|
- 选择 `2` 进入。
|
|
118
120
|
- 选择源图片文件。
|
|
119
|
-
- 选择目标格式 (PNG / JPG / WebP)。
|
|
121
|
+
- 选择目标格式 (PNG / JPG / WebP / ICO)。
|
|
120
122
|
- 设置压缩参数(如 JPG 质量 1-100,PNG 压缩等级 0-9)。
|
|
121
123
|
- 生成的新图片将保存在源文件同级目录。
|
|
124
|
+
- 生成 ICO 时可自定义尺寸,留空默认生成 `256` 尺寸;输出文件名包含时间戳。
|
|
122
125
|
|
|
123
126
|
### 3. 占位图生成 (Placeholder Image)
|
|
124
127
|
- 选择 `3` 进入。
|
|
@@ -287,8 +290,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
287
290
|
|
|
288
291
|
- **Base64 Tools**: Convert strings to/from Base64, supports reading from clipboard, file, or manual input.
|
|
289
292
|
- **Image Tools**:
|
|
290
|
-
- **Format Conversion**: Convert between PNG, JPG, WebP, with adjustable compression quality.
|
|
293
|
+
- **Format Conversion**: Convert between PNG, JPG, WebP, ICO, with adjustable compression quality. For ICO, you can specify sizes (comma-separated); leave empty to generate `256` size by default. Output filenames include timestamps to avoid overwriting.
|
|
291
294
|
- **Image ↔ Base64**: Convert images to Base64 strings and vice versa.
|
|
295
|
+
- **Image Dominant Color**: Extract dominant color (Hex/RGB/HSL/HSV), copy Hex to clipboard by default, or save details to file.
|
|
292
296
|
- **Placeholder Image**: Quickly generate placeholder images with custom size, color, and text.
|
|
293
297
|
- **Mock Data**:
|
|
294
298
|
- Generate: Lorem Ipsum, Chinese characters, ID cards, Emails, URLs, Order IDs, Phone numbers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xw-devtool-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "基于node的开发者助手cli",
|
|
6
6
|
"main": "index.js",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"qrcode": "^1.5.4",
|
|
76
76
|
"sharp": "^0.33.5",
|
|
77
77
|
"tinycolor2": "^1.6.0",
|
|
78
|
-
"uuid": "^13.0.0"
|
|
78
|
+
"uuid": "^13.0.0",
|
|
79
|
+
"to-ico": "^1.1.5"
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import tinycolor from 'tinycolor2';
|
|
4
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
5
|
+
import { selectFile, saveFile } from '../utils/fileDialog.js';
|
|
6
|
+
import { outputText, defaultFileName } from '../utils/output.js';
|
|
7
|
+
|
|
8
|
+
export async function dominantColorHandler() {
|
|
9
|
+
const method = await selectFromMenu('Image input method', [
|
|
10
|
+
{ name: 'Select file (dialog)', value: 'dialog' },
|
|
11
|
+
{ name: 'Enter file path manually', value: 'manual' }
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
let filePath = '';
|
|
15
|
+
if (method === 'dialog') {
|
|
16
|
+
filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.webp;*.gif|All Files|*.*') || '';
|
|
17
|
+
}
|
|
18
|
+
if (!filePath) {
|
|
19
|
+
const ans = await inquirer.prompt([
|
|
20
|
+
{ type: 'input', name: 'filePath', message: 'Enter image file path:' }
|
|
21
|
+
]);
|
|
22
|
+
filePath = ans.filePath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const image = sharp(filePath);
|
|
27
|
+
// Resize to 100x100 to speed up processing while maintaining color distribution
|
|
28
|
+
const { data, info } = await image
|
|
29
|
+
.resize(100, 100, { fit: 'inside' })
|
|
30
|
+
.ensureAlpha()
|
|
31
|
+
.raw()
|
|
32
|
+
.toBuffer({ resolveWithObject: true });
|
|
33
|
+
|
|
34
|
+
const pixelCount = info.width * info.height;
|
|
35
|
+
const colorMap = new Map();
|
|
36
|
+
|
|
37
|
+
// Quantize colors to group similar ones (4 bits per channel = 16 values per channel)
|
|
38
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
39
|
+
const offset = i * 4;
|
|
40
|
+
const r = data[offset];
|
|
41
|
+
const g = data[offset + 1];
|
|
42
|
+
const b = data[offset + 2];
|
|
43
|
+
const a = data[offset + 3];
|
|
44
|
+
|
|
45
|
+
if (a < 128) continue; // Ignore transparent pixels
|
|
46
|
+
|
|
47
|
+
// Quantize: divide by 16 (right shift 4)
|
|
48
|
+
const key = `${r >> 4}-${g >> 4}-${b >> 4}`;
|
|
49
|
+
|
|
50
|
+
if (!colorMap.has(key)) {
|
|
51
|
+
colorMap.set(key, { count: 0, r: 0, g: 0, b: 0 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const bucket = colorMap.get(key);
|
|
55
|
+
bucket.count++;
|
|
56
|
+
bucket.r += r;
|
|
57
|
+
bucket.g += g;
|
|
58
|
+
bucket.b += b;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let maxCount = 0;
|
|
62
|
+
let dominant = { r: 0, g: 0, b: 0 };
|
|
63
|
+
|
|
64
|
+
for (const bucket of colorMap.values()) {
|
|
65
|
+
if (bucket.count > maxCount) {
|
|
66
|
+
maxCount = bucket.count;
|
|
67
|
+
dominant = {
|
|
68
|
+
r: Math.round(bucket.r / bucket.count),
|
|
69
|
+
g: Math.round(bucket.g / bucket.count),
|
|
70
|
+
b: Math.round(bucket.b / bucket.count)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { r, g, b } = dominant;
|
|
76
|
+
const hex = tinycolor({ r, g, b }).toHexString().toUpperCase();
|
|
77
|
+
const rgb = `rgb(${r}, ${g}, ${b})`;
|
|
78
|
+
const hsl = tinycolor({ r, g, b }).toHslString();
|
|
79
|
+
const hsv = tinycolor({ r, g, b }).toHsvString();
|
|
80
|
+
|
|
81
|
+
const text = [
|
|
82
|
+
'=== Dominant Color ===',
|
|
83
|
+
`Hex: ${hex}`,
|
|
84
|
+
`RGB: ${rgb}`,
|
|
85
|
+
`HSL: ${hsl}`,
|
|
86
|
+
`HSV: ${hsv}`,
|
|
87
|
+
'======================'
|
|
88
|
+
].join('\n');
|
|
89
|
+
|
|
90
|
+
const outputMethod = await selectFromMenu('Output', [
|
|
91
|
+
{ name: 'Copy Hex to clipboard', value: 'copy' },
|
|
92
|
+
{ name: 'Save details to file', value: 'file' },
|
|
93
|
+
{ name: 'Preview + copy Hex', value: 'preview' }
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
if (outputMethod === 'file') {
|
|
97
|
+
const def = defaultFileName('dominant-color', 'txt');
|
|
98
|
+
const out = saveFile(def, 'Text Files|*.txt|All Files|*.*') || def;
|
|
99
|
+
await outputText(text, { showPreview: false, savePath: out });
|
|
100
|
+
} else if (outputMethod === 'preview') {
|
|
101
|
+
await outputText(text, { showPreview: true });
|
|
102
|
+
await outputText(hex, { showPreview: false });
|
|
103
|
+
} else {
|
|
104
|
+
await outputText(hex, { showPreview: false });
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error('Failed to analyze image:', e.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -2,6 +2,7 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
+
import toIco from 'to-ico';
|
|
5
6
|
import { selectFromMenu } from '../utils/menu.js';
|
|
6
7
|
import { selectFile, saveFile } from '../utils/fileDialog.js';
|
|
7
8
|
import { defaultFileName } from '../utils/output.js';
|
|
@@ -15,7 +16,8 @@ function getExt(p) {
|
|
|
15
16
|
function suggestOutputPath(inputPath, targetExt) {
|
|
16
17
|
const dir = path.dirname(inputPath);
|
|
17
18
|
const base = path.basename(inputPath, path.extname(inputPath));
|
|
18
|
-
|
|
19
|
+
const ts = Date.now();
|
|
20
|
+
return path.join(dir, `${base}-${ts}.${targetExt}`);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
async function pickInputFile() {
|
|
@@ -48,7 +50,8 @@ async function pickTargetFormat(currentExt) {
|
|
|
48
50
|
const options = [
|
|
49
51
|
{ name: 'png', value: 'png' },
|
|
50
52
|
{ name: 'jpg', value: 'jpg' },
|
|
51
|
-
{ name: 'webp', value: 'webp' }
|
|
53
|
+
{ name: 'webp', value: 'webp' },
|
|
54
|
+
{ name: 'ico', value: 'ico' }
|
|
52
55
|
].filter(o => o.value !== currentExt);
|
|
53
56
|
return await selectFromMenu('Target format', options);
|
|
54
57
|
}
|
|
@@ -69,6 +72,18 @@ async function pickQuality(targetExt) {
|
|
|
69
72
|
{ type: 'number', name: 'quality', message: 'WebP quality (1-100):', default: 80, validate: n => (n >= 1 && n <= 100) ? true : '1-100' }
|
|
70
73
|
]);
|
|
71
74
|
return { webpQuality: quality };
|
|
75
|
+
} else if (targetExt === 'ico') {
|
|
76
|
+
const { sizesStr } = await inquirer.prompt([
|
|
77
|
+
{ type: 'input', name: 'sizesStr', message: 'Enter ICO sizes (comma-separated). Leave empty for default 256:', default: '' }
|
|
78
|
+
]);
|
|
79
|
+
const defaultSizes = [256];
|
|
80
|
+
const allowed = new Set([16, 24, 32, 48, 64, 128, 256]);
|
|
81
|
+
const parsed = (sizesStr || '')
|
|
82
|
+
.split(',')
|
|
83
|
+
.map(s => parseInt(s.trim()))
|
|
84
|
+
.filter(n => Number.isFinite(n) && allowed.has(n));
|
|
85
|
+
const uniq = Array.from(new Set(parsed));
|
|
86
|
+
return { icoSizes: uniq.length ? uniq : defaultSizes };
|
|
72
87
|
}
|
|
73
88
|
return {};
|
|
74
89
|
}
|
|
@@ -82,7 +97,7 @@ export async function imgConvertHandler() {
|
|
|
82
97
|
console.log(`Target format: ${targetExt}`);
|
|
83
98
|
const opts = await pickQuality(targetExt);
|
|
84
99
|
|
|
85
|
-
let outPath = saveFile(suggestOutputPath(inputPath, targetExt), 'Image Files|*.png;*.jpg;*.jpeg;*.webp|All Files|*.*');
|
|
100
|
+
let outPath = saveFile(suggestOutputPath(inputPath, targetExt), 'Image Files|*.png;*.jpg;*.jpeg;*.webp;*.ico|All Files|*.*');
|
|
86
101
|
if (!outPath) {
|
|
87
102
|
const def = suggestOutputPath(inputPath, targetExt);
|
|
88
103
|
const ans = await inquirer.prompt([
|
|
@@ -93,19 +108,27 @@ export async function imgConvertHandler() {
|
|
|
93
108
|
|
|
94
109
|
try {
|
|
95
110
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
if (targetExt === 'ico') {
|
|
112
|
+
const basePng = await sharp(inputPath).ensureAlpha().png().toBuffer();
|
|
113
|
+
const sizes = opts.icoSizes ?? [16, 24, 32, 48, 64, 128, 256];
|
|
114
|
+
const icoBuffer = await toIco([basePng], { resize: true, sizes });
|
|
115
|
+
fs.writeFileSync(outPath, icoBuffer);
|
|
116
|
+
console.log(`Converted to ICO: ${inputPath} -> ${outPath}`);
|
|
117
|
+
} else {
|
|
118
|
+
let pipeline = sharp(inputPath);
|
|
119
|
+
if (targetExt === 'png') {
|
|
120
|
+
pipeline = pipeline.png({ compressionLevel: opts.pngCompressionLevel ?? 6 });
|
|
121
|
+
} else if (targetExt === 'jpg') {
|
|
122
|
+
pipeline = pipeline.jpeg({ quality: opts.jpegQuality ?? 80 });
|
|
123
|
+
if (inputExt !== 'jpg') {
|
|
124
|
+
pipeline = pipeline.flatten({ background: { r: 255, g: 255, b: 255 } });
|
|
125
|
+
}
|
|
126
|
+
} else if (targetExt === 'webp') {
|
|
127
|
+
pipeline = pipeline.webp({ quality: opts.webpQuality ?? 80 });
|
|
103
128
|
}
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
await pipeline.toFile(outPath);
|
|
130
|
+
console.log(`Converted: ${inputPath} -> ${outPath}`);
|
|
106
131
|
}
|
|
107
|
-
await pipeline.toFile(outPath);
|
|
108
|
-
console.log(`Converted: ${inputPath} -> ${outPath}`);
|
|
109
132
|
} catch (e) {
|
|
110
133
|
console.error('Conversion error:', e.message);
|
|
111
134
|
}
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { htmlEntitiesHandler } from './commands/htmlEntities.js';
|
|
|
24
24
|
import { placeholderImgHandler } from './commands/placeholderImg.js';
|
|
25
25
|
import { markdownHandler } from './commands/markdown.js';
|
|
26
26
|
import { vscodeSnippetHandler } from './commands/vscodeSnippet.js';
|
|
27
|
+
import { dominantColorHandler } from './commands/dominantColor.js';
|
|
27
28
|
|
|
28
29
|
process.on('SIGINT', () => {
|
|
29
30
|
console.log(`\n${i18next.t('menu.bye')}`);
|
|
@@ -45,6 +46,7 @@ function getFeatures() {
|
|
|
45
46
|
// Image Tools
|
|
46
47
|
{ name: i18next.t('menu.features.imgBase64'), value: 'imgBase64' },
|
|
47
48
|
{ name: i18next.t('menu.features.imgConvert'), value: 'imgConvert' },
|
|
49
|
+
{ name: i18next.t('menu.features.dominantColor'), value: 'dominantColor' },
|
|
48
50
|
{ name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
|
|
49
51
|
{ name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
|
|
50
52
|
|
|
@@ -171,6 +173,9 @@ async function handleAction(action) {
|
|
|
171
173
|
case 'imgConvert':
|
|
172
174
|
await imgConvertHandler();
|
|
173
175
|
break;
|
|
176
|
+
case 'dominantColor':
|
|
177
|
+
await dominantColorHandler();
|
|
178
|
+
break;
|
|
174
179
|
case 'timeFormat':
|
|
175
180
|
await timeFormatHandler();
|
|
176
181
|
break;
|
package/src/locales/en.js
CHANGED