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 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 PNG converter')
10
- .version('1.0.0')
11
- .argument('<input>', 'Input SVG file')
12
- .argument('<output>', 'Output PNG file')
13
- .option('-s, --size <pixels>', 'Output resolution (width and height)', 1024)
14
- .action((input, output, options) => {
15
- if (!fs.existsSync(input)) {
16
- console.error(`Error: File ${input} not found.`);
17
- process.exit(1);
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
- console.log(`Vexellizing ${input} -> ${output}...`);
23
-
24
- sharp(input)
25
- .resize(size, size, { fit: 'inside' })
26
- .png({ compressionLevel: 9, adaptiveFiltering: true, force: true })
27
- .toFile(output)
28
- .then(info => {
29
- console.log(`Successfully rendered! (${info.width}x${info.height})`);
30
- })
31
- .catch(err => {
32
- console.error(`Error converting file: ${err.message}`);
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.0",
4
- "description": "Blazing fast lossless SVG to PNG converter",
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