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