@emasoft/svg-matrix 1.1.0 → 1.2.0

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 (55) hide show
  1. package/bin/svg-matrix.js +7 -6
  2. package/bin/svgm.js +109 -40
  3. package/dist/svg-matrix.min.js +7 -7
  4. package/dist/svg-toolbox.min.js +148 -228
  5. package/dist/svgm.min.js +152 -232
  6. package/dist/version.json +5 -5
  7. package/package.json +1 -1
  8. package/scripts/postinstall.js +72 -41
  9. package/scripts/test-postinstall.js +18 -16
  10. package/scripts/version-sync.js +78 -60
  11. package/src/animation-optimization.js +190 -98
  12. package/src/animation-references.js +11 -3
  13. package/src/arc-length.js +23 -20
  14. package/src/bezier-analysis.js +9 -13
  15. package/src/bezier-intersections.js +18 -4
  16. package/src/browser-verify.js +35 -8
  17. package/src/clip-path-resolver.js +285 -114
  18. package/src/convert-path-data.js +20 -8
  19. package/src/css-specificity.js +33 -9
  20. package/src/douglas-peucker.js +272 -141
  21. package/src/geometry-to-path.js +79 -22
  22. package/src/gjk-collision.js +287 -126
  23. package/src/index.js +56 -21
  24. package/src/inkscape-support.js +122 -101
  25. package/src/logger.js +43 -27
  26. package/src/marker-resolver.js +201 -121
  27. package/src/mask-resolver.js +231 -98
  28. package/src/matrix.js +9 -5
  29. package/src/mesh-gradient.js +22 -14
  30. package/src/off-canvas-detection.js +53 -17
  31. package/src/path-optimization.js +356 -171
  32. package/src/path-simplification.js +671 -256
  33. package/src/pattern-resolver.js +1 -3
  34. package/src/polygon-clip.js +396 -78
  35. package/src/svg-boolean-ops.js +90 -23
  36. package/src/svg-collections.js +1546 -667
  37. package/src/svg-flatten.js +152 -38
  38. package/src/svg-matrix-lib.js +2 -2
  39. package/src/svg-parser.js +5 -1
  40. package/src/svg-rendering-context.js +3 -1
  41. package/src/svg-toolbox-lib.js +2 -2
  42. package/src/svg-toolbox.js +99 -457
  43. package/src/svg-validation-data.js +513 -345
  44. package/src/svg2-polyfills.js +156 -93
  45. package/src/svgm-lib.js +8 -4
  46. package/src/transform-optimization.js +168 -51
  47. package/src/transforms2d.js +73 -40
  48. package/src/transforms3d.js +34 -27
  49. package/src/use-symbol-resolver.js +175 -76
  50. package/src/vector.js +80 -44
  51. package/src/vendor/inkscape-hatch-polyfill.js +143 -108
  52. package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
  53. package/src/vendor/inkscape-mesh-polyfill.js +953 -766
  54. package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
  55. package/src/verification.js +3 -4
package/bin/svg-matrix.js CHANGED
@@ -603,6 +603,7 @@ function parseFileList(listPath) {
603
603
  if (isFile(resolved)) files.push(resolved);
604
604
  else if (isDir(resolved))
605
605
  files.push(...getSvgFiles(resolved, config.recursive));
606
+ // NOTE: Intentional batch mode behavior - warn but continue processing remaining files
606
607
  else logWarn(`File not found: ${trimmed}`);
607
608
  } catch (e) {
608
609
  logWarn(`Invalid path in list: ${trimmed} - ${e.message}`);
@@ -893,7 +894,7 @@ ${boxLine("", W)}
893
894
  ${boxLine(` ${colors.dim}-o, --output <path>${colors.reset} Output file or directory`, W)}
894
895
  ${boxLine(` ${colors.dim}-l, --list <file>${colors.reset} Read input files from text file`, W)}
895
896
  ${boxLine(` ${colors.dim}-r, --recursive${colors.reset} Process directories recursively`, W)}
896
- ${boxLine(` ${colors.dim}-p, --precision <n>${colors.reset} Decimal precision (default: 6, max: 50)`, W)}
897
+ ${boxLine(` ${colors.dim}-p, --precision <n>${colors.reset} Decimal precision (default: 6, min: 1, max: 50)`, W)}
897
898
  ${boxLine(` ${colors.dim}-f, --force${colors.reset} Overwrite existing output files`, W)}
898
899
  ${boxLine(` ${colors.dim}-n, --dry-run${colors.reset} Show what would be done`, W)}
899
900
  ${boxLine(` ${colors.dim}-q, --quiet${colors.reset} Suppress all output except errors`, W)}
@@ -2176,9 +2177,7 @@ function parseArgs(args) {
2176
2177
  .filter((ns) => ns.length > 0);
2177
2178
  // Why: Reject empty array after filtering whitespace-only entries
2178
2179
  if (cfg.preserveNamespaces.length === 0) {
2179
- logError(
2180
- "--preserve-ns list is empty after filtering whitespace",
2181
- );
2180
+ logError("--preserve-ns list is empty after filtering whitespace");
2182
2181
  process.exit(CONSTANTS.EXIT_ERROR);
2183
2182
  }
2184
2183
  break;
@@ -2230,8 +2229,10 @@ function parseArgs(args) {
2230
2229
  logError("--e2e-tolerance requires a value");
2231
2230
  process.exit(CONSTANTS.EXIT_ERROR);
2232
2231
  }
2233
- if (!/^1e-\d+$/.test(value)) {
2234
- logError("e2e-tolerance must be in format 1e-N (e.g., 1e-10, 1e-12)");
2232
+ if (!/^[\d.]+e[+-]?\d+$/i.test(value)) {
2233
+ logError(
2234
+ "e2e-tolerance must be in scientific notation (e.g., 1e-10, 0.5e-12, 2e-8)",
2235
+ );
2235
2236
  process.exit(CONSTANTS.EXIT_ERROR);
2236
2237
  }
2237
2238
  cfg.e2eTolerance = value;
package/bin/svgm.js CHANGED
@@ -137,12 +137,30 @@ function log(msg) {
137
137
  function logError(msg) {
138
138
  // Why: Validate parameter to prevent runtime errors with null/undefined
139
139
  if (typeof msg !== "string") {
140
- console.error(`${colors.red}error:${colors.reset} Invalid error message type: ${typeof msg}`);
140
+ console.error(
141
+ `${colors.red}error:${colors.reset} Invalid error message type: ${typeof msg}`,
142
+ );
141
143
  return;
142
144
  }
143
145
  console.error(`${colors.red}error:${colors.reset} ${msg}`);
144
146
  }
145
147
 
148
+ /**
149
+ * Log warning message to console.
150
+ * @param {string} msg - Warning message
151
+ * @returns {void}
152
+ */
153
+ function logWarn(msg) {
154
+ // Why: Validate parameter to prevent runtime errors with null/undefined
155
+ if (typeof msg !== "string") {
156
+ console.warn(
157
+ `${colors.yellow}warn:${colors.reset} Invalid warning message type: ${typeof msg}`,
158
+ );
159
+ return;
160
+ }
161
+ console.warn(`${colors.yellow}warn:${colors.reset} ${msg}`);
162
+ }
163
+
146
164
  // ============================================================================
147
165
  // AVAILABLE OPTIMIZATIONS (matching SVGO plugins)
148
166
  // ============================================================================
@@ -381,7 +399,9 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
381
399
  throw new Error(`getSvgFiles: directory does not exist: ${dir}`);
382
400
  }
383
401
  if (!Array.isArray(exclude)) {
384
- throw new TypeError(`getSvgFiles: expected array exclude, got ${typeof exclude}`);
402
+ throw new TypeError(
403
+ `getSvgFiles: expected array exclude, got ${typeof exclude}`,
404
+ );
385
405
  }
386
406
 
387
407
  // Why: Validate and compile regex patterns once before scanning to fail fast
@@ -418,8 +438,8 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
418
438
  const fullPath = join(d, entry.name);
419
439
 
420
440
  // Check exclusion patterns using pre-compiled regexes
421
- const shouldExclude = excludeRegexes.some((regex) =>
422
- regex.test(fullPath) || regex.test(entry.name)
441
+ const shouldExclude = excludeRegexes.some(
442
+ (regex) => regex.test(fullPath) || regex.test(entry.name),
423
443
  );
424
444
  if (shouldExclude) continue;
425
445
 
@@ -451,13 +471,17 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
451
471
  async function optimizeSvg(content, options = {}) {
452
472
  // Why: Validate parameters to prevent runtime errors
453
473
  if (typeof content !== "string") {
454
- throw new TypeError(`optimizeSvg: expected string content, got ${typeof content}`);
474
+ throw new TypeError(
475
+ `optimizeSvg: expected string content, got ${typeof content}`,
476
+ );
455
477
  }
456
478
  if (content.length === 0) {
457
479
  throw new Error("optimizeSvg: content is empty");
458
480
  }
459
481
  if (options !== null && typeof options !== "object") {
460
- throw new TypeError(`optimizeSvg: expected object options, got ${typeof options}`);
482
+ throw new TypeError(
483
+ `optimizeSvg: expected object options, got ${typeof options}`,
484
+ );
461
485
  }
462
486
  const doc = parseSVG(content);
463
487
  const pipeline = DEFAULT_PIPELINE;
@@ -478,8 +502,10 @@ async function optimizeSvg(content, options = {}) {
478
502
  precision: options.precision,
479
503
  preserveNamespaces: options.preserveNamespaces,
480
504
  });
481
- } catch {
482
- // Skip failed optimizations silently
505
+ } catch (err) {
506
+ // Batch processing: log and continue so one plugin failure doesn't abort the pipeline.
507
+ // This is intentional - optimization plugins are non-critical and should not halt processing.
508
+ logWarn(`Plugin "${pluginName}" failed: ${err.message}`);
483
509
  }
484
510
  }
485
511
  }
@@ -494,8 +520,10 @@ async function optimizeSvg(content, options = {}) {
494
520
  precision: options.precision,
495
521
  preserveNamespaces: options.preserveNamespaces,
496
522
  });
497
- } catch {
498
- // Skip failed optimizations silently
523
+ } catch (err) {
524
+ // Batch processing: log and continue so one plugin failure doesn't abort the pipeline.
525
+ // This is intentional - optimization plugins are non-critical and should not halt processing.
526
+ logWarn(`Plugin "${pluginName}" failed: ${err.message}`);
499
527
  }
500
528
  }
501
529
  }
@@ -567,7 +595,9 @@ function prettifyXml(xml, indent = 2) {
567
595
  throw new TypeError(`prettifyXml: expected string xml, got ${typeof xml}`);
568
596
  }
569
597
  if (typeof indent !== "number" || indent < 0 || indent > 16) {
570
- throw new RangeError(`prettifyXml: indent must be number 0-16, got ${indent}`);
598
+ throw new RangeError(
599
+ `prettifyXml: indent must be number 0-16, got ${indent}`,
600
+ );
571
601
  }
572
602
  // Simple XML prettifier
573
603
  const indentStr = " ".repeat(indent);
@@ -614,11 +644,15 @@ function prettifyXml(xml, indent = 2) {
614
644
  function toDataUri(content, format) {
615
645
  // Why: Validate parameters to prevent runtime errors
616
646
  if (typeof content !== "string") {
617
- throw new TypeError(`toDataUri: expected string content, got ${typeof content}`);
647
+ throw new TypeError(
648
+ `toDataUri: expected string content, got ${typeof content}`,
649
+ );
618
650
  }
619
651
  const validFormats = ["base64", "enc", "unenc"];
620
652
  if (!validFormats.includes(format)) {
621
- throw new Error(`toDataUri: format must be one of ${validFormats.join(", ")}, got ${format}`);
653
+ throw new Error(
654
+ `toDataUri: format must be one of ${validFormats.join(", ")}, got ${format}`,
655
+ );
622
656
  }
623
657
  if (format === "base64") {
624
658
  return (
@@ -644,13 +678,25 @@ function toDataUri(content, format) {
644
678
  async function processFile(inputPath, outputPath, options) {
645
679
  // Why: Validate parameters to prevent runtime errors
646
680
  if (typeof inputPath !== "string") {
647
- return { success: false, error: `Invalid input path: ${typeof inputPath}`, inputPath };
681
+ return {
682
+ success: false,
683
+ error: `Invalid input path: ${typeof inputPath}`,
684
+ inputPath,
685
+ };
648
686
  }
649
687
  if (typeof outputPath !== "string") {
650
- return { success: false, error: `Invalid output path: ${typeof outputPath}`, inputPath };
688
+ return {
689
+ success: false,
690
+ error: `Invalid output path: ${typeof outputPath}`,
691
+ inputPath,
692
+ };
651
693
  }
652
694
  if (options !== null && typeof options !== "object") {
653
- return { success: false, error: `Invalid options: ${typeof options}`, inputPath };
695
+ return {
696
+ success: false,
697
+ error: `Invalid options: ${typeof options}`,
698
+ inputPath,
699
+ };
654
700
  }
655
701
 
656
702
  try {
@@ -662,7 +708,7 @@ async function processFile(inputPath, outputPath, options) {
662
708
  return {
663
709
  success: false,
664
710
  error: `File too large: ${originalSize} bytes (max ${CONSTANTS.MAX_FILE_SIZE_BYTES} bytes)`,
665
- inputPath
711
+ inputPath,
666
712
  };
667
713
  }
668
714
 
@@ -716,9 +762,8 @@ async function processFile(inputPath, outputPath, options) {
716
762
 
717
763
  const savings = originalSize - optimizedSize;
718
764
  // Why: Handle case where optimizedSize > originalSize (negative savings)
719
- const percent = originalSize > 0
720
- ? ((savings / originalSize) * 100).toFixed(1)
721
- : "0.0";
765
+ const percent =
766
+ originalSize > 0 ? ((savings / originalSize) * 100).toFixed(1) : "0.0";
722
767
 
723
768
  return {
724
769
  success: true,
@@ -752,7 +797,7 @@ Arguments:
752
797
 
753
798
  Options:
754
799
  -v, --version Output the version number
755
- -i, --input <INPUT...> Input files, "-" for STDIN
800
+ -i, --input <INPUT...> Input files
756
801
  -s, --string <STRING> Input SVG data string
757
802
  -f, --folder <FOLDER> Input folder, optimize and rewrite all *.svg files
758
803
  -o, --output <OUTPUT...> Output file or folder (by default same as input),
@@ -857,7 +902,9 @@ function loadConfigFile(configPath) {
857
902
 
858
903
  // Why: Validate loaded config is an object to prevent runtime errors
859
904
  if (loadedConfig === null || typeof loadedConfig !== "object") {
860
- logError(`Invalid config file: expected object, got ${typeof loadedConfig}`);
905
+ logError(
906
+ `Invalid config file: expected object, got ${typeof loadedConfig}`,
907
+ );
861
908
  process.exit(CONSTANTS.EXIT_ERROR);
862
909
  }
863
910
 
@@ -1045,7 +1092,9 @@ function parseArgs(args) {
1045
1092
  const parsed = parseInt(precisionArg, 10);
1046
1093
  // Why: Validate parseInt result to prevent NaN and negative values
1047
1094
  if (isNaN(parsed) || parsed < 0) {
1048
- logError(`--precision requires a non-negative number, got: ${precisionArg}`);
1095
+ logError(
1096
+ `--precision requires a non-negative number, got: ${precisionArg}`,
1097
+ );
1049
1098
  process.exit(CONSTANTS.EXIT_ERROR);
1050
1099
  }
1051
1100
  cfg.precision = parsed;
@@ -1074,7 +1123,9 @@ function parseArgs(args) {
1074
1123
  const parsed = parseInt(indentArg, 10);
1075
1124
  // Why: Validate parseInt result to prevent NaN and negative values
1076
1125
  if (isNaN(parsed) || parsed < 0) {
1077
- logError(`--indent requires a non-negative number, got: ${indentArg}`);
1126
+ logError(
1127
+ `--indent requires a non-negative number, got: ${indentArg}`,
1128
+ );
1078
1129
  process.exit(CONSTANTS.EXIT_ERROR);
1079
1130
  }
1080
1131
  cfg.indent = parsed;
@@ -1236,7 +1287,9 @@ function parseArgs(args) {
1236
1287
  const parsed = parseInt(depthArg, 10);
1237
1288
  // Why: Validate parseInt result to prevent NaN and negative/zero values
1238
1289
  if (isNaN(parsed) || parsed < 1) {
1239
- logError(`--embed-max-depth requires a positive number, got: ${depthArg}`);
1290
+ logError(
1291
+ `--embed-max-depth requires a positive number, got: ${depthArg}`,
1292
+ );
1240
1293
  process.exit(CONSTANTS.EXIT_ERROR);
1241
1294
  }
1242
1295
  cfg.embedMaxRecursionDepth = parsed;
@@ -1251,7 +1304,9 @@ function parseArgs(args) {
1251
1304
  const parsed = parseInt(timeoutArg, 10);
1252
1305
  // Why: Validate parseInt result to prevent NaN and negative/zero values
1253
1306
  if (isNaN(parsed) || parsed < 1000) {
1254
- logError(`--embed-timeout requires a number >= 1000 (ms), got: ${timeoutArg}`);
1307
+ logError(
1308
+ `--embed-timeout requires a number >= 1000 (ms), got: ${timeoutArg}`,
1309
+ );
1255
1310
  process.exit(CONSTANTS.EXIT_ERROR);
1256
1311
  }
1257
1312
  cfg.embedTimeout = parsed;
@@ -1280,23 +1335,34 @@ function parseArgs(args) {
1280
1335
  // Validate numeric arguments
1281
1336
  // Why: Check type first before using isNaN to prevent incorrect validation
1282
1337
  if (
1283
- cfg.precision !== undefined && cfg.precision !== null &&
1284
- (typeof cfg.precision !== "number" || isNaN(cfg.precision) ||
1285
- cfg.precision < CONSTANTS.MIN_PRECISION || cfg.precision > CONSTANTS.MAX_PRECISION)
1338
+ cfg.precision !== undefined &&
1339
+ cfg.precision !== null &&
1340
+ (typeof cfg.precision !== "number" ||
1341
+ isNaN(cfg.precision) ||
1342
+ cfg.precision < CONSTANTS.MIN_PRECISION ||
1343
+ cfg.precision > CONSTANTS.MAX_PRECISION)
1286
1344
  ) {
1287
- logError(`--precision must be a number between ${CONSTANTS.MIN_PRECISION} and ${CONSTANTS.MAX_PRECISION}`);
1345
+ logError(
1346
+ `--precision must be a number between ${CONSTANTS.MIN_PRECISION} and ${CONSTANTS.MAX_PRECISION}`,
1347
+ );
1288
1348
  process.exit(CONSTANTS.EXIT_ERROR);
1289
1349
  }
1290
1350
  if (
1291
- cfg.indent !== undefined && cfg.indent !== null &&
1292
- (typeof cfg.indent !== "number" || isNaN(cfg.indent) || cfg.indent < 0 || cfg.indent > 16)
1351
+ cfg.indent !== undefined &&
1352
+ cfg.indent !== null &&
1353
+ (typeof cfg.indent !== "number" ||
1354
+ isNaN(cfg.indent) ||
1355
+ cfg.indent < 0 ||
1356
+ cfg.indent > 16)
1293
1357
  ) {
1294
1358
  logError("--indent must be a number between 0 and 16");
1295
1359
  process.exit(CONSTANTS.EXIT_ERROR);
1296
1360
  }
1297
1361
  if (
1298
- cfg.embedMaxRecursionDepth !== undefined && cfg.embedMaxRecursionDepth !== null &&
1299
- (typeof cfg.embedMaxRecursionDepth !== "number" || isNaN(cfg.embedMaxRecursionDepth) ||
1362
+ cfg.embedMaxRecursionDepth !== undefined &&
1363
+ cfg.embedMaxRecursionDepth !== null &&
1364
+ (typeof cfg.embedMaxRecursionDepth !== "number" ||
1365
+ isNaN(cfg.embedMaxRecursionDepth) ||
1300
1366
  cfg.embedMaxRecursionDepth < 1 ||
1301
1367
  cfg.embedMaxRecursionDepth > 100)
1302
1368
  ) {
@@ -1304,8 +1370,10 @@ function parseArgs(args) {
1304
1370
  process.exit(CONSTANTS.EXIT_ERROR);
1305
1371
  }
1306
1372
  if (
1307
- cfg.embedTimeout !== undefined && cfg.embedTimeout !== null &&
1308
- (typeof cfg.embedTimeout !== "number" || isNaN(cfg.embedTimeout) ||
1373
+ cfg.embedTimeout !== undefined &&
1374
+ cfg.embedTimeout !== null &&
1375
+ (typeof cfg.embedTimeout !== "number" ||
1376
+ isNaN(cfg.embedTimeout) ||
1309
1377
  cfg.embedTimeout < 1000 ||
1310
1378
  cfg.embedTimeout > 300000)
1311
1379
  ) {
@@ -1327,7 +1395,6 @@ function parseArgs(args) {
1327
1395
  const validSvgMode = ["extract", "full"];
1328
1396
  if (
1329
1397
  cfg.embedExternalSVGMode != null &&
1330
- cfg.embedExternalSVGMode !== "extract" &&
1331
1398
  !validSvgMode.includes(cfg.embedExternalSVGMode)
1332
1399
  ) {
1333
1400
  logError(`--embed-svg-mode must be one of: ${validSvgMode.join(", ")}`);
@@ -1485,7 +1552,9 @@ async function main() {
1485
1552
  if (config.output === "-") {
1486
1553
  // Why: Validate stdout output only works with single file
1487
1554
  if (files.length > 1) {
1488
- logError("Cannot output multiple files to stdout (use -o <dir> instead)");
1555
+ logError(
1556
+ "Cannot output multiple files to stdout (use -o <dir> instead)",
1557
+ );
1489
1558
  process.exit(CONSTANTS.EXIT_ERROR);
1490
1559
  }
1491
1560
  outputPath = "-";
@@ -1494,7 +1563,7 @@ async function main() {
1494
1563
  if (config.output.length !== files.length) {
1495
1564
  logError(
1496
1565
  `Output count mismatch: ${files.length} input file(s) but ${config.output.length} output path(s). ` +
1497
- `Provide either one output directory or exactly ${files.length} output file(s).`
1566
+ `Provide either one output directory or exactly ${files.length} output file(s).`,
1498
1567
  );
1499
1568
  process.exit(CONSTANTS.EXIT_ERROR);
1500
1569
  }
@@ -1506,7 +1575,7 @@ async function main() {
1506
1575
  if (files.length > 1 && !isDir(outputDir)) {
1507
1576
  logError(
1508
1577
  `Processing ${files.length} files but output "${config.output}" is not a directory. ` +
1509
- `Create the directory first or provide ${files.length} output paths.`
1578
+ `Create the directory first or provide ${files.length} output paths.`,
1510
1579
  );
1511
1580
  process.exit(CONSTANTS.EXIT_ERROR);
1512
1581
  }