@emasoft/svg-matrix 1.0.21 → 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/bin/svgm.js ADDED
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview SVGO-compatible CLI for @emasoft/svg-matrix
4
+ * Drop-in replacement for SVGO with arbitrary-precision transforms.
5
+ *
6
+ * Usage mirrors SVGO exactly:
7
+ * svgm input.svg # optimize in place
8
+ * svgm input.svg -o output.svg # optimize to output
9
+ * svgm -f folder/ -o out/ # batch folder
10
+ * svgm -p 3 input.svg # set precision
11
+ *
12
+ * @module bin/svgm
13
+ * @license MIT
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
17
+ import { join, dirname, basename, extname, resolve, isAbsolute } from 'path';
18
+
19
+ // Import library modules
20
+ import { VERSION } from '../src/index.js';
21
+ import * as SVGToolbox from '../src/svg-toolbox.js';
22
+ import { parseSVG, serializeSVG } from '../src/svg-parser.js';
23
+
24
+ // ============================================================================
25
+ // CONSTANTS
26
+ // ============================================================================
27
+ const CONSTANTS = {
28
+ DEFAULT_PRECISION: 6,
29
+ MAX_PRECISION: 50,
30
+ MIN_PRECISION: 0,
31
+ MAX_FILE_SIZE_BYTES: 50 * 1024 * 1024,
32
+ EXIT_SUCCESS: 0,
33
+ EXIT_ERROR: 1,
34
+ SVG_EXTENSIONS: ['.svg', '.svgz'],
35
+ };
36
+
37
+ // ============================================================================
38
+ // COLORS (respects NO_COLOR env)
39
+ // ============================================================================
40
+ const colors = process.env.NO_COLOR !== undefined || process.argv.includes('--no-color') ? {
41
+ reset: '', red: '', yellow: '', green: '', cyan: '', dim: '', bright: '',
42
+ } : {
43
+ reset: '\x1b[0m', red: '\x1b[31m', yellow: '\x1b[33m',
44
+ green: '\x1b[32m', cyan: '\x1b[36m', dim: '\x1b[2m', bright: '\x1b[1m',
45
+ };
46
+
47
+ // ============================================================================
48
+ // CONFIGURATION
49
+ // ============================================================================
50
+ const DEFAULT_CONFIG = {
51
+ inputs: [],
52
+ folder: null,
53
+ output: null,
54
+ string: null,
55
+ precision: null, // null means use default from plugins
56
+ multipass: false,
57
+ pretty: false,
58
+ indent: 2,
59
+ eol: null,
60
+ finalNewline: false,
61
+ recursive: false,
62
+ exclude: [],
63
+ quiet: false,
64
+ datauri: null,
65
+ showPlugins: false,
66
+ };
67
+
68
+ let config = { ...DEFAULT_CONFIG };
69
+
70
+ // ============================================================================
71
+ // LOGGING
72
+ // ============================================================================
73
+ function log(msg) {
74
+ if (!config.quiet) console.log(msg);
75
+ }
76
+
77
+ function logError(msg) {
78
+ console.error(`${colors.red}error:${colors.reset} ${msg}`);
79
+ }
80
+
81
+ // ============================================================================
82
+ // AVAILABLE OPTIMIZATIONS (matching SVGO plugins)
83
+ // ============================================================================
84
+ const OPTIMIZATIONS = [
85
+ // Cleanup plugins
86
+ { name: 'cleanupAttributes', description: 'Remove useless attributes' },
87
+ { name: 'cleanupIds', description: 'Remove unused and minify used IDs' },
88
+ { name: 'cleanupNumericValues', description: 'Round numeric values, remove default px units' },
89
+ { name: 'cleanupListOfValues', description: 'Round list of numeric values' },
90
+ { name: 'cleanupEnableBackground', description: 'Remove or fix enable-background attribute' },
91
+ // Remove plugins
92
+ { name: 'removeDoctype', description: 'Remove DOCTYPE declaration' },
93
+ { name: 'removeXMLProcInst', description: 'Remove XML processing instructions' },
94
+ { name: 'removeComments', description: 'Remove comments' },
95
+ { name: 'removeMetadata', description: 'Remove <metadata> elements' },
96
+ { name: 'removeTitle', description: 'Remove <title> elements (not in default)' },
97
+ { name: 'removeDesc', description: 'Remove <desc> elements' },
98
+ { name: 'removeEditorsNSData', description: 'Remove editor namespaces, elements, and attributes' },
99
+ { name: 'removeEmptyAttrs', description: 'Remove empty attributes' },
100
+ { name: 'removeEmptyContainers', description: 'Remove empty container elements' },
101
+ { name: 'removeEmptyText', description: 'Remove empty text elements' },
102
+ { name: 'removeHiddenElements', description: 'Remove hidden elements' },
103
+ { name: 'removeUselessDefs', description: 'Remove unused <defs> content' },
104
+ { name: 'removeUnknownsAndDefaults', description: 'Remove unknown elements and default attribute values' },
105
+ { name: 'removeNonInheritableGroupAttrs', description: 'Remove non-inheritable presentation attributes from groups' },
106
+ // Convert plugins
107
+ { name: 'convertShapesToPath', description: 'Convert basic shapes to paths' },
108
+ { name: 'convertPathData', description: 'Optimize path data: convert, remove useless, etc.' },
109
+ { name: 'convertTransform', description: 'Collapse multiple transforms into one, convert matrices' },
110
+ { name: 'convertColors', description: 'Convert color values to shorter form' },
111
+ { name: 'convertStyleToAttrs', description: 'Convert style to presentation attributes (not in default)' },
112
+ { name: 'convertEllipseToCircle', description: 'Convert ellipse to circle when rx equals ry' },
113
+ // Structure plugins
114
+ { name: 'collapseGroups', description: 'Collapse useless groups' },
115
+ { name: 'mergePaths', description: 'Merge multiple paths into one' },
116
+ { name: 'moveGroupAttrsToElems', description: 'Move group attributes to contained elements' },
117
+ { name: 'moveElemsAttrsToGroup', description: 'Move common element attributes to parent group' },
118
+ // Style plugins
119
+ { name: 'minifyStyles', description: 'Minify <style> elements content' },
120
+ { name: 'inlineStyles', description: 'Inline styles from <style> to element style attributes' },
121
+ // Sort plugins
122
+ { name: 'sortAttrs', description: 'Sort attributes for better gzip compression' },
123
+ { name: 'sortDefsChildren', description: 'Sort children of <defs> for better gzip compression' },
124
+ ];
125
+
126
+ // ============================================================================
127
+ // DEFAULT PIPELINE - Matches SVGO preset-default exactly (34 plugins)
128
+ // Order matters! This is the exact order from SVGO's preset-default.js
129
+ // ============================================================================
130
+ const DEFAULT_PIPELINE = [
131
+ // 1-6: Initial cleanup (matching SVGO preset-default order)
132
+ 'removeDoctype',
133
+ 'removeXMLProcInst',
134
+ 'removeComments',
135
+ // removeDeprecatedAttrs - not implemented (rarely needed)
136
+ 'removeMetadata',
137
+ 'removeEditorsNSData',
138
+ // 7-11: Style processing
139
+ 'cleanupAttributes',
140
+ // mergeStyles - not implemented
141
+ 'inlineStyles',
142
+ 'minifyStyles',
143
+ 'cleanupIds',
144
+ // 12-18: Remove unnecessary elements
145
+ 'removeUselessDefs',
146
+ 'cleanupNumericValues',
147
+ 'convertColors',
148
+ 'removeUnknownsAndDefaults',
149
+ 'removeNonInheritableGroupAttrs',
150
+ // removeUselessStrokeAndFill - not implemented
151
+ 'cleanupEnableBackground',
152
+ 'removeHiddenElements',
153
+ 'removeEmptyText',
154
+ // 19-27: Convert and optimize
155
+ // NOTE: convertShapesToPath removed - SVGO only converts when it saves bytes
156
+ // Our version converts all shapes which often increases size
157
+ 'convertEllipseToCircle',
158
+ 'moveElemsAttrsToGroup',
159
+ 'moveGroupAttrsToElems',
160
+ 'collapseGroups',
161
+ 'convertPathData',
162
+ 'convertTransform',
163
+ // 28-34: Final cleanup
164
+ 'removeEmptyAttrs',
165
+ 'removeEmptyContainers',
166
+ 'mergePaths',
167
+ // removeUnusedNS - not implemented
168
+ 'sortAttrs',
169
+ 'sortDefsChildren',
170
+ 'removeDesc',
171
+ ];
172
+
173
+ // ============================================================================
174
+ // PATH UTILITIES
175
+ // ============================================================================
176
+ function normalizePath(p) { return p.replace(/\\/g, '/'); }
177
+
178
+ function resolvePath(p) {
179
+ return isAbsolute(p) ? normalizePath(p) : normalizePath(resolve(process.cwd(), p));
180
+ }
181
+
182
+ function isDir(p) { try { return statSync(p).isDirectory(); } catch { return false; } }
183
+ function isFile(p) { try { return statSync(p).isFile(); } catch { return false; } }
184
+
185
+ function ensureDir(dir) {
186
+ if (!existsSync(dir)) {
187
+ mkdirSync(dir, { recursive: true });
188
+ }
189
+ }
190
+
191
+ function getSvgFiles(dir, recursive = false, exclude = []) {
192
+ const files = [];
193
+ function scan(d) {
194
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
195
+ const fullPath = join(d, entry.name);
196
+ // Check exclusion patterns
197
+ const shouldExclude = exclude.some(pattern => {
198
+ const regex = new RegExp(pattern);
199
+ return regex.test(fullPath) || regex.test(entry.name);
200
+ });
201
+ if (shouldExclude) continue;
202
+
203
+ if (entry.isDirectory() && recursive) scan(fullPath);
204
+ else if (entry.isFile() && CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())) {
205
+ files.push(normalizePath(fullPath));
206
+ }
207
+ }
208
+ }
209
+ scan(dir);
210
+ return files;
211
+ }
212
+
213
+ // ============================================================================
214
+ // SVG OPTIMIZATION
215
+ // ============================================================================
216
+ async function optimizeSvg(content, options = {}) {
217
+ const doc = parseSVG(content);
218
+ const pipeline = DEFAULT_PIPELINE;
219
+
220
+ // Run optimization pipeline
221
+ for (const pluginName of pipeline) {
222
+ const fn = SVGToolbox[pluginName];
223
+ if (fn && typeof fn === 'function') {
224
+ try {
225
+ await fn(doc, { precision: options.precision });
226
+ } catch (e) {
227
+ // Skip failed optimizations silently
228
+ }
229
+ }
230
+ }
231
+
232
+ // Multipass: run again if requested
233
+ if (options.multipass) {
234
+ for (const pluginName of pipeline) {
235
+ const fn = SVGToolbox[pluginName];
236
+ if (fn && typeof fn === 'function') {
237
+ try {
238
+ await fn(doc, { precision: options.precision });
239
+ } catch (e) {
240
+ // Skip failed optimizations silently
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ let result = serializeSVG(doc);
247
+
248
+ // Pretty print if requested, otherwise minify (SVGO default behavior)
249
+ if (options.pretty) {
250
+ result = prettifyXml(result, options.indent);
251
+ } else {
252
+ result = minifyXml(result);
253
+ }
254
+
255
+ // Handle EOL
256
+ if (options.eol === 'crlf') {
257
+ result = result.replace(/\n/g, '\r\n');
258
+ }
259
+
260
+ // Final newline
261
+ if (options.finalNewline && !result.endsWith('\n')) {
262
+ result += '\n';
263
+ }
264
+
265
+ return result;
266
+ }
267
+
268
+ /**
269
+ * Minify XML output (matching SVGO behavior)
270
+ * - Remove XML declaration
271
+ * - Remove whitespace between tags
272
+ * - Collapse multiple spaces
273
+ */
274
+ function minifyXml(xml) {
275
+ return xml
276
+ // Remove XML declaration (SVGO removes it by default)
277
+ .replace(/<\?xml[^?]*\?>\s*/gi, '')
278
+ // Remove newlines and collapse whitespace between tags
279
+ .replace(/>\s+</g, '><')
280
+ // Remove leading/trailing whitespace
281
+ .trim();
282
+ }
283
+
284
+ function prettifyXml(xml, indent = 2) {
285
+ // Simple XML prettifier
286
+ const indentStr = ' '.repeat(indent);
287
+ let formatted = '';
288
+ let depth = 0;
289
+
290
+ // Split on tags
291
+ xml.replace(/>\s*</g, '>\n<').split('\n').forEach(line => {
292
+ line = line.trim();
293
+ if (!line) return;
294
+
295
+ // Decrease depth for closing tags
296
+ if (line.startsWith('</')) {
297
+ depth = Math.max(0, depth - 1);
298
+ }
299
+
300
+ formatted += indentStr.repeat(depth) + line + '\n';
301
+
302
+ // Increase depth for opening tags (not self-closing)
303
+ if (line.startsWith('<') && !line.startsWith('</') && !line.startsWith('<?') &&
304
+ !line.startsWith('<!') && !line.endsWith('/>') && !line.includes('</')) {
305
+ depth++;
306
+ }
307
+ });
308
+
309
+ return formatted.trim();
310
+ }
311
+
312
+ function toDataUri(content, format) {
313
+ if (format === 'base64') {
314
+ return 'data:image/svg+xml;base64,' + Buffer.from(content).toString('base64');
315
+ } else if (format === 'enc') {
316
+ return 'data:image/svg+xml,' + encodeURIComponent(content);
317
+ } else {
318
+ return 'data:image/svg+xml,' + content;
319
+ }
320
+ }
321
+
322
+ // ============================================================================
323
+ // PROCESS FILES
324
+ // ============================================================================
325
+ async function processFile(inputPath, outputPath, options) {
326
+ try {
327
+ const content = readFileSync(inputPath, 'utf8');
328
+ const originalSize = Buffer.byteLength(content);
329
+
330
+ const optimized = await optimizeSvg(content, options);
331
+ const optimizedSize = Buffer.byteLength(optimized);
332
+
333
+ let output = optimized;
334
+ if (options.datauri) {
335
+ output = toDataUri(optimized, options.datauri);
336
+ }
337
+
338
+ if (outputPath === '-') {
339
+ process.stdout.write(output);
340
+ } else {
341
+ ensureDir(dirname(outputPath));
342
+ writeFileSync(outputPath, output, 'utf8');
343
+ }
344
+
345
+ const savings = originalSize - optimizedSize;
346
+ const percent = ((savings / originalSize) * 100).toFixed(1);
347
+
348
+ return { success: true, originalSize, optimizedSize, savings, percent, inputPath, outputPath };
349
+ } catch (error) {
350
+ return { success: false, error: error.message, inputPath };
351
+ }
352
+ }
353
+
354
+ // ============================================================================
355
+ // HELP
356
+ // ============================================================================
357
+ function showHelp() {
358
+ console.log(`Usage: svgm [options] [INPUT...]
359
+
360
+ SVGM is an SVGO-compatible CLI powered by svg-matrix for arbitrary-precision
361
+ SVG optimization.
362
+
363
+ Arguments:
364
+ INPUT Input files (or use -i, -f, -s)
365
+
366
+ Options:
367
+ -v, --version Output the version number
368
+ -i, --input <INPUT...> Input files, "-" for STDIN
369
+ -s, --string <STRING> Input SVG data string
370
+ -f, --folder <FOLDER> Input folder, optimize and rewrite all *.svg files
371
+ -o, --output <OUTPUT...> Output file or folder (by default same as input),
372
+ "-" for STDOUT
373
+ -p, --precision <INTEGER> Set number of digits in the fractional part,
374
+ overrides plugins params
375
+ --datauri <FORMAT> Output as Data URI string (base64), URI encoded
376
+ (enc) or unencoded (unenc)
377
+ --multipass Pass over SVGs multiple times to ensure all
378
+ optimizations are applied
379
+ --pretty Make SVG pretty printed
380
+ --indent <INTEGER> Indent number when pretty printing SVGs
381
+ --eol <EOL> Line break to use when outputting SVG: lf, crlf
382
+ --final-newline Ensure SVG ends with a line break
383
+ -r, --recursive Use with '--folder'. Optimizes *.svg files in
384
+ folders recursively.
385
+ --exclude <PATTERN...> Use with '--folder'. Exclude files matching
386
+ regular expression pattern.
387
+ -q, --quiet Only output error messages
388
+ --show-plugins Show available plugins and exit
389
+ --no-color Output plain text without color
390
+ -h, --help Display help for command
391
+
392
+ Examples:
393
+ svgm input.svg -o output.svg
394
+ svgm -f ./icons/ -o ./optimized/
395
+ svgm input.svg --pretty --indent 4
396
+ svgm -p 2 --multipass input.svg
397
+
398
+ Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
399
+ }
400
+
401
+ function showVersion() {
402
+ console.log(VERSION);
403
+ }
404
+
405
+ function showPlugins() {
406
+ console.log('\nAvailable optimizations:\n');
407
+ for (const opt of OPTIMIZATIONS) {
408
+ console.log(` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`);
409
+ }
410
+ console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
411
+ }
412
+
413
+ // ============================================================================
414
+ // ARGUMENT PARSING
415
+ // ============================================================================
416
+ function parseArgs(args) {
417
+ const cfg = { ...DEFAULT_CONFIG };
418
+ const inputs = [];
419
+ let i = 0;
420
+
421
+ while (i < args.length) {
422
+ const arg = args[i];
423
+
424
+ switch (arg) {
425
+ case '-v':
426
+ case '--version':
427
+ showVersion();
428
+ process.exit(CONSTANTS.EXIT_SUCCESS);
429
+ break;
430
+
431
+ case '-h':
432
+ case '--help':
433
+ showHelp();
434
+ process.exit(CONSTANTS.EXIT_SUCCESS);
435
+ break;
436
+
437
+ case '-i':
438
+ case '--input':
439
+ i++;
440
+ while (i < args.length && !args[i].startsWith('-')) {
441
+ inputs.push(args[i]);
442
+ i++;
443
+ }
444
+ i--; // Back up one since the while loop went past
445
+ break;
446
+
447
+ case '-s':
448
+ case '--string':
449
+ cfg.string = args[++i];
450
+ break;
451
+
452
+ case '-f':
453
+ case '--folder':
454
+ cfg.folder = args[++i];
455
+ break;
456
+
457
+ case '-o':
458
+ case '--output':
459
+ i++;
460
+ // Collect output(s)
461
+ const outputs = [];
462
+ while (i < args.length && !args[i].startsWith('-')) {
463
+ outputs.push(args[i]);
464
+ i++;
465
+ }
466
+ i--;
467
+ cfg.output = outputs.length === 1 ? outputs[0] : outputs;
468
+ break;
469
+
470
+ case '-p':
471
+ case '--precision':
472
+ cfg.precision = parseInt(args[++i], 10);
473
+ break;
474
+
475
+ case '--datauri':
476
+ cfg.datauri = args[++i];
477
+ break;
478
+
479
+ case '--multipass':
480
+ cfg.multipass = true;
481
+ break;
482
+
483
+ case '--pretty':
484
+ cfg.pretty = true;
485
+ break;
486
+
487
+ case '--indent':
488
+ cfg.indent = parseInt(args[++i], 10);
489
+ break;
490
+
491
+ case '--eol':
492
+ cfg.eol = args[++i];
493
+ break;
494
+
495
+ case '--final-newline':
496
+ cfg.finalNewline = true;
497
+ break;
498
+
499
+ case '-r':
500
+ case '--recursive':
501
+ cfg.recursive = true;
502
+ break;
503
+
504
+ case '--exclude':
505
+ i++;
506
+ while (i < args.length && !args[i].startsWith('-')) {
507
+ cfg.exclude.push(args[i]);
508
+ i++;
509
+ }
510
+ i--;
511
+ break;
512
+
513
+ case '-q':
514
+ case '--quiet':
515
+ cfg.quiet = true;
516
+ break;
517
+
518
+ case '--show-plugins':
519
+ cfg.showPlugins = true;
520
+ break;
521
+
522
+ case '--no-color':
523
+ // Already handled in colors initialization
524
+ break;
525
+
526
+ default:
527
+ if (arg.startsWith('-')) {
528
+ logError(`Unknown option: ${arg}`);
529
+ process.exit(CONSTANTS.EXIT_ERROR);
530
+ }
531
+ inputs.push(arg);
532
+ }
533
+ i++;
534
+ }
535
+
536
+ cfg.inputs = inputs;
537
+ return cfg;
538
+ }
539
+
540
+ // ============================================================================
541
+ // MAIN
542
+ // ============================================================================
543
+ async function main() {
544
+ const args = process.argv.slice(2);
545
+
546
+ if (args.length === 0) {
547
+ showHelp();
548
+ process.exit(CONSTANTS.EXIT_SUCCESS);
549
+ }
550
+
551
+ config = parseArgs(args);
552
+
553
+ if (config.showPlugins) {
554
+ showPlugins();
555
+ process.exit(CONSTANTS.EXIT_SUCCESS);
556
+ }
557
+
558
+ const options = {
559
+ precision: config.precision,
560
+ multipass: config.multipass,
561
+ pretty: config.pretty,
562
+ indent: config.indent,
563
+ eol: config.eol,
564
+ finalNewline: config.finalNewline,
565
+ datauri: config.datauri,
566
+ };
567
+
568
+ // Handle string input
569
+ if (config.string) {
570
+ try {
571
+ const result = await optimizeSvg(config.string, options);
572
+ const output = config.datauri ? toDataUri(result, config.datauri) : result;
573
+ if (config.output && config.output !== '-') {
574
+ writeFileSync(config.output, output, 'utf8');
575
+ log(`${colors.green}Done!${colors.reset}`);
576
+ } else {
577
+ process.stdout.write(output);
578
+ }
579
+ } catch (e) {
580
+ logError(e.message);
581
+ process.exit(CONSTANTS.EXIT_ERROR);
582
+ }
583
+ return;
584
+ }
585
+
586
+ // Gather input files
587
+ let files = [];
588
+
589
+ if (config.folder) {
590
+ const folderPath = resolvePath(config.folder);
591
+ if (!isDir(folderPath)) {
592
+ logError(`Folder not found: ${config.folder}`);
593
+ process.exit(CONSTANTS.EXIT_ERROR);
594
+ }
595
+ files = getSvgFiles(folderPath, config.recursive, config.exclude);
596
+ }
597
+
598
+ // Add explicit inputs
599
+ for (const input of config.inputs) {
600
+ if (input === '-') {
601
+ // STDIN handling would go here
602
+ logError('STDIN not yet supported');
603
+ process.exit(CONSTANTS.EXIT_ERROR);
604
+ }
605
+ const resolved = resolvePath(input);
606
+ if (isFile(resolved)) {
607
+ files.push(resolved);
608
+ } else if (isDir(resolved)) {
609
+ files.push(...getSvgFiles(resolved, config.recursive, config.exclude));
610
+ } else {
611
+ logError(`File not found: ${input}`);
612
+ process.exit(CONSTANTS.EXIT_ERROR);
613
+ }
614
+ }
615
+
616
+ if (files.length === 0) {
617
+ logError('No input files');
618
+ process.exit(CONSTANTS.EXIT_ERROR);
619
+ }
620
+
621
+ // Process files
622
+ let totalOriginal = 0;
623
+ let totalOptimized = 0;
624
+ let successCount = 0;
625
+ let errorCount = 0;
626
+
627
+ for (let i = 0; i < files.length; i++) {
628
+ const inputPath = files[i];
629
+ let outputPath;
630
+
631
+ if (config.output) {
632
+ if (config.output === '-') {
633
+ outputPath = '-';
634
+ } else if (Array.isArray(config.output)) {
635
+ outputPath = config.output[i] || config.output[0];
636
+ } else if (files.length > 1 || isDir(resolvePath(config.output))) {
637
+ // Multiple files or output is a directory
638
+ outputPath = join(resolvePath(config.output), basename(inputPath));
639
+ } else {
640
+ outputPath = resolvePath(config.output);
641
+ }
642
+ } else {
643
+ // In-place optimization (same as input)
644
+ outputPath = inputPath;
645
+ }
646
+
647
+ const result = await processFile(inputPath, outputPath, options);
648
+
649
+ if (result.success) {
650
+ successCount++;
651
+ totalOriginal += result.originalSize;
652
+ totalOptimized += result.optimizedSize;
653
+
654
+ if (outputPath !== '-') {
655
+ log(`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`);
656
+ }
657
+ } else {
658
+ errorCount++;
659
+ logError(`${basename(inputPath)}: ${result.error}`);
660
+ }
661
+ }
662
+
663
+ // Summary
664
+ if (files.length > 1 && !config.quiet) {
665
+ const totalSavings = totalOriginal - totalOptimized;
666
+ const totalPercent = totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
667
+ console.log(`\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`);
668
+ console.log(`${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`);
669
+ }
670
+
671
+ if (errorCount > 0) {
672
+ process.exit(CONSTANTS.EXIT_ERROR);
673
+ }
674
+ }
675
+
676
+ main().catch((e) => {
677
+ logError(e.message);
678
+ process.exit(CONSTANTS.EXIT_ERROR);
679
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emasoft/svg-matrix",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "bin": {
57
57
  "svg-matrix": "bin/svg-matrix.js",
58
+ "svgm": "bin/svgm.js",
58
59
  "svglinter": "bin/svglinter.cjs"
59
60
  },
60
61
  "engines": {
package/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * SVG path conversion, and 2D/3D affine transformations using Decimal.js.
6
6
  *
7
7
  * @module @emasoft/svg-matrix
8
- * @version 1.0.21
8
+ * @version 1.0.23
9
9
  * @license MIT
10
10
  *
11
11
  * @example
@@ -86,7 +86,7 @@ Decimal.set({ precision: 80 });
86
86
  * Library version
87
87
  * @constant {string}
88
88
  */
89
- export const VERSION = '1.0.21';
89
+ export const VERSION = '1.0.23';
90
90
 
91
91
  /**
92
92
  * Default precision for path output (decimal places)