@emasoft/svg-matrix 1.0.28 → 1.0.30

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
package/bin/svgm.js CHANGED
@@ -13,14 +13,26 @@
13
13
  * @license MIT
14
14
  */
15
15
 
16
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
17
- import { join, dirname, basename, extname, resolve, isAbsolute } from 'path';
16
+ import {
17
+ readFileSync,
18
+ writeFileSync,
19
+ existsSync,
20
+ mkdirSync,
21
+ readdirSync,
22
+ statSync,
23
+ } from "fs";
24
+ import { join, dirname, basename, extname, resolve, isAbsolute } from "path";
25
+ import yaml from "js-yaml";
18
26
 
19
27
  // 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
- import { injectPolyfills, detectSVG2Features } from '../src/svg2-polyfills.js';
28
+ import { VERSION } from "../src/index.js";
29
+ import * as SVGToolbox from "../src/svg-toolbox.js";
30
+ import { parseSVG, serializeSVG } from "../src/svg-parser.js";
31
+ import {
32
+ injectPolyfills,
33
+ detectSVG2Features,
34
+ setPolyfillMinification,
35
+ } from "../src/svg2-polyfills.js";
24
36
 
25
37
  // ============================================================================
26
38
  // CONSTANTS
@@ -32,18 +44,32 @@ const CONSTANTS = {
32
44
  MAX_FILE_SIZE_BYTES: 50 * 1024 * 1024,
33
45
  EXIT_SUCCESS: 0,
34
46
  EXIT_ERROR: 1,
35
- SVG_EXTENSIONS: ['.svg', '.svgz'],
47
+ SVG_EXTENSIONS: [".svg", ".svgz"],
36
48
  };
37
49
 
38
50
  // ============================================================================
39
51
  // COLORS (respects NO_COLOR env)
40
52
  // ============================================================================
41
- const colors = process.env.NO_COLOR !== undefined || process.argv.includes('--no-color') ? {
42
- reset: '', red: '', yellow: '', green: '', cyan: '', dim: '', bright: '',
43
- } : {
44
- reset: '\x1b[0m', red: '\x1b[31m', yellow: '\x1b[33m',
45
- green: '\x1b[32m', cyan: '\x1b[36m', dim: '\x1b[2m', bright: '\x1b[1m',
46
- };
53
+ const colors =
54
+ process.env.NO_COLOR !== undefined || process.argv.includes("--no-color")
55
+ ? {
56
+ reset: "",
57
+ red: "",
58
+ yellow: "",
59
+ green: "",
60
+ cyan: "",
61
+ dim: "",
62
+ bright: "",
63
+ }
64
+ : {
65
+ reset: "\x1b[0m",
66
+ red: "\x1b[31m",
67
+ yellow: "\x1b[33m",
68
+ green: "\x1b[32m",
69
+ cyan: "\x1b[36m",
70
+ dim: "\x1b[2m",
71
+ bright: "\x1b[1m",
72
+ };
47
73
 
48
74
  // ============================================================================
49
75
  // CONFIGURATION
@@ -64,8 +90,24 @@ const DEFAULT_CONFIG = {
64
90
  quiet: false,
65
91
  datauri: null,
66
92
  preserveNamespaces: [],
93
+ preserveVendor: false,
67
94
  svg2Polyfills: false,
68
95
  showPlugins: false,
96
+ configFile: null,
97
+ // Embed options
98
+ embed: false,
99
+ embedImages: false,
100
+ embedExternalSVGs: false,
101
+ embedExternalSVGMode: "extract",
102
+ embedCSS: false,
103
+ embedFonts: false,
104
+ embedScripts: false,
105
+ embedAudio: false,
106
+ embedSubsetFonts: false,
107
+ embedRecursive: false,
108
+ embedMaxRecursionDepth: 10,
109
+ embedTimeout: 30000,
110
+ embedOnMissingResource: "warn",
69
111
  };
70
112
 
71
113
  let config = { ...DEFAULT_CONFIG };
@@ -73,10 +115,20 @@ let config = { ...DEFAULT_CONFIG };
73
115
  // ============================================================================
74
116
  // LOGGING
75
117
  // ============================================================================
118
+ /**
119
+ * Log message to console unless in quiet mode.
120
+ * @param {string} msg - Message to log
121
+ * @returns {void}
122
+ */
76
123
  function log(msg) {
77
124
  if (!config.quiet) console.log(msg);
78
125
  }
79
126
 
127
+ /**
128
+ * Log error message to console.
129
+ * @param {string} msg - Error message
130
+ * @returns {void}
131
+ */
80
132
  function logError(msg) {
81
133
  console.error(`${colors.red}error:${colors.reset} ${msg}`);
82
134
  }
@@ -86,44 +138,98 @@ function logError(msg) {
86
138
  // ============================================================================
87
139
  const OPTIMIZATIONS = [
88
140
  // Cleanup plugins
89
- { name: 'cleanupAttributes', description: 'Remove useless attributes' },
90
- { name: 'cleanupIds', description: 'Remove unused and minify used IDs' },
91
- { name: 'cleanupNumericValues', description: 'Round numeric values, remove default px units' },
92
- { name: 'cleanupListOfValues', description: 'Round list of numeric values' },
93
- { name: 'cleanupEnableBackground', description: 'Remove or fix enable-background attribute' },
141
+ { name: "cleanupAttributes", description: "Remove useless attributes" },
142
+ { name: "cleanupIds", description: "Remove unused and minify used IDs" },
143
+ {
144
+ name: "cleanupNumericValues",
145
+ description: "Round numeric values, remove default px units",
146
+ },
147
+ { name: "cleanupListOfValues", description: "Round list of numeric values" },
148
+ {
149
+ name: "cleanupEnableBackground",
150
+ description: "Remove or fix enable-background attribute",
151
+ },
94
152
  // Remove plugins
95
- { name: 'removeDoctype', description: 'Remove DOCTYPE declaration' },
96
- { name: 'removeXMLProcInst', description: 'Remove XML processing instructions' },
97
- { name: 'removeComments', description: 'Remove comments' },
98
- { name: 'removeMetadata', description: 'Remove <metadata> elements' },
99
- { name: 'removeTitle', description: 'Remove <title> elements (not in default)' },
100
- { name: 'removeDesc', description: 'Remove <desc> elements' },
101
- { name: 'removeEditorsNSData', description: 'Remove editor namespaces, elements, and attributes' },
102
- { name: 'removeEmptyAttrs', description: 'Remove empty attributes' },
103
- { name: 'removeEmptyContainers', description: 'Remove empty container elements' },
104
- { name: 'removeEmptyText', description: 'Remove empty text elements' },
105
- { name: 'removeHiddenElements', description: 'Remove hidden elements' },
106
- { name: 'removeUselessDefs', description: 'Remove unused <defs> content' },
107
- { name: 'removeUnknownsAndDefaults', description: 'Remove unknown elements and default attribute values' },
108
- { name: 'removeNonInheritableGroupAttrs', description: 'Remove non-inheritable presentation attributes from groups' },
153
+ { name: "removeDoctype", description: "Remove DOCTYPE declaration" },
154
+ {
155
+ name: "removeXMLProcInst",
156
+ description: "Remove XML processing instructions",
157
+ },
158
+ { name: "removeComments", description: "Remove comments" },
159
+ { name: "removeMetadata", description: "Remove <metadata> elements" },
160
+ {
161
+ name: "removeTitle",
162
+ description: "Remove <title> elements (not in default)",
163
+ },
164
+ { name: "removeDesc", description: "Remove <desc> elements" },
165
+ {
166
+ name: "removeEditorsNSData",
167
+ description: "Remove editor namespaces, elements, and attributes",
168
+ },
169
+ { name: "removeEmptyAttrs", description: "Remove empty attributes" },
170
+ {
171
+ name: "removeEmptyContainers",
172
+ description: "Remove empty container elements",
173
+ },
174
+ { name: "removeEmptyText", description: "Remove empty text elements" },
175
+ { name: "removeHiddenElements", description: "Remove hidden elements" },
176
+ { name: "removeUselessDefs", description: "Remove unused <defs> content" },
177
+ {
178
+ name: "removeUnknownsAndDefaults",
179
+ description: "Remove unknown elements and default attribute values",
180
+ },
181
+ {
182
+ name: "removeNonInheritableGroupAttrs",
183
+ description: "Remove non-inheritable presentation attributes from groups",
184
+ },
109
185
  // Convert plugins
110
- { name: 'convertShapesToPath', description: 'Convert basic shapes to paths' },
111
- { name: 'convertPathData', description: 'Optimize path data: convert, remove useless, etc.' },
112
- { name: 'convertTransform', description: 'Collapse multiple transforms into one, convert matrices' },
113
- { name: 'convertColors', description: 'Convert color values to shorter form' },
114
- { name: 'convertStyleToAttrs', description: 'Convert style to presentation attributes (not in default)' },
115
- { name: 'convertEllipseToCircle', description: 'Convert ellipse to circle when rx equals ry' },
186
+ { name: "convertShapesToPath", description: "Convert basic shapes to paths" },
187
+ {
188
+ name: "convertPathData",
189
+ description: "Optimize path data: convert, remove useless, etc.",
190
+ },
191
+ {
192
+ name: "convertTransform",
193
+ description: "Collapse multiple transforms into one, convert matrices",
194
+ },
195
+ {
196
+ name: "convertColors",
197
+ description: "Convert color values to shorter form",
198
+ },
199
+ {
200
+ name: "convertStyleToAttrs",
201
+ description: "Convert style to presentation attributes (not in default)",
202
+ },
203
+ {
204
+ name: "convertEllipseToCircle",
205
+ description: "Convert ellipse to circle when rx equals ry",
206
+ },
116
207
  // Structure plugins
117
- { name: 'collapseGroups', description: 'Collapse useless groups' },
118
- { name: 'mergePaths', description: 'Merge multiple paths into one' },
119
- { name: 'moveGroupAttrsToElems', description: 'Move group attributes to contained elements' },
120
- { name: 'moveElemsAttrsToGroup', description: 'Move common element attributes to parent group' },
208
+ { name: "collapseGroups", description: "Collapse useless groups" },
209
+ { name: "mergePaths", description: "Merge multiple paths into one" },
210
+ {
211
+ name: "moveGroupAttrsToElems",
212
+ description: "Move group attributes to contained elements",
213
+ },
214
+ {
215
+ name: "moveElemsAttrsToGroup",
216
+ description: "Move common element attributes to parent group",
217
+ },
121
218
  // Style plugins
122
- { name: 'minifyStyles', description: 'Minify <style> elements content' },
123
- { name: 'inlineStyles', description: 'Inline styles from <style> to element style attributes' },
219
+ { name: "minifyStyles", description: "Minify <style> elements content" },
220
+ {
221
+ name: "inlineStyles",
222
+ description: "Inline styles from <style> to element style attributes",
223
+ },
124
224
  // Sort plugins
125
- { name: 'sortAttrs', description: 'Sort attributes for better gzip compression' },
126
- { name: 'sortDefsChildren', description: 'Sort children of <defs> for better gzip compression' },
225
+ {
226
+ name: "sortAttrs",
227
+ description: "Sort attributes for better gzip compression",
228
+ },
229
+ {
230
+ name: "sortDefsChildren",
231
+ description: "Sort children of <defs> for better gzip compression",
232
+ },
127
233
  ];
128
234
 
129
235
  // ============================================================================
@@ -132,79 +238,131 @@ const OPTIMIZATIONS = [
132
238
  // ============================================================================
133
239
  const DEFAULT_PIPELINE = [
134
240
  // 1-6: Initial cleanup (matching SVGO preset-default order)
135
- 'removeDoctype',
136
- 'removeXMLProcInst',
137
- 'removeComments',
241
+ "removeDoctype",
242
+ "removeXMLProcInst",
243
+ "removeComments",
138
244
  // removeDeprecatedAttrs - not implemented (rarely needed)
139
- 'removeMetadata',
140
- 'removeEditorsNSData',
245
+ "removeMetadata",
246
+ "removeEditorsNSData",
141
247
  // 7-11: Style processing
142
- 'cleanupAttributes',
248
+ "cleanupAttributes",
143
249
  // mergeStyles - not implemented
144
- 'inlineStyles',
145
- 'minifyStyles',
146
- 'cleanupIds',
250
+ "inlineStyles",
251
+ "minifyStyles",
252
+ "cleanupIds",
147
253
  // 12-18: Remove unnecessary elements
148
- 'removeUselessDefs',
149
- 'cleanupNumericValues',
150
- 'convertColors',
151
- 'removeUnknownsAndDefaults',
152
- 'removeNonInheritableGroupAttrs',
254
+ "removeUselessDefs",
255
+ "cleanupNumericValues",
256
+ "convertColors",
257
+ "removeUnknownsAndDefaults",
258
+ "removeNonInheritableGroupAttrs",
153
259
  // removeUselessStrokeAndFill - not implemented
154
- 'cleanupEnableBackground',
155
- 'removeHiddenElements',
156
- 'removeEmptyText',
260
+ "cleanupEnableBackground",
261
+ "removeHiddenElements",
262
+ "removeEmptyText",
157
263
  // 19-27: Convert and optimize
158
264
  // NOTE: convertShapesToPath removed - SVGO only converts when it saves bytes
159
265
  // Our version converts all shapes which often increases size
160
- 'convertEllipseToCircle',
161
- 'moveElemsAttrsToGroup',
162
- 'moveGroupAttrsToElems',
163
- 'collapseGroups',
164
- 'convertPathData',
165
- 'convertTransform',
266
+ "convertEllipseToCircle",
267
+ "moveElemsAttrsToGroup",
268
+ "moveGroupAttrsToElems",
269
+ "collapseGroups",
270
+ "convertPathData",
271
+ "convertTransform",
166
272
  // 28-34: Final cleanup
167
- 'removeEmptyAttrs',
168
- 'removeEmptyContainers',
169
- 'mergePaths',
273
+ "removeEmptyAttrs",
274
+ "removeEmptyContainers",
275
+ "mergePaths",
170
276
  // removeUnusedNS - not implemented
171
- 'sortAttrs',
172
- 'sortDefsChildren',
173
- 'removeDesc',
277
+ "sortAttrs",
278
+ "sortDefsChildren",
279
+ "removeDesc",
174
280
  ];
175
281
 
176
282
  // ============================================================================
177
283
  // PATH UTILITIES
178
284
  // ============================================================================
179
- function normalizePath(p) { return p.replace(/\\/g, '/'); }
285
+ /**
286
+ * Normalize path separators to forward slashes.
287
+ * @param {string} p - Path to normalize
288
+ * @returns {string} Normalized path
289
+ */
290
+ function normalizePath(p) {
291
+ return p.replace(/\\/g, "/");
292
+ }
180
293
 
294
+ /**
295
+ * Resolve path to absolute path with normalized separators.
296
+ * @param {string} p - Path to resolve
297
+ * @returns {string} Absolute normalized path
298
+ */
181
299
  function resolvePath(p) {
182
- return isAbsolute(p) ? normalizePath(p) : normalizePath(resolve(process.cwd(), p));
300
+ return isAbsolute(p)
301
+ ? normalizePath(p)
302
+ : normalizePath(resolve(process.cwd(), p));
303
+ }
304
+
305
+ /**
306
+ * Check if path is a directory.
307
+ * @param {string} p - Path to check
308
+ * @returns {boolean} True if directory exists
309
+ */
310
+ function isDir(p) {
311
+ try {
312
+ return statSync(p).isDirectory();
313
+ } catch {
314
+ return false;
315
+ }
183
316
  }
184
317
 
185
- function isDir(p) { try { return statSync(p).isDirectory(); } catch { return false; } }
186
- function isFile(p) { try { return statSync(p).isFile(); } catch { return false; } }
318
+ /**
319
+ * Check if path is a file.
320
+ * @param {string} p - Path to check
321
+ * @returns {boolean} True if file exists
322
+ */
323
+ function isFile(p) {
324
+ try {
325
+ return statSync(p).isFile();
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
187
330
 
331
+ /**
332
+ * Ensure directory exists, creating it if necessary.
333
+ * @param {string} dir - Directory path
334
+ * @returns {void}
335
+ */
188
336
  function ensureDir(dir) {
189
337
  if (!existsSync(dir)) {
190
338
  mkdirSync(dir, { recursive: true });
191
339
  }
192
340
  }
193
341
 
342
+ /**
343
+ * Get all SVG files in directory with optional exclusion patterns.
344
+ * @param {string} dir - Directory path
345
+ * @param {boolean} recursive - Whether to search recursively
346
+ * @param {string[]} exclude - Array of exclusion patterns
347
+ * @returns {string[]} Array of SVG file paths
348
+ */
194
349
  function getSvgFiles(dir, recursive = false, exclude = []) {
195
350
  const files = [];
196
351
  function scan(d) {
197
352
  for (const entry of readdirSync(d, { withFileTypes: true })) {
198
353
  const fullPath = join(d, entry.name);
199
354
  // Check exclusion patterns
200
- const shouldExclude = exclude.some(pattern => {
355
+ const shouldExclude = exclude.some((pattern) => {
201
356
  const regex = new RegExp(pattern);
202
357
  return regex.test(fullPath) || regex.test(entry.name);
203
358
  });
204
359
  if (shouldExclude) continue;
205
360
 
206
361
  if (entry.isDirectory() && recursive) scan(fullPath);
207
- else if (entry.isFile() && CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())) {
362
+ else if (
363
+ entry.isFile() &&
364
+ CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())
365
+ ) {
208
366
  files.push(normalizePath(fullPath));
209
367
  }
210
368
  }
@@ -216,6 +374,12 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
216
374
  // ============================================================================
217
375
  // SVG OPTIMIZATION
218
376
  // ============================================================================
377
+ /**
378
+ * Optimize SVG content using default pipeline.
379
+ * @param {string} content - SVG content
380
+ * @param {Object} options - Optimization options
381
+ * @returns {Promise<string>} Optimized SVG content
382
+ */
219
383
  async function optimizeSvg(content, options = {}) {
220
384
  const doc = parseSVG(content);
221
385
  const pipeline = DEFAULT_PIPELINE;
@@ -230,10 +394,13 @@ async function optimizeSvg(content, options = {}) {
230
394
  // Run optimization pipeline
231
395
  for (const pluginName of pipeline) {
232
396
  const fn = SVGToolbox[pluginName];
233
- if (fn && typeof fn === 'function') {
397
+ if (fn && typeof fn === "function") {
234
398
  try {
235
- await fn(doc, { precision: options.precision, preserveNamespaces: options.preserveNamespaces });
236
- } catch (e) {
399
+ await fn(doc, {
400
+ precision: options.precision,
401
+ preserveNamespaces: options.preserveNamespaces,
402
+ });
403
+ } catch {
237
404
  // Skip failed optimizations silently
238
405
  }
239
406
  }
@@ -243,10 +410,13 @@ async function optimizeSvg(content, options = {}) {
243
410
  if (options.multipass) {
244
411
  for (const pluginName of pipeline) {
245
412
  const fn = SVGToolbox[pluginName];
246
- if (fn && typeof fn === 'function') {
413
+ if (fn && typeof fn === "function") {
247
414
  try {
248
- await fn(doc, { precision: options.precision, preserveNamespaces: options.preserveNamespaces });
249
- } catch (e) {
415
+ await fn(doc, {
416
+ precision: options.precision,
417
+ preserveNamespaces: options.preserveNamespaces,
418
+ });
419
+ } catch {
250
420
  // Skip failed optimizations silently
251
421
  }
252
422
  }
@@ -255,7 +425,10 @@ async function optimizeSvg(content, options = {}) {
255
425
 
256
426
  // Inject SVG 2 polyfills if requested (using pre-detected features)
257
427
  if (options.svg2Polyfills && svg2Features) {
258
- if (svg2Features.meshGradients.length > 0 || svg2Features.hatches.length > 0) {
428
+ if (
429
+ svg2Features.meshGradients.length > 0 ||
430
+ svg2Features.hatches.length > 0
431
+ ) {
259
432
  // Pass pre-detected features since pipeline may have removed SVG2 elements
260
433
  injectPolyfills(doc, { features: svg2Features });
261
434
  }
@@ -271,78 +444,140 @@ async function optimizeSvg(content, options = {}) {
271
444
  }
272
445
 
273
446
  // Handle EOL
274
- if (options.eol === 'crlf') {
275
- result = result.replace(/\n/g, '\r\n');
447
+ if (options.eol === "crlf") {
448
+ result = result.replace(/\n/g, "\r\n");
276
449
  }
277
450
 
278
451
  // Final newline
279
- if (options.finalNewline && !result.endsWith('\n')) {
280
- result += '\n';
452
+ if (options.finalNewline && !result.endsWith("\n")) {
453
+ result += "\n";
281
454
  }
282
455
 
283
456
  return result;
284
457
  }
285
458
 
286
459
  /**
287
- * Minify XML output while keeping it valid
288
- * - KEEPS XML declaration (ensures valid SVG)
289
- * - Remove whitespace between tags
290
- * - Collapse multiple spaces
460
+ * Minify XML output while keeping it valid.
461
+ * KEEPS XML declaration (ensures valid SVG), removes whitespace between tags,
462
+ * and collapses multiple spaces.
463
+ * @param {string} xml - XML content to minify
464
+ * @returns {string} Minified XML
291
465
  */
292
466
  function minifyXml(xml) {
293
- return xml
294
- // Remove newlines and collapse whitespace between tags
295
- .replace(/>\s+</g, '><')
296
- // Remove leading/trailing whitespace
297
- .trim();
467
+ return (
468
+ xml
469
+ // Remove newlines and collapse whitespace between tags
470
+ .replace(/>\s+</g, "><")
471
+ // Remove leading/trailing whitespace
472
+ .trim()
473
+ );
298
474
  }
299
475
 
476
+ /**
477
+ * Prettify XML with indentation.
478
+ * @param {string} xml - XML content to prettify
479
+ * @param {number} indent - Number of spaces per indent level
480
+ * @returns {string} Prettified XML
481
+ */
300
482
  function prettifyXml(xml, indent = 2) {
301
483
  // Simple XML prettifier
302
- const indentStr = ' '.repeat(indent);
303
- let formatted = '';
484
+ const indentStr = " ".repeat(indent);
485
+ let formatted = "";
304
486
  let depth = 0;
305
487
 
306
488
  // Split on tags
307
- xml.replace(/>\s*</g, '>\n<').split('\n').forEach(line => {
308
- line = line.trim();
309
- if (!line) return;
310
-
311
- // Decrease depth for closing tags
312
- if (line.startsWith('</')) {
313
- depth = Math.max(0, depth - 1);
314
- }
315
-
316
- formatted += indentStr.repeat(depth) + line + '\n';
489
+ xml
490
+ .replace(/>\s*</g, ">\n<")
491
+ .split("\n")
492
+ .forEach((originalLine) => {
493
+ const line = originalLine.trim();
494
+ if (!line) return;
495
+
496
+ // Decrease depth for closing tags
497
+ if (line.startsWith("</")) {
498
+ depth = Math.max(0, depth - 1);
499
+ }
317
500
 
318
- // Increase depth for opening tags (not self-closing)
319
- if (line.startsWith('<') && !line.startsWith('</') && !line.startsWith('<?') &&
320
- !line.startsWith('<!') && !line.endsWith('/>') && !line.includes('</')) {
321
- depth++;
322
- }
323
- });
501
+ formatted += indentStr.repeat(depth) + line + "\n";
502
+
503
+ // Increase depth for opening tags (not self-closing)
504
+ if (
505
+ line.startsWith("<") &&
506
+ !line.startsWith("</") &&
507
+ !line.startsWith("<?") &&
508
+ !line.startsWith("<!") &&
509
+ !line.endsWith("/>") &&
510
+ !line.includes("</")
511
+ ) {
512
+ depth++;
513
+ }
514
+ });
324
515
 
325
516
  return formatted.trim();
326
517
  }
327
518
 
519
+ /**
520
+ * Convert SVG content to data URI.
521
+ * @param {string} content - SVG content
522
+ * @param {string} format - Format: 'base64', 'enc', or 'unenc'
523
+ * @returns {string} Data URI string
524
+ */
328
525
  function toDataUri(content, format) {
329
- if (format === 'base64') {
330
- return 'data:image/svg+xml;base64,' + Buffer.from(content).toString('base64');
331
- } else if (format === 'enc') {
332
- return 'data:image/svg+xml,' + encodeURIComponent(content);
526
+ if (format === "base64") {
527
+ return (
528
+ "data:image/svg+xml;base64," + Buffer.from(content).toString("base64")
529
+ );
530
+ } else if (format === "enc") {
531
+ return "data:image/svg+xml," + encodeURIComponent(content);
333
532
  } else {
334
- return 'data:image/svg+xml,' + content;
533
+ return "data:image/svg+xml," + content;
335
534
  }
336
535
  }
337
536
 
338
537
  // ============================================================================
339
538
  // PROCESS FILES
340
539
  // ============================================================================
540
+ /**
541
+ * Process single SVG file with optimization.
542
+ * @param {string} inputPath - Input file path
543
+ * @param {string} outputPath - Output file path
544
+ * @param {Object} options - Processing options
545
+ * @returns {Promise<Object>} Processing result with success status and metrics
546
+ */
341
547
  async function processFile(inputPath, outputPath, options) {
342
548
  try {
343
- const content = readFileSync(inputPath, 'utf8');
549
+ let content = readFileSync(inputPath, "utf8");
344
550
  const originalSize = Buffer.byteLength(content);
345
551
 
552
+ // Apply embedding if enabled
553
+ if (options.embed) {
554
+ const doc = parseSVG(content);
555
+ const embedOptions = {
556
+ images: options.embedImages,
557
+ externalSVGs: options.embedExternalSVGs,
558
+ externalSVGMode: options.embedExternalSVGMode,
559
+ css: options.embedCSS,
560
+ fonts: options.embedFonts,
561
+ scripts: options.embedScripts,
562
+ audio: options.embedAudio,
563
+ subsetFonts: options.embedSubsetFonts,
564
+ recursive: options.embedRecursive,
565
+ maxRecursionDepth: options.embedMaxRecursionDepth,
566
+ timeout: options.embedTimeout,
567
+ onMissingResource: options.embedOnMissingResource,
568
+ baseDir: dirname(inputPath),
569
+ };
570
+
571
+ if (SVGToolbox.embedExternalDependencies) {
572
+ await SVGToolbox.embedExternalDependencies(doc, embedOptions);
573
+ content = serializeSVG(doc);
574
+ } else {
575
+ log(
576
+ `${colors.yellow}Warning:${colors.reset} embedExternalDependencies not available in svg-toolbox`,
577
+ );
578
+ }
579
+ }
580
+
346
581
  const optimized = await optimizeSvg(content, options);
347
582
  const optimizedSize = Buffer.byteLength(optimized);
348
583
 
@@ -351,17 +586,25 @@ async function processFile(inputPath, outputPath, options) {
351
586
  output = toDataUri(optimized, options.datauri);
352
587
  }
353
588
 
354
- if (outputPath === '-') {
589
+ if (outputPath === "-") {
355
590
  process.stdout.write(output);
356
591
  } else {
357
592
  ensureDir(dirname(outputPath));
358
- writeFileSync(outputPath, output, 'utf8');
593
+ writeFileSync(outputPath, output, "utf8");
359
594
  }
360
595
 
361
596
  const savings = originalSize - optimizedSize;
362
597
  const percent = ((savings / originalSize) * 100).toFixed(1);
363
598
 
364
- return { success: true, originalSize, optimizedSize, savings, percent, inputPath, outputPath };
599
+ return {
600
+ success: true,
601
+ originalSize,
602
+ optimizedSize,
603
+ savings,
604
+ percent,
605
+ inputPath,
606
+ outputPath,
607
+ };
365
608
  } catch (error) {
366
609
  return { success: false, error: error.message, inputPath };
367
610
  }
@@ -370,6 +613,10 @@ async function processFile(inputPath, outputPath, options) {
370
613
  // ============================================================================
371
614
  // HELP
372
615
  // ============================================================================
616
+ /**
617
+ * Display help message.
618
+ * @returns {void}
619
+ */
373
620
  function showHelp() {
374
621
  console.log(`Usage: svgm [options] [INPUT...]
375
622
 
@@ -404,37 +651,142 @@ Options:
404
651
  --show-plugins Show available plugins and exit
405
652
  --preserve-ns <NS,...> Preserve vendor namespaces (inkscape, sodipodi,
406
653
  illustrator, figma, etc.). Comma-separated.
654
+ --preserve-vendor Keep all vendor prefixes and editor namespaces
407
655
  --svg2-polyfills Inject JavaScript polyfills for SVG 2 features
408
656
  (mesh gradients, hatches) for browser support
657
+ --no-minify-polyfills Use full (non-minified) polyfills for debugging
409
658
  --no-color Output plain text without color
410
659
  -h, --help Display help for command
411
660
 
661
+ Embed Options:
662
+ --config <path> Load settings from YAML configuration file
663
+ --embed, --embed-all Enable all embedding options
664
+ --embed-images Embed external images as data URIs
665
+ --embed-external-svgs Embed external SVG files
666
+ --embed-svg-mode <mode> Mode for external SVGs: 'extract' or 'full'
667
+ --embed-css Embed external CSS files
668
+ --embed-fonts Embed external font files
669
+ --embed-scripts Embed external JavaScript files
670
+ --embed-audio Embed external audio files
671
+ --embed-subset-fonts Subset fonts to used glyphs only
672
+ --embed-recursive Recursively embed dependencies
673
+ --embed-max-depth <n> Maximum recursion depth (default: 10)
674
+ --embed-timeout <ms> Timeout for external resources (default: 30000)
675
+ --embed-on-missing <mode> Handle missing resources: 'warn', 'fail', 'skip'
676
+
412
677
  Examples:
413
678
  svgm input.svg -o output.svg
414
679
  svgm -f ./icons/ -o ./optimized/
415
680
  svgm input.svg --pretty --indent 4
416
681
  svgm -p 2 --multipass input.svg
682
+ svgm input.svg --embed-all -o output.svg
683
+ svgm input.svg --config svgm.yml -o output.svg
417
684
 
418
685
  Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
419
686
  }
420
687
 
688
+ /**
689
+ * Display version number.
690
+ * @returns {void}
691
+ */
421
692
  function showVersion() {
422
693
  console.log(VERSION);
423
694
  }
424
695
 
696
+ /**
697
+ * Display available optimization plugins.
698
+ * @returns {void}
699
+ */
425
700
  function showPlugins() {
426
- console.log('\nAvailable optimizations:\n');
701
+ console.log("\nAvailable optimizations:\n");
427
702
  for (const opt of OPTIMIZATIONS) {
428
- console.log(` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`);
703
+ console.log(
704
+ ` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`,
705
+ );
429
706
  }
430
707
  console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
431
708
  }
432
709
 
710
+ // ============================================================================
711
+ // CONFIG FILE LOADING
712
+ // ============================================================================
713
+ /**
714
+ * Load and parse configuration file (YAML).
715
+ * @param {string} configPath - Path to config file
716
+ * @returns {Object} Parsed configuration
717
+ */
718
+ function loadConfigFile(configPath) {
719
+ try {
720
+ const absolutePath = resolvePath(configPath);
721
+ if (!existsSync(absolutePath)) {
722
+ logError(`Config file not found: ${configPath}`);
723
+ process.exit(CONSTANTS.EXIT_ERROR);
724
+ }
725
+ const content = readFileSync(absolutePath, "utf8");
726
+ const loadedConfig = yaml.load(content);
727
+
728
+ // Convert YAML config structure to CLI config structure
729
+ const result = {};
730
+
731
+ if (loadedConfig.embed) {
732
+ const embedCfg = loadedConfig.embed;
733
+ if (embedCfg.images !== undefined) result.embedImages = embedCfg.images;
734
+ if (embedCfg.externalSVGs !== undefined)
735
+ result.embedExternalSVGs = embedCfg.externalSVGs;
736
+ if (embedCfg.externalSVGMode !== undefined)
737
+ result.embedExternalSVGMode = embedCfg.externalSVGMode;
738
+ if (embedCfg.css !== undefined) result.embedCSS = embedCfg.css;
739
+ if (embedCfg.fonts !== undefined) result.embedFonts = embedCfg.fonts;
740
+ if (embedCfg.scripts !== undefined)
741
+ result.embedScripts = embedCfg.scripts;
742
+ if (embedCfg.audio !== undefined) result.embedAudio = embedCfg.audio;
743
+ if (embedCfg.subsetFonts !== undefined)
744
+ result.embedSubsetFonts = embedCfg.subsetFonts;
745
+ if (embedCfg.recursive !== undefined)
746
+ result.embedRecursive = embedCfg.recursive;
747
+ if (embedCfg.maxRecursionDepth !== undefined)
748
+ result.embedMaxRecursionDepth = embedCfg.maxRecursionDepth;
749
+ if (embedCfg.timeout !== undefined)
750
+ result.embedTimeout = embedCfg.timeout;
751
+ if (embedCfg.onMissingResource !== undefined)
752
+ result.embedOnMissingResource = embedCfg.onMissingResource;
753
+
754
+ // If any embed option is enabled, set embed flag
755
+ if (
756
+ Object.keys(result).some(
757
+ (key) => key.startsWith("embed") && result[key] === true,
758
+ )
759
+ ) {
760
+ result.embed = true;
761
+ }
762
+ }
763
+
764
+ // Support other config options if present
765
+ if (loadedConfig.precision !== undefined)
766
+ result.precision = loadedConfig.precision;
767
+ if (loadedConfig.multipass !== undefined)
768
+ result.multipass = loadedConfig.multipass;
769
+ if (loadedConfig.pretty !== undefined) result.pretty = loadedConfig.pretty;
770
+ if (loadedConfig.indent !== undefined) result.indent = loadedConfig.indent;
771
+ if (loadedConfig.quiet !== undefined) result.quiet = loadedConfig.quiet;
772
+
773
+ return result;
774
+ } catch (error) {
775
+ logError(`Failed to load config file: ${error.message}`);
776
+ process.exit(CONSTANTS.EXIT_ERROR);
777
+ }
778
+ }
779
+
433
780
  // ============================================================================
434
781
  // ARGUMENT PARSING
435
782
  // ============================================================================
783
+ /**
784
+ * Parse command-line arguments.
785
+ * @param {string[]} args - Command-line arguments
786
+ * @returns {Object} Parsed configuration object
787
+ */
436
788
  function parseArgs(args) {
437
- const cfg = { ...DEFAULT_CONFIG };
789
+ let cfg = { ...DEFAULT_CONFIG };
438
790
  const inputs = [];
439
791
  let i = 0;
440
792
 
@@ -443,127 +795,219 @@ function parseArgs(args) {
443
795
  let argValue = null;
444
796
 
445
797
  // Handle --arg=value format
446
- if (arg.includes('=') && arg.startsWith('--')) {
447
- const eqIdx = arg.indexOf('=');
798
+ if (arg.includes("=") && arg.startsWith("--")) {
799
+ const eqIdx = arg.indexOf("=");
448
800
  argValue = arg.substring(eqIdx + 1);
449
801
  arg = arg.substring(0, eqIdx);
450
802
  }
451
803
 
452
804
  switch (arg) {
453
- case '-v':
454
- case '--version':
805
+ case "-v":
806
+ case "--version":
455
807
  showVersion();
456
808
  process.exit(CONSTANTS.EXIT_SUCCESS);
457
809
  break;
458
810
 
459
- case '-h':
460
- case '--help':
811
+ case "-h":
812
+ case "--help":
461
813
  showHelp();
462
814
  process.exit(CONSTANTS.EXIT_SUCCESS);
463
815
  break;
464
816
 
465
- case '-i':
466
- case '--input':
817
+ case "-i":
818
+ case "--input":
467
819
  i++;
468
- while (i < args.length && !args[i].startsWith('-')) {
820
+ while (i < args.length && !args[i].startsWith("-")) {
469
821
  inputs.push(args[i]);
470
822
  i++;
471
823
  }
472
824
  i--; // Back up one since the while loop went past
473
825
  break;
474
826
 
475
- case '-s':
476
- case '--string':
827
+ case "-s":
828
+ case "--string":
477
829
  cfg.string = args[++i];
478
830
  break;
479
831
 
480
- case '-f':
481
- case '--folder':
832
+ case "-f":
833
+ case "--folder":
482
834
  cfg.folder = args[++i];
483
835
  break;
484
836
 
485
- case '-o':
486
- case '--output':
837
+ case "-o":
838
+ case "--output": {
487
839
  i++;
488
840
  // Collect output(s)
489
841
  const outputs = [];
490
- while (i < args.length && !args[i].startsWith('-')) {
842
+ while (i < args.length && !args[i].startsWith("-")) {
491
843
  outputs.push(args[i]);
492
844
  i++;
493
845
  }
494
846
  i--;
495
847
  cfg.output = outputs.length === 1 ? outputs[0] : outputs;
496
848
  break;
849
+ }
497
850
 
498
- case '-p':
499
- case '--precision':
851
+ case "-p":
852
+ case "--precision":
500
853
  cfg.precision = parseInt(args[++i], 10);
501
854
  break;
502
855
 
503
- case '--datauri':
856
+ case "--datauri":
504
857
  cfg.datauri = args[++i];
505
858
  break;
506
859
 
507
- case '--multipass':
860
+ case "--multipass":
508
861
  cfg.multipass = true;
509
862
  break;
510
863
 
511
- case '--pretty':
864
+ case "--pretty":
512
865
  cfg.pretty = true;
513
866
  break;
514
867
 
515
- case '--indent':
868
+ case "--indent":
516
869
  cfg.indent = parseInt(args[++i], 10);
517
870
  break;
518
871
 
519
- case '--eol':
872
+ case "--eol":
520
873
  cfg.eol = args[++i];
521
874
  break;
522
875
 
523
- case '--final-newline':
876
+ case "--final-newline":
524
877
  cfg.finalNewline = true;
525
878
  break;
526
879
 
527
- case '-r':
528
- case '--recursive':
880
+ case "-r":
881
+ case "--recursive":
529
882
  cfg.recursive = true;
530
883
  break;
531
884
 
532
- case '--exclude':
885
+ case "--exclude":
533
886
  i++;
534
- while (i < args.length && !args[i].startsWith('-')) {
887
+ while (i < args.length && !args[i].startsWith("-")) {
535
888
  cfg.exclude.push(args[i]);
536
889
  i++;
537
890
  }
538
891
  i--;
539
892
  break;
540
893
 
541
- case '-q':
542
- case '--quiet':
894
+ case "-q":
895
+ case "--quiet":
543
896
  cfg.quiet = true;
544
897
  break;
545
898
 
546
- case '--show-plugins':
899
+ case "--show-plugins":
547
900
  cfg.showPlugins = true;
548
901
  break;
549
902
 
550
- case '--preserve-ns':
903
+ case "--preserve-ns":
551
904
  {
552
905
  const val = argValue || args[++i];
553
- cfg.preserveNamespaces = val.split(',').map(s => s.trim().toLowerCase());
906
+ if (!val) {
907
+ logError(
908
+ "--preserve-ns requires a comma-separated list of namespaces",
909
+ );
910
+ process.exit(1);
911
+ }
912
+ cfg.preserveNamespaces = val
913
+ .split(",")
914
+ .map((s) => s.trim().toLowerCase())
915
+ .filter((s) => s.length > 0);
554
916
  }
555
917
  break;
556
918
 
557
- case '--svg2-polyfills':
919
+ case "--preserve-vendor":
920
+ cfg.preserveVendor = true;
921
+ break;
922
+
923
+ case "--svg2-polyfills":
558
924
  cfg.svg2Polyfills = true;
559
925
  break;
560
926
 
561
- case '--no-color':
927
+ case "--no-minify-polyfills":
928
+ cfg.noMinifyPolyfills = true;
929
+ setPolyfillMinification(false);
930
+ break;
931
+
932
+ case "--no-color":
562
933
  // Already handled in colors initialization
563
934
  break;
564
935
 
936
+ case "--config":
937
+ cfg.configFile = args[++i];
938
+ break;
939
+
940
+ case "--embed":
941
+ case "--embed-all":
942
+ cfg.embed = true;
943
+ cfg.embedImages = true;
944
+ cfg.embedExternalSVGs = true;
945
+ cfg.embedCSS = true;
946
+ cfg.embedFonts = true;
947
+ cfg.embedScripts = true;
948
+ cfg.embedAudio = true;
949
+ cfg.embedSubsetFonts = true;
950
+ cfg.embedRecursive = true;
951
+ break;
952
+
953
+ case "--embed-images":
954
+ cfg.embed = true;
955
+ cfg.embedImages = true;
956
+ break;
957
+
958
+ case "--embed-external-svgs":
959
+ cfg.embed = true;
960
+ cfg.embedExternalSVGs = true;
961
+ break;
962
+
963
+ case "--embed-svg-mode":
964
+ cfg.embedExternalSVGMode = args[++i];
965
+ break;
966
+
967
+ case "--embed-css":
968
+ cfg.embed = true;
969
+ cfg.embedCSS = true;
970
+ break;
971
+
972
+ case "--embed-fonts":
973
+ cfg.embed = true;
974
+ cfg.embedFonts = true;
975
+ break;
976
+
977
+ case "--embed-scripts":
978
+ cfg.embed = true;
979
+ cfg.embedScripts = true;
980
+ break;
981
+
982
+ case "--embed-audio":
983
+ cfg.embed = true;
984
+ cfg.embedAudio = true;
985
+ break;
986
+
987
+ case "--embed-subset-fonts":
988
+ cfg.embed = true;
989
+ cfg.embedSubsetFonts = true;
990
+ break;
991
+
992
+ case "--embed-recursive":
993
+ cfg.embed = true;
994
+ cfg.embedRecursive = true;
995
+ break;
996
+
997
+ case "--embed-max-depth":
998
+ cfg.embedMaxRecursionDepth = parseInt(args[++i], 10);
999
+ break;
1000
+
1001
+ case "--embed-timeout":
1002
+ cfg.embedTimeout = parseInt(args[++i], 10);
1003
+ break;
1004
+
1005
+ case "--embed-on-missing":
1006
+ cfg.embedOnMissingResource = args[++i];
1007
+ break;
1008
+
565
1009
  default:
566
- if (arg.startsWith('-')) {
1010
+ if (arg.startsWith("-")) {
567
1011
  logError(`Unknown option: ${arg}`);
568
1012
  process.exit(CONSTANTS.EXIT_ERROR);
569
1013
  }
@@ -573,12 +1017,87 @@ function parseArgs(args) {
573
1017
  }
574
1018
 
575
1019
  cfg.inputs = inputs;
1020
+
1021
+ // Validate numeric arguments
1022
+ if (
1023
+ cfg.precision !== undefined &&
1024
+ (isNaN(cfg.precision) || cfg.precision < 0 || cfg.precision > 20)
1025
+ ) {
1026
+ logError("--precision must be a number between 0 and 20");
1027
+ process.exit(CONSTANTS.EXIT_ERROR);
1028
+ }
1029
+ if (
1030
+ cfg.indent !== undefined &&
1031
+ (isNaN(cfg.indent) || cfg.indent < 0 || cfg.indent > 16)
1032
+ ) {
1033
+ logError("--indent must be a number between 0 and 16");
1034
+ process.exit(CONSTANTS.EXIT_ERROR);
1035
+ }
1036
+ if (
1037
+ cfg.embedMaxRecursionDepth !== undefined &&
1038
+ (isNaN(cfg.embedMaxRecursionDepth) ||
1039
+ cfg.embedMaxRecursionDepth < 1 ||
1040
+ cfg.embedMaxRecursionDepth > 100)
1041
+ ) {
1042
+ logError("--embed-max-depth must be a number between 1 and 100");
1043
+ process.exit(CONSTANTS.EXIT_ERROR);
1044
+ }
1045
+ if (
1046
+ cfg.embedTimeout !== undefined &&
1047
+ (isNaN(cfg.embedTimeout) ||
1048
+ cfg.embedTimeout < 1000 ||
1049
+ cfg.embedTimeout > 300000)
1050
+ ) {
1051
+ logError("--embed-timeout must be a number between 1000 and 300000 (ms)");
1052
+ process.exit(CONSTANTS.EXIT_ERROR);
1053
+ }
1054
+
1055
+ // Validate enum arguments (only check if explicitly set, not null/undefined defaults)
1056
+ const validEol = ["lf", "crlf"];
1057
+ if (cfg.eol != null && !validEol.includes(cfg.eol)) {
1058
+ logError(`--eol must be one of: ${validEol.join(", ")}`);
1059
+ process.exit(CONSTANTS.EXIT_ERROR);
1060
+ }
1061
+ const validDatauri = ["base64", "enc", "unenc"];
1062
+ if (cfg.datauri != null && !validDatauri.includes(cfg.datauri)) {
1063
+ logError(`--datauri must be one of: ${validDatauri.join(", ")}`);
1064
+ process.exit(CONSTANTS.EXIT_ERROR);
1065
+ }
1066
+ const validSvgMode = ["extract", "full"];
1067
+ if (
1068
+ cfg.embedExternalSVGMode != null &&
1069
+ cfg.embedExternalSVGMode !== "extract" &&
1070
+ !validSvgMode.includes(cfg.embedExternalSVGMode)
1071
+ ) {
1072
+ logError(`--embed-svg-mode must be one of: ${validSvgMode.join(", ")}`);
1073
+ process.exit(CONSTANTS.EXIT_ERROR);
1074
+ }
1075
+ const validOnMissing = ["warn", "fail", "skip"];
1076
+ if (
1077
+ cfg.embedOnMissingResource != null &&
1078
+ !validOnMissing.includes(cfg.embedOnMissingResource)
1079
+ ) {
1080
+ logError(`--embed-on-missing must be one of: ${validOnMissing.join(", ")}`);
1081
+ process.exit(CONSTANTS.EXIT_ERROR);
1082
+ }
1083
+
1084
+ // Load config file if specified and merge with CLI options (CLI takes precedence)
1085
+ if (cfg.configFile) {
1086
+ const fileConfig = loadConfigFile(cfg.configFile);
1087
+ // Merge: file config first, then CLI overrides
1088
+ cfg = { ...DEFAULT_CONFIG, ...fileConfig, ...cfg };
1089
+ }
1090
+
576
1091
  return cfg;
577
1092
  }
578
1093
 
579
1094
  // ============================================================================
580
1095
  // MAIN
581
1096
  // ============================================================================
1097
+ /**
1098
+ * Main entry point for CLI.
1099
+ * @returns {Promise<void>}
1100
+ */
582
1101
  async function main() {
583
1102
  const args = process.argv.slice(2);
584
1103
 
@@ -604,15 +1123,32 @@ async function main() {
604
1123
  datauri: config.datauri,
605
1124
  preserveNamespaces: config.preserveNamespaces,
606
1125
  svg2Polyfills: config.svg2Polyfills,
1126
+ noMinifyPolyfills: config.noMinifyPolyfills, // Pass through for pipeline consistency
1127
+ // Embed options
1128
+ embed: config.embed,
1129
+ embedImages: config.embedImages,
1130
+ embedExternalSVGs: config.embedExternalSVGs,
1131
+ embedExternalSVGMode: config.embedExternalSVGMode,
1132
+ embedCSS: config.embedCSS,
1133
+ embedFonts: config.embedFonts,
1134
+ embedScripts: config.embedScripts,
1135
+ embedAudio: config.embedAudio,
1136
+ embedSubsetFonts: config.embedSubsetFonts,
1137
+ embedRecursive: config.embedRecursive,
1138
+ embedMaxRecursionDepth: config.embedMaxRecursionDepth,
1139
+ embedTimeout: config.embedTimeout,
1140
+ embedOnMissingResource: config.embedOnMissingResource,
607
1141
  };
608
1142
 
609
1143
  // Handle string input
610
1144
  if (config.string) {
611
1145
  try {
612
1146
  const result = await optimizeSvg(config.string, options);
613
- const output = config.datauri ? toDataUri(result, config.datauri) : result;
614
- if (config.output && config.output !== '-') {
615
- writeFileSync(config.output, output, 'utf8');
1147
+ const output = config.datauri
1148
+ ? toDataUri(result, config.datauri)
1149
+ : result;
1150
+ if (config.output && config.output !== "-") {
1151
+ writeFileSync(config.output, output, "utf8");
616
1152
  log(`${colors.green}Done!${colors.reset}`);
617
1153
  } else {
618
1154
  process.stdout.write(output);
@@ -638,9 +1174,9 @@ async function main() {
638
1174
 
639
1175
  // Add explicit inputs
640
1176
  for (const input of config.inputs) {
641
- if (input === '-') {
1177
+ if (input === "-") {
642
1178
  // STDIN handling would go here
643
- logError('STDIN not yet supported');
1179
+ logError("STDIN not yet supported");
644
1180
  process.exit(CONSTANTS.EXIT_ERROR);
645
1181
  }
646
1182
  const resolved = resolvePath(input);
@@ -655,7 +1191,7 @@ async function main() {
655
1191
  }
656
1192
 
657
1193
  if (files.length === 0) {
658
- logError('No input files');
1194
+ logError("No input files");
659
1195
  process.exit(CONSTANTS.EXIT_ERROR);
660
1196
  }
661
1197
 
@@ -670,8 +1206,8 @@ async function main() {
670
1206
  let outputPath;
671
1207
 
672
1208
  if (config.output) {
673
- if (config.output === '-') {
674
- outputPath = '-';
1209
+ if (config.output === "-") {
1210
+ outputPath = "-";
675
1211
  } else if (Array.isArray(config.output)) {
676
1212
  outputPath = config.output[i] || config.output[0];
677
1213
  } else if (files.length > 1 || isDir(resolvePath(config.output))) {
@@ -692,8 +1228,10 @@ async function main() {
692
1228
  totalOriginal += result.originalSize;
693
1229
  totalOptimized += result.optimizedSize;
694
1230
 
695
- if (outputPath !== '-') {
696
- log(`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`);
1231
+ if (outputPath !== "-") {
1232
+ log(
1233
+ `${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`,
1234
+ );
697
1235
  }
698
1236
  } else {
699
1237
  errorCount++;
@@ -704,9 +1242,14 @@ async function main() {
704
1242
  // Summary
705
1243
  if (files.length > 1 && !config.quiet) {
706
1244
  const totalSavings = totalOriginal - totalOptimized;
707
- const totalPercent = totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
708
- console.log(`\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`);
709
- console.log(`${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`);
1245
+ const totalPercent =
1246
+ totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
1247
+ console.log(
1248
+ `\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`,
1249
+ );
1250
+ console.log(
1251
+ `${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`,
1252
+ );
710
1253
  }
711
1254
 
712
1255
  if (errorCount > 0) {