@emasoft/svg-matrix 1.0.30 → 1.0.31

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 (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. package/src/verification.js +392 -1
@@ -16,8 +16,6 @@ import Decimal from "decimal.js";
16
16
 
17
17
  Decimal.set({ precision: 80 });
18
18
 
19
- const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
20
-
21
19
  // SVG path command parameters count
22
20
  const COMMAND_PARAMS = {
23
21
  M: 2,
@@ -52,6 +50,13 @@ const COMMAND_PARAMS = {
52
50
  * @returns {Array<number>|null} Parsed arc parameters or null if invalid
53
51
  */
54
52
  function parseArcArgs(argsStr) {
53
+ // Parameter validation: argsStr must be a string
54
+ if (typeof argsStr !== "string") {
55
+ throw new TypeError(
56
+ `parseArcArgs: argsStr must be a string, got ${typeof argsStr}`,
57
+ );
58
+ }
59
+
55
60
  const args = [];
56
61
  // Regex to match: number, then optionally flags and more numbers
57
62
  // Arc has: rx ry rotation flag flag x y (7 params per arc)
@@ -170,6 +175,14 @@ export function parsePath(d) {
170
175
 
171
176
  const paramCount = COMMAND_PARAMS[cmd];
172
177
 
178
+ // BUG FIX #6: Validate command exists in COMMAND_PARAMS
179
+ if (paramCount === undefined) {
180
+ console.warn(
181
+ `Unknown SVG path command '${cmd}' - skipping`,
182
+ );
183
+ continue;
184
+ }
185
+
173
186
  if (paramCount === 0 || nums.length === 0) {
174
187
  commands.push({ command: cmd, args: [] });
175
188
  } else {
@@ -203,6 +216,21 @@ export function parsePath(d) {
203
216
  * @returns {string} Formatted number
204
217
  */
205
218
  export function formatNumber(num, precision = 3) {
219
+ // Parameter validation: num must be a number
220
+ if (typeof num !== "number") {
221
+ throw new TypeError(`formatNumber: num must be a number, got ${typeof num}`);
222
+ }
223
+ // Parameter validation: precision must be a non-negative integer
224
+ if (
225
+ typeof precision !== "number" ||
226
+ !Number.isInteger(precision) ||
227
+ precision < 0
228
+ ) {
229
+ throw new TypeError(
230
+ `formatNumber: precision must be a non-negative integer, got ${precision}`,
231
+ );
232
+ }
233
+
206
234
  // BUG FIX #3: Handle NaN, Infinity, -Infinity
207
235
  if (!isFinite(num)) return "0";
208
236
  if (num === 0) return "0";
@@ -233,9 +261,35 @@ export function formatNumber(num, precision = 3) {
233
261
  * Convert a command to absolute form.
234
262
  */
235
263
  export function toAbsolute(cmd, cx, cy) {
264
+ // Parameter validation: cmd must be an object with command and args
265
+ if (!cmd || typeof cmd !== "object") {
266
+ throw new TypeError(`toAbsolute: cmd must be an object, got ${typeof cmd}`);
267
+ }
268
+ if (typeof cmd.command !== "string") {
269
+ throw new TypeError(
270
+ `toAbsolute: cmd.command must be a string, got ${typeof cmd.command}`,
271
+ );
272
+ }
273
+ if (!Array.isArray(cmd.args)) {
274
+ throw new TypeError(`toAbsolute: cmd.args must be an array`);
275
+ }
276
+ // Parameter validation: cx and cy must be numbers
277
+ if (typeof cx !== "number" || !isFinite(cx)) {
278
+ throw new TypeError(`toAbsolute: cx must be a finite number, got ${cx}`);
279
+ }
280
+ if (typeof cy !== "number" || !isFinite(cy)) {
281
+ throw new TypeError(`toAbsolute: cy must be a finite number, got ${cy}`);
282
+ }
283
+
236
284
  const { command, args } = cmd;
237
285
 
238
- if (command === command.toUpperCase()) return cmd;
286
+ // BUG FIX #7: Validate command is known before early return
287
+ if (command === command.toUpperCase()) {
288
+ if (COMMAND_PARAMS[command] === undefined) {
289
+ throw new TypeError(`toAbsolute: unknown command '${command}'`);
290
+ }
291
+ return cmd;
292
+ }
239
293
 
240
294
  const absCmd = command.toUpperCase();
241
295
  const absArgs = [...args];
@@ -244,16 +298,40 @@ export function toAbsolute(cmd, cx, cy) {
244
298
  case "m":
245
299
  case "l":
246
300
  case "t":
301
+ // Bounds check: need at least 2 args
302
+ if (absArgs.length < 2) {
303
+ throw new RangeError(
304
+ `toAbsolute: command ${command} requires at least 2 args, got ${absArgs.length}`,
305
+ );
306
+ }
247
307
  absArgs[0] += cx;
248
308
  absArgs[1] += cy;
249
309
  break;
250
310
  case "h":
311
+ // Bounds check: need at least 1 arg
312
+ if (absArgs.length < 1) {
313
+ throw new RangeError(
314
+ `toAbsolute: command ${command} requires at least 1 arg, got ${absArgs.length}`,
315
+ );
316
+ }
251
317
  absArgs[0] += cx;
252
318
  break;
253
319
  case "v":
320
+ // Bounds check: need at least 1 arg
321
+ if (absArgs.length < 1) {
322
+ throw new RangeError(
323
+ `toAbsolute: command ${command} requires at least 1 arg, got ${absArgs.length}`,
324
+ );
325
+ }
254
326
  absArgs[0] += cy;
255
327
  break;
256
328
  case "c":
329
+ // Bounds check: need at least 6 args
330
+ if (absArgs.length < 6) {
331
+ throw new RangeError(
332
+ `toAbsolute: command ${command} requires at least 6 args, got ${absArgs.length}`,
333
+ );
334
+ }
257
335
  absArgs[0] += cx;
258
336
  absArgs[1] += cy;
259
337
  absArgs[2] += cx;
@@ -263,15 +341,32 @@ export function toAbsolute(cmd, cx, cy) {
263
341
  break;
264
342
  case "s":
265
343
  case "q":
344
+ // Bounds check: need at least 4 args
345
+ if (absArgs.length < 4) {
346
+ throw new RangeError(
347
+ `toAbsolute: command ${command} requires at least 4 args, got ${absArgs.length}`,
348
+ );
349
+ }
266
350
  absArgs[0] += cx;
267
351
  absArgs[1] += cy;
268
352
  absArgs[2] += cx;
269
353
  absArgs[3] += cy;
270
354
  break;
271
355
  case "a":
356
+ // Bounds check: need at least 7 args
357
+ if (absArgs.length < 7) {
358
+ throw new RangeError(
359
+ `toAbsolute: command ${command} requires at least 7 args, got ${absArgs.length}`,
360
+ );
361
+ }
272
362
  absArgs[5] += cx;
273
363
  absArgs[6] += cy;
274
364
  break;
365
+ default:
366
+ // BUG FIX #8: Handle unknown commands in default case
367
+ throw new TypeError(
368
+ `toAbsolute: unsupported command '${command}' for conversion`,
369
+ );
275
370
  }
276
371
 
277
372
  return { command: absCmd, args: absArgs };
@@ -281,9 +376,35 @@ export function toAbsolute(cmd, cx, cy) {
281
376
  * Convert a command to relative form.
282
377
  */
283
378
  export function toRelative(cmd, cx, cy) {
379
+ // Parameter validation: cmd must be an object with command and args
380
+ if (!cmd || typeof cmd !== "object") {
381
+ throw new TypeError(`toRelative: cmd must be an object, got ${typeof cmd}`);
382
+ }
383
+ if (typeof cmd.command !== "string") {
384
+ throw new TypeError(
385
+ `toRelative: cmd.command must be a string, got ${typeof cmd.command}`,
386
+ );
387
+ }
388
+ if (!Array.isArray(cmd.args)) {
389
+ throw new TypeError(`toRelative: cmd.args must be an array`);
390
+ }
391
+ // Parameter validation: cx and cy must be numbers
392
+ if (typeof cx !== "number" || !isFinite(cx)) {
393
+ throw new TypeError(`toRelative: cx must be a finite number, got ${cx}`);
394
+ }
395
+ if (typeof cy !== "number" || !isFinite(cy)) {
396
+ throw new TypeError(`toRelative: cy must be a finite number, got ${cy}`);
397
+ }
398
+
284
399
  const { command, args } = cmd;
285
400
 
286
- if (command === command.toLowerCase() && command !== "z") return cmd;
401
+ // BUG FIX #9: Validate command is known before early return
402
+ if (command === command.toLowerCase() && command !== "z") {
403
+ if (COMMAND_PARAMS[command] === undefined) {
404
+ throw new TypeError(`toRelative: unknown command '${command}'`);
405
+ }
406
+ return cmd;
407
+ }
287
408
  if (command === "Z" || command === "z") return { command: "z", args: [] };
288
409
 
289
410
  const relCmd = command.toLowerCase();
@@ -293,16 +414,40 @@ export function toRelative(cmd, cx, cy) {
293
414
  case "M":
294
415
  case "L":
295
416
  case "T":
417
+ // Bounds check: need at least 2 args
418
+ if (relArgs.length < 2) {
419
+ throw new RangeError(
420
+ `toRelative: command ${command} requires at least 2 args, got ${relArgs.length}`,
421
+ );
422
+ }
296
423
  relArgs[0] -= cx;
297
424
  relArgs[1] -= cy;
298
425
  break;
299
426
  case "H":
427
+ // Bounds check: need at least 1 arg
428
+ if (relArgs.length < 1) {
429
+ throw new RangeError(
430
+ `toRelative: command ${command} requires at least 1 arg, got ${relArgs.length}`,
431
+ );
432
+ }
300
433
  relArgs[0] -= cx;
301
434
  break;
302
435
  case "V":
436
+ // Bounds check: need at least 1 arg
437
+ if (relArgs.length < 1) {
438
+ throw new RangeError(
439
+ `toRelative: command ${command} requires at least 1 arg, got ${relArgs.length}`,
440
+ );
441
+ }
303
442
  relArgs[0] -= cy;
304
443
  break;
305
444
  case "C":
445
+ // Bounds check: need at least 6 args
446
+ if (relArgs.length < 6) {
447
+ throw new RangeError(
448
+ `toRelative: command ${command} requires at least 6 args, got ${relArgs.length}`,
449
+ );
450
+ }
306
451
  relArgs[0] -= cx;
307
452
  relArgs[1] -= cy;
308
453
  relArgs[2] -= cx;
@@ -312,15 +457,32 @@ export function toRelative(cmd, cx, cy) {
312
457
  break;
313
458
  case "S":
314
459
  case "Q":
460
+ // Bounds check: need at least 4 args
461
+ if (relArgs.length < 4) {
462
+ throw new RangeError(
463
+ `toRelative: command ${command} requires at least 4 args, got ${relArgs.length}`,
464
+ );
465
+ }
315
466
  relArgs[0] -= cx;
316
467
  relArgs[1] -= cy;
317
468
  relArgs[2] -= cx;
318
469
  relArgs[3] -= cy;
319
470
  break;
320
471
  case "A":
472
+ // Bounds check: need at least 7 args
473
+ if (relArgs.length < 7) {
474
+ throw new RangeError(
475
+ `toRelative: command ${command} requires at least 7 args, got ${relArgs.length}`,
476
+ );
477
+ }
321
478
  relArgs[5] -= cx;
322
479
  relArgs[6] -= cy;
323
480
  break;
481
+ default:
482
+ // BUG FIX #10: Handle unknown commands in default case
483
+ throw new TypeError(
484
+ `toRelative: unsupported command '${command}' for conversion`,
485
+ );
324
486
  }
325
487
 
326
488
  return { command: relCmd, args: relArgs };
@@ -330,9 +492,41 @@ export function toRelative(cmd, cx, cy) {
330
492
  * Convert L command to H or V when applicable.
331
493
  */
332
494
  export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
495
+ // Parameter validation: cmd must be an object with command and args
496
+ if (!cmd || typeof cmd !== "object") {
497
+ throw new TypeError(`lineToHV: cmd must be an object, got ${typeof cmd}`);
498
+ }
499
+ if (typeof cmd.command !== "string") {
500
+ throw new TypeError(
501
+ `lineToHV: cmd.command must be a string, got ${typeof cmd.command}`,
502
+ );
503
+ }
504
+ if (!Array.isArray(cmd.args)) {
505
+ throw new TypeError(`lineToHV: cmd.args must be an array`);
506
+ }
507
+ // Parameter validation: cx, cy, and tolerance must be numbers
508
+ if (typeof cx !== "number" || !isFinite(cx)) {
509
+ throw new TypeError(`lineToHV: cx must be a finite number, got ${cx}`);
510
+ }
511
+ if (typeof cy !== "number" || !isFinite(cy)) {
512
+ throw new TypeError(`lineToHV: cy must be a finite number, got ${cy}`);
513
+ }
514
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
515
+ throw new TypeError(
516
+ `lineToHV: tolerance must be a non-negative finite number, got ${tolerance}`,
517
+ );
518
+ }
519
+
333
520
  const { command, args } = cmd;
334
521
  if (command !== "L" && command !== "l") return null;
335
522
 
523
+ // Bounds check: need at least 2 args
524
+ if (args.length < 2) {
525
+ throw new RangeError(
526
+ `lineToHV: command ${command} requires at least 2 args, got ${args.length}`,
527
+ );
528
+ }
529
+
336
530
  const isAbs = command === "L";
337
531
  const endX = isAbs ? args[0] : cx + args[0];
338
532
  const endY = isAbs ? args[1] : cy + args[1];
@@ -354,9 +548,47 @@ export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
354
548
  * Check if a line command returns to subpath start (can use Z).
355
549
  */
356
550
  export function lineToZ(cmd, cx, cy, startX, startY, tolerance = 1e-6) {
551
+ // Parameter validation: cmd must be an object with command and args
552
+ if (!cmd || typeof cmd !== "object") {
553
+ throw new TypeError(`lineToZ: cmd must be an object, got ${typeof cmd}`);
554
+ }
555
+ if (typeof cmd.command !== "string") {
556
+ throw new TypeError(
557
+ `lineToZ: cmd.command must be a string, got ${typeof cmd.command}`,
558
+ );
559
+ }
560
+ if (!Array.isArray(cmd.args)) {
561
+ throw new TypeError(`lineToZ: cmd.args must be an array`);
562
+ }
563
+ // Parameter validation: cx, cy, startX, startY, and tolerance must be numbers
564
+ if (typeof cx !== "number" || !isFinite(cx)) {
565
+ throw new TypeError(`lineToZ: cx must be a finite number, got ${cx}`);
566
+ }
567
+ if (typeof cy !== "number" || !isFinite(cy)) {
568
+ throw new TypeError(`lineToZ: cy must be a finite number, got ${cy}`);
569
+ }
570
+ if (typeof startX !== "number" || !isFinite(startX)) {
571
+ throw new TypeError(`lineToZ: startX must be a finite number, got ${startX}`);
572
+ }
573
+ if (typeof startY !== "number" || !isFinite(startY)) {
574
+ throw new TypeError(`lineToZ: startY must be a finite number, got ${startY}`);
575
+ }
576
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
577
+ throw new TypeError(
578
+ `lineToZ: tolerance must be a non-negative finite number, got ${tolerance}`,
579
+ );
580
+ }
581
+
357
582
  const { command, args } = cmd;
358
583
  if (command !== "L" && command !== "l") return false;
359
584
 
585
+ // Bounds check: need at least 2 args
586
+ if (args.length < 2) {
587
+ throw new RangeError(
588
+ `lineToZ: command ${command} requires at least 2 args, got ${args.length}`,
589
+ );
590
+ }
591
+
360
592
  const isAbs = command === "L";
361
593
  const endX = isAbs ? args[0] : cx + args[0];
362
594
  const endY = isAbs ? args[1] : cy + args[1];
@@ -380,6 +612,21 @@ export function isCurveStraight(
380
612
  y3,
381
613
  tolerance = 0.5,
382
614
  ) {
615
+ // Parameter validation: all parameters must be finite numbers
616
+ const params = { x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance };
617
+ for (const [name, value] of Object.entries(params)) {
618
+ if (typeof value !== "number" || !isFinite(value)) {
619
+ throw new TypeError(
620
+ `isCurveStraight: ${name} must be a finite number, got ${value}`,
621
+ );
622
+ }
623
+ }
624
+ if (tolerance < 0) {
625
+ throw new TypeError(
626
+ `isCurveStraight: tolerance must be non-negative, got ${tolerance}`,
627
+ );
628
+ }
629
+
383
630
  const chordLengthSq = (x3 - x0) ** 2 + (y3 - y0) ** 2;
384
631
 
385
632
  if (chordLengthSq < 1e-10) {
@@ -403,9 +650,47 @@ export function isCurveStraight(
403
650
  * Convert a straight cubic bezier to a line command.
404
651
  */
405
652
  export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
653
+ // Parameter validation: cmd must be an object with command and args
654
+ if (!cmd || typeof cmd !== "object") {
655
+ throw new TypeError(
656
+ `straightCurveToLine: cmd must be an object, got ${typeof cmd}`,
657
+ );
658
+ }
659
+ if (typeof cmd.command !== "string") {
660
+ throw new TypeError(
661
+ `straightCurveToLine: cmd.command must be a string, got ${typeof cmd.command}`,
662
+ );
663
+ }
664
+ if (!Array.isArray(cmd.args)) {
665
+ throw new TypeError(`straightCurveToLine: cmd.args must be an array`);
666
+ }
667
+ // Parameter validation: cx, cy, and tolerance must be numbers
668
+ if (typeof cx !== "number" || !isFinite(cx)) {
669
+ throw new TypeError(
670
+ `straightCurveToLine: cx must be a finite number, got ${cx}`,
671
+ );
672
+ }
673
+ if (typeof cy !== "number" || !isFinite(cy)) {
674
+ throw new TypeError(
675
+ `straightCurveToLine: cy must be a finite number, got ${cy}`,
676
+ );
677
+ }
678
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
679
+ throw new TypeError(
680
+ `straightCurveToLine: tolerance must be a non-negative finite number, got ${tolerance}`,
681
+ );
682
+ }
683
+
406
684
  const { command, args } = cmd;
407
685
  if (command !== "C" && command !== "c") return null;
408
686
 
687
+ // Bounds check: need at least 6 args
688
+ if (args.length < 6) {
689
+ throw new RangeError(
690
+ `straightCurveToLine: command ${command} requires at least 6 args, got ${args.length}`,
691
+ );
692
+ }
693
+
409
694
  const isAbs = command === "C";
410
695
  const cp1x = isAbs ? args[0] : cx + args[0];
411
696
  const cp1y = isAbs ? args[1] : cy + args[1];
@@ -436,6 +721,43 @@ export function serializeCommand(
436
721
  precision = 3,
437
722
  prevLastArgHadDecimal = false,
438
723
  ) {
724
+ // Parameter validation: cmd must be an object with command and args
725
+ if (!cmd || typeof cmd !== "object") {
726
+ throw new TypeError(
727
+ `serializeCommand: cmd must be an object, got ${typeof cmd}`,
728
+ );
729
+ }
730
+ if (typeof cmd.command !== "string") {
731
+ throw new TypeError(
732
+ `serializeCommand: cmd.command must be a string, got ${typeof cmd.command}`,
733
+ );
734
+ }
735
+ if (!Array.isArray(cmd.args)) {
736
+ throw new TypeError(`serializeCommand: cmd.args must be an array`);
737
+ }
738
+ // Parameter validation: prevCommand must be string or null
739
+ if (prevCommand !== null && typeof prevCommand !== "string") {
740
+ throw new TypeError(
741
+ `serializeCommand: prevCommand must be a string or null, got ${typeof prevCommand}`,
742
+ );
743
+ }
744
+ // Parameter validation: precision must be non-negative integer
745
+ if (
746
+ typeof precision !== "number" ||
747
+ !Number.isInteger(precision) ||
748
+ precision < 0
749
+ ) {
750
+ throw new TypeError(
751
+ `serializeCommand: precision must be a non-negative integer, got ${precision}`,
752
+ );
753
+ }
754
+ // Parameter validation: prevLastArgHadDecimal must be boolean
755
+ if (typeof prevLastArgHadDecimal !== "boolean") {
756
+ throw new TypeError(
757
+ `serializeCommand: prevLastArgHadDecimal must be a boolean, got ${typeof prevLastArgHadDecimal}`,
758
+ );
759
+ }
760
+
439
761
  const { command, args } = cmd;
440
762
 
441
763
  if (command === "Z" || command === "z") {
@@ -446,6 +768,20 @@ export function serializeCommand(
446
768
  // Arc format: rx ry rotation large-arc-flag sweep-flag x y
447
769
  // Per SVG spec: flags MUST be exactly 0 or 1, arc commands CANNOT be implicitly repeated
448
770
  if (command === "A" || command === "a") {
771
+ // Bounds check: arc commands need at least 7 args
772
+ if (args.length < 7) {
773
+ throw new RangeError(
774
+ `serializeCommand: arc command requires at least 7 args, got ${args.length}`,
775
+ );
776
+ }
777
+
778
+ // BUG FIX #11: Warn if args.length > 7 (multiple arcs should be separate commands)
779
+ if (args.length > 7) {
780
+ console.warn(
781
+ `serializeCommand: arc command has ${args.length} args (expected 7) - only first 7 will be used`,
782
+ );
783
+ }
784
+
449
785
  const arcArgs = [
450
786
  formatNumber(args[0], precision), // rx
451
787
  formatNumber(args[1], precision), // ry
@@ -463,6 +799,14 @@ export function serializeCommand(
463
799
  };
464
800
  }
465
801
 
802
+ // Edge case: handle empty args array (should not happen for valid commands except Z)
803
+ if (args.length === 0) {
804
+ // Only Z/z should have empty args, but we already handled that above
805
+ throw new RangeError(
806
+ `serializeCommand: command ${command} has empty args array (only Z/z should have no args)`,
807
+ );
808
+ }
809
+
466
810
  const formattedArgs = args.map((n) => formatNumber(n, precision));
467
811
 
468
812
  let argsStr = "";
@@ -525,6 +869,23 @@ export function serializeCommand(
525
869
  * Serialize a complete path to minimal string.
526
870
  */
527
871
  export function serializePath(commands, precision = 3) {
872
+ // Parameter validation: commands must be an array
873
+ if (!Array.isArray(commands)) {
874
+ throw new TypeError(
875
+ `serializePath: commands must be an array, got ${typeof commands}`,
876
+ );
877
+ }
878
+ // Parameter validation: precision must be non-negative integer
879
+ if (
880
+ typeof precision !== "number" ||
881
+ !Number.isInteger(precision) ||
882
+ precision < 0
883
+ ) {
884
+ throw new TypeError(
885
+ `serializePath: precision must be a non-negative integer, got ${precision}`,
886
+ );
887
+ }
888
+
528
889
  if (commands.length === 0) return "";
529
890
 
530
891
  let result = "";
@@ -553,6 +914,17 @@ export function serializePath(commands, precision = 3) {
553
914
  * @returns {{d: string, originalLength: number, optimizedLength: number, savings: number}}
554
915
  */
555
916
  export function convertPathData(d, options = {}) {
917
+ // Parameter validation: d must be a string
918
+ if (typeof d !== "string") {
919
+ throw new TypeError(`convertPathData: d must be a string, got ${typeof d}`);
920
+ }
921
+ // Parameter validation: options must be an object
922
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
923
+ throw new TypeError(
924
+ `convertPathData: options must be an object, got ${typeof options}`,
925
+ );
926
+ }
927
+
556
928
  const {
557
929
  floatPrecision = 3,
558
930
  straightCurves = true,
@@ -562,6 +934,46 @@ export function convertPathData(d, options = {}) {
562
934
  straightTolerance = 0.5,
563
935
  } = options;
564
936
 
937
+ // Validate option types
938
+ if (
939
+ typeof floatPrecision !== "number" ||
940
+ !Number.isInteger(floatPrecision) ||
941
+ floatPrecision < 0
942
+ ) {
943
+ throw new TypeError(
944
+ `convertPathData: floatPrecision must be a non-negative integer, got ${floatPrecision}`,
945
+ );
946
+ }
947
+ if (typeof straightCurves !== "boolean") {
948
+ throw new TypeError(
949
+ `convertPathData: straightCurves must be a boolean, got ${typeof straightCurves}`,
950
+ );
951
+ }
952
+ if (typeof lineShorthands !== "boolean") {
953
+ throw new TypeError(
954
+ `convertPathData: lineShorthands must be a boolean, got ${typeof lineShorthands}`,
955
+ );
956
+ }
957
+ if (typeof convertToZ !== "boolean") {
958
+ throw new TypeError(
959
+ `convertPathData: convertToZ must be a boolean, got ${typeof convertToZ}`,
960
+ );
961
+ }
962
+ if (typeof utilizeAbsolute !== "boolean") {
963
+ throw new TypeError(
964
+ `convertPathData: utilizeAbsolute must be a boolean, got ${typeof utilizeAbsolute}`,
965
+ );
966
+ }
967
+ if (
968
+ typeof straightTolerance !== "number" ||
969
+ !isFinite(straightTolerance) ||
970
+ straightTolerance < 0
971
+ ) {
972
+ throw new TypeError(
973
+ `convertPathData: straightTolerance must be a non-negative finite number, got ${straightTolerance}`,
974
+ );
975
+ }
976
+
565
977
  const originalLength = d.length;
566
978
  const commands = parsePath(d);
567
979
 
@@ -580,16 +992,20 @@ export function convertPathData(d, options = {}) {
580
992
 
581
993
  // BUG FIX #3: Convert zero arc radii to line per SVG spec Section 8.3.4
582
994
  // When rx or ry is 0, the arc degenerates to a straight line
583
- if (
584
- (cmd.command === "A" || cmd.command === "a") &&
585
- (cmd.args[0] === 0 || cmd.args[1] === 0)
586
- ) {
587
- const isAbs = cmd.command === "A";
588
- const endX = cmd.args[5];
589
- const endY = cmd.args[6];
590
- cmd = isAbs
591
- ? { command: "L", args: [endX, endY] }
592
- : { command: "l", args: [endX, endY] };
995
+ // BUG FIX #12: Add bounds check before accessing arc args
996
+ if (cmd.command === "A" || cmd.command === "a") {
997
+ if (cmd.args.length < 7) {
998
+ console.warn(
999
+ `Arc command has insufficient args (${cmd.args.length} < 7) - skipping optimization`,
1000
+ );
1001
+ } else if (cmd.args[0] === 0 || cmd.args[1] === 0) {
1002
+ const isAbs = cmd.command === "A";
1003
+ const endX = cmd.args[5];
1004
+ const endY = cmd.args[6];
1005
+ cmd = isAbs
1006
+ ? { command: "L", args: [endX, endY] }
1007
+ : { command: "l", args: [endX, endY] };
1008
+ }
593
1009
  }
594
1010
 
595
1011
  // 1. Straight curve to line
@@ -626,41 +1042,59 @@ export function convertPathData(d, options = {}) {
626
1042
 
627
1043
  // Update position
628
1044
  const finalCmd = toAbsolute(cmd, cx, cy);
1045
+ // BUG FIX #13: Add bounds checks before accessing finalCmd.args
629
1046
  switch (finalCmd.command) {
630
1047
  case "M":
631
- cx = finalCmd.args[0];
632
- cy = finalCmd.args[1];
633
- startX = cx;
634
- startY = cy;
1048
+ if (finalCmd.args.length >= 2) {
1049
+ cx = finalCmd.args[0];
1050
+ cy = finalCmd.args[1];
1051
+ startX = cx;
1052
+ startY = cy;
1053
+ }
635
1054
  break;
636
1055
  case "L":
637
1056
  case "T":
638
- cx = finalCmd.args[0];
639
- cy = finalCmd.args[1];
1057
+ if (finalCmd.args.length >= 2) {
1058
+ cx = finalCmd.args[0];
1059
+ cy = finalCmd.args[1];
1060
+ }
640
1061
  break;
641
1062
  case "H":
642
- cx = finalCmd.args[0];
1063
+ if (finalCmd.args.length >= 1) {
1064
+ cx = finalCmd.args[0];
1065
+ }
643
1066
  break;
644
1067
  case "V":
645
- cy = finalCmd.args[0];
1068
+ if (finalCmd.args.length >= 1) {
1069
+ cy = finalCmd.args[0];
1070
+ }
646
1071
  break;
647
1072
  case "C":
648
- cx = finalCmd.args[4];
649
- cy = finalCmd.args[5];
1073
+ if (finalCmd.args.length >= 6) {
1074
+ cx = finalCmd.args[4];
1075
+ cy = finalCmd.args[5];
1076
+ }
650
1077
  break;
651
1078
  case "S":
652
1079
  case "Q":
653
- cx = finalCmd.args[2];
654
- cy = finalCmd.args[3];
1080
+ if (finalCmd.args.length >= 4) {
1081
+ cx = finalCmd.args[2];
1082
+ cy = finalCmd.args[3];
1083
+ }
655
1084
  break;
656
1085
  case "A":
657
- cx = finalCmd.args[5];
658
- cy = finalCmd.args[6];
1086
+ if (finalCmd.args.length >= 7) {
1087
+ cx = finalCmd.args[5];
1088
+ cy = finalCmd.args[6];
1089
+ }
659
1090
  break;
660
1091
  case "Z":
661
1092
  cx = startX;
662
1093
  cy = startY;
663
1094
  break;
1095
+ default:
1096
+ // Unknown command - don't update position
1097
+ break;
664
1098
  }
665
1099
  }
666
1100
 
@@ -678,11 +1112,28 @@ export function convertPathData(d, options = {}) {
678
1112
  * Optimize all path elements in an SVG document.
679
1113
  */
680
1114
  export function optimizeDocumentPaths(root, options = {}) {
1115
+ // Parameter validation: root must be an object (DOM element)
1116
+ if (!root || typeof root !== "object") {
1117
+ throw new TypeError(
1118
+ `optimizeDocumentPaths: root must be a DOM element object, got ${typeof root}`,
1119
+ );
1120
+ }
1121
+ // Parameter validation: options must be an object
1122
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1123
+ throw new TypeError(
1124
+ `optimizeDocumentPaths: options must be an object, got ${typeof options}`,
1125
+ );
1126
+ }
1127
+
681
1128
  let pathsOptimized = 0;
682
1129
  let totalSavings = 0;
683
1130
  const details = [];
684
1131
 
685
1132
  const processElement = (el) => {
1133
+ // Validate element is an object
1134
+ if (!el || typeof el !== "object") {
1135
+ return; // Skip invalid elements silently
1136
+ }
686
1137
  const tagName = el.tagName?.toLowerCase();
687
1138
 
688
1139
  if (tagName === "path") {