vexell 1.0.0 ā 1.0.1
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/index.js +233 -21
- package/package.json +8 -2
- package/output_node.png +0 -0
- package/output_rust.png +0 -0
package/index.js
CHANGED
|
@@ -3,35 +3,247 @@
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const sharp = require('sharp');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { globSync } = require('glob');
|
|
8
|
+
const chokidar = require('chokidar');
|
|
9
|
+
const readline = require('readline');
|
|
6
10
|
|
|
7
11
|
program
|
|
8
12
|
.name('vexell')
|
|
9
|
-
.description('Vexell: Blazing fast lossless SVG to
|
|
10
|
-
.version('1.0.
|
|
11
|
-
.argument('
|
|
12
|
-
.
|
|
13
|
-
.option('-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
.description('Vexell: Blazing fast lossless SVG to image converter')
|
|
14
|
+
.version('1.0.1')
|
|
15
|
+
.argument('[input]', 'Input SVG file or glob pattern (leave empty for interactive mode)')
|
|
16
|
+
.option('-b, --background <color>', 'Background color (e.g., "#ffffff", "white")')
|
|
17
|
+
.option('-O, --optimize', 'Optimize output (higher compression)', false)
|
|
18
|
+
.option('-w, --watch', 'Watch input files for changes', false)
|
|
19
|
+
.action(async (inputPattern, output, options) => {
|
|
20
|
+
const chalk = (await import('chalk')).default;
|
|
21
|
+
const ora = (await import('ora')).default;
|
|
22
|
+
const figlet = require('figlet');
|
|
23
|
+
|
|
24
|
+
readline.emitKeypressEvents(process.stdin);
|
|
25
|
+
if (process.stdin.isTTY) {
|
|
26
|
+
process.stdin.setRawMode(true);
|
|
27
|
+
process.stdin.resume();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!inputPattern) {
|
|
31
|
+
const { input, select, confirm } = await import('@inquirer/prompts');
|
|
32
|
+
|
|
33
|
+
let isWatching = false;
|
|
34
|
+
let currentWatcher = null;
|
|
35
|
+
|
|
36
|
+
// Map ESC to Ctrl+C so Inquirer natively aborts the prompt
|
|
37
|
+
const globalEscListener = (str, key) => {
|
|
38
|
+
if (key && key.name === 'escape') {
|
|
39
|
+
if (isWatching && currentWatcher) {
|
|
40
|
+
currentWatcher.close();
|
|
41
|
+
isWatching = false;
|
|
42
|
+
currentWatcher = null;
|
|
43
|
+
} else {
|
|
44
|
+
// Simulate Ctrl+C to tell Inquirer to abort
|
|
45
|
+
process.stdin.emit('keypress', '\x03', { name: 'c', ctrl: true, meta: false, shift: false });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
process.stdin.on('keypress', globalEscListener);
|
|
50
|
+
|
|
51
|
+
const mainMenuLoop = async () => {
|
|
52
|
+
while (true) {
|
|
53
|
+
console.clear();
|
|
54
|
+
console.log(chalk.cyanBright(figlet.textSync('Vexell', { font: 'Slant' })));
|
|
55
|
+
console.log(chalk.gray('Interactive Console Mode. Press ESC anytime to cancel operation.\n'));
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const action = await select({
|
|
59
|
+
message: chalk.white('Main Menu:'),
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: 'šŖ Start Conversion Wizard', value: 'wizard' },
|
|
62
|
+
{ name: 'š Start Watch Mode', value: 'watch' },
|
|
63
|
+
{ name: 'šŖ Exit Vexell', value: 'exit' }
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (action === 'exit') {
|
|
68
|
+
console.log(chalk.green('\nGoodbye! āØ\n'));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log();
|
|
73
|
+
let pat = await input({
|
|
74
|
+
message: chalk.white('Enter input SVG file or glob pattern:'),
|
|
75
|
+
validate: (val) => val.trim().length > 0 || 'Input is required'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let format = await select({
|
|
79
|
+
message: chalk.white('Select output format:'),
|
|
80
|
+
choices: [
|
|
81
|
+
{ name: 'PNG', value: 'png' },
|
|
82
|
+
{ name: 'WebP', value: 'webp' },
|
|
83
|
+
{ name: 'AVIF', value: 'avif' },
|
|
84
|
+
{ name: 'JPEG', value: 'jpeg' },
|
|
85
|
+
]
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let size = await input({ message: chalk.white('Enter output resolution in pixels:'), default: '1024' });
|
|
89
|
+
|
|
90
|
+
let bg = null;
|
|
91
|
+
const addBg = await confirm({ message: chalk.white('Add a background color?'), default: false });
|
|
92
|
+
if (addBg) {
|
|
93
|
+
bg = await input({ message: chalk.white('Enter background color (e.g. #ffffff):'), default: '#ffffff' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let opt = await confirm({ message: chalk.white('Enable aggressive optimization?'), default: false });
|
|
97
|
+
|
|
98
|
+
let outInput = await input({ message: chalk.white('Enter output file or directory (leave blank to auto-generate):') });
|
|
99
|
+
let out = outInput.trim() || undefined;
|
|
100
|
+
|
|
101
|
+
if (action === 'watch') {
|
|
102
|
+
console.log(chalk.yellow('\n[Press ESC at any time to stop watching and return to the main menu]\n'));
|
|
103
|
+
|
|
104
|
+
// Inquirer pauses stdin after prompts. We MUST resume it to catch ESC.
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
process.stdin.setRawMode(true);
|
|
107
|
+
process.stdin.resume();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isWatching = true;
|
|
111
|
+
|
|
112
|
+
await new Promise((resolve) => {
|
|
113
|
+
currentWatcher = chokidar.watch(pat, { persistent: true });
|
|
114
|
+
currentWatcher
|
|
115
|
+
.on('add', (fp) => doProcess(fp, out, size, format, bg, opt, chalk, ora))
|
|
116
|
+
.on('change', (fp) => doProcess(fp, out, size, format, bg, opt, chalk, ora))
|
|
117
|
+
.on('error', err => console.error(chalk.red(`Watcher error: ${err}`)));
|
|
118
|
+
|
|
119
|
+
const checkInterval = setInterval(() => {
|
|
120
|
+
if (!isWatching) {
|
|
121
|
+
clearInterval(checkInterval);
|
|
122
|
+
console.log(chalk.yellow('\nWatch mode stopped. Returning to menu...\n'));
|
|
123
|
+
setTimeout(resolve, 1000);
|
|
124
|
+
}
|
|
125
|
+
}, 200);
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
const normalizedPattern = pat.replace(/\\/g, '/');
|
|
129
|
+
const files = globSync(normalizedPattern);
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
console.log(chalk.red(`\nNo files found matching ${pat}\n`));
|
|
132
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.blueBright(`\nš Starting conversion for ${files.length} file(s)...\n`));
|
|
136
|
+
await Promise.all(files.map(f => doProcess(f, out, size, format, bg, opt, chalk, ora)));
|
|
137
|
+
console.log(chalk.greenBright(`\n⨠Done! Returning to menu...\n`));
|
|
138
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err.name === 'ExitPromptError') {
|
|
143
|
+
console.log(chalk.yellow('\nOperation cancelled. Returning to menu...\n'));
|
|
144
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
145
|
+
} else {
|
|
146
|
+
console.error(chalk.red('\nError:'), err);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
await mainMenuLoop();
|
|
154
|
+
return;
|
|
18
155
|
}
|
|
19
156
|
|
|
157
|
+
// ---- Standard CLI Mode (arguments provided) ----
|
|
158
|
+
process.stdin.on('keypress', (str, key) => {
|
|
159
|
+
if (key && (key.name === 'escape' || (key.ctrl && key.name === 'c'))) {
|
|
160
|
+
console.log('\n\x1b[33mAborted.\x1b[0m');
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
20
165
|
const size = parseInt(options.size);
|
|
166
|
+
const format = options.format.toLowerCase();
|
|
167
|
+
|
|
168
|
+
if (!['png', 'webp', 'avif', 'jpeg', 'jpg'].includes(format)) {
|
|
169
|
+
console.error(chalk.red(`Error: Unsupported format ${format}`));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isOutputDirectory = !output || output.endsWith('/') || output.endsWith('\\') || (fs.existsSync(output) && fs.statSync(output).isDirectory()) || !output.includes('.');
|
|
21
174
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
175
|
+
if (options.watch) {
|
|
176
|
+
console.log(chalk.blueBright(`\nš Watching for changes: ${inputPattern}\n`));
|
|
177
|
+
console.log(chalk.yellow(`[Press ESC to abort]\n`));
|
|
178
|
+
const watcher = chokidar.watch(inputPattern, { persistent: true });
|
|
179
|
+
watcher
|
|
180
|
+
.on('add', (fp) => doProcess(fp, output, size, format, options.background, options.optimize, chalk, ora, isOutputDirectory))
|
|
181
|
+
.on('change', (fp) => doProcess(fp, output, size, format, options.background, options.optimize, chalk, ora, isOutputDirectory))
|
|
182
|
+
.on('error', error => console.error(chalk.red(`Watcher error: ${error}`)));
|
|
183
|
+
} else {
|
|
184
|
+
const normalizedPattern = inputPattern.replace(/\\/g, '/');
|
|
185
|
+
const files = globSync(normalizedPattern);
|
|
186
|
+
|
|
187
|
+
if (files.length === 0) {
|
|
188
|
+
console.error(chalk.red(`\nError: No files found matching ${inputPattern}\n`));
|
|
33
189
|
process.exit(1);
|
|
34
|
-
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (files.length > 1 && !isOutputDirectory && output) {
|
|
193
|
+
console.error(chalk.red(`\nError: Output must be a directory when processing multiple files.\n`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(chalk.blueBright(`\nš Starting conversion for ${files.length} file(s)...\n`));
|
|
198
|
+
await Promise.all(files.map(fp => doProcess(fp, output, size, format, options.background, options.optimize, chalk, ora, isOutputDirectory)));
|
|
199
|
+
console.log(chalk.greenBright(`\n⨠All tasks completed successfully!\n`));
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
35
202
|
});
|
|
36
203
|
|
|
204
|
+
async function doProcess(filePath, output, size, format, background, optimize, chalk, ora, isOutputDirectoryParam) {
|
|
205
|
+
const isOutputDirectory = isOutputDirectoryParam ?? (!output || output.endsWith('/') || output.endsWith('\\') || (fs.existsSync(output) && fs.statSync(output).isDirectory()) || !output.includes('.'));
|
|
206
|
+
|
|
207
|
+
let outPath = output;
|
|
208
|
+
|
|
209
|
+
if (!output) {
|
|
210
|
+
outPath = filePath.replace(/\.svg$/i, `.${format}`);
|
|
211
|
+
} else if (isOutputDirectory) {
|
|
212
|
+
if (!fs.existsSync(output)) {
|
|
213
|
+
fs.mkdirSync(output, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
216
|
+
outPath = path.join(output, `${baseName}.${format}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const spinner = ora(`Vexellizing ${chalk.cyan(filePath)} -> ${chalk.greenBright(outPath)}`).start();
|
|
220
|
+
|
|
221
|
+
let pipeline = sharp(filePath).resize(parseInt(size), parseInt(size), { fit: 'inside' });
|
|
222
|
+
|
|
223
|
+
if (background) {
|
|
224
|
+
pipeline = pipeline.flatten({ background: background });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (format === 'png') {
|
|
228
|
+
pipeline = pipeline.png({
|
|
229
|
+
compressionLevel: optimize ? 9 : 6,
|
|
230
|
+
adaptiveFiltering: true,
|
|
231
|
+
force: true
|
|
232
|
+
});
|
|
233
|
+
} else if (format === 'webp') {
|
|
234
|
+
pipeline = pipeline.webp({ quality: optimize ? 80 : 100, lossless: !optimize });
|
|
235
|
+
} else if (format === 'avif') {
|
|
236
|
+
pipeline = pipeline.avif({ quality: optimize ? 50 : 80, lossless: !optimize });
|
|
237
|
+
} else if (format === 'jpeg' || format === 'jpg') {
|
|
238
|
+
pipeline = pipeline.jpeg({ quality: optimize ? 80 : 100 });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const info = await pipeline.toFile(outPath);
|
|
243
|
+
spinner.succeed(`Rendered ${chalk.greenBright(outPath)} ${chalk.gray(`(${info.width}x${info.height})`)}`);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
spinner.fail(chalk.red(`Failed ${filePath}: ${err.message}`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
37
249
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vexell",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Blazing fast lossless SVG to
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Blazing fast lossless SVG to image converter",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vexell": "./index.js"
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"type": "commonjs",
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@inquirer/prompts": "^8.5.2",
|
|
26
|
+
"chalk": "^5.6.2",
|
|
27
|
+
"chokidar": "^5.0.0",
|
|
25
28
|
"commander": "^15.0.0",
|
|
29
|
+
"figlet": "^1.11.0",
|
|
30
|
+
"glob": "^13.0.6",
|
|
31
|
+
"ora": "^9.4.0",
|
|
26
32
|
"sharp": "^0.35.2"
|
|
27
33
|
}
|
|
28
34
|
}
|
package/output_node.png
DELETED
|
Binary file
|
package/output_rust.png
DELETED
|
Binary file
|