@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
@@ -69,16 +69,38 @@ Decimal.set({ precision: 80 });
69
69
  * // Shows region from (-50,-50) to (150,150) in user space
70
70
  */
71
71
  export function parseViewBox(viewBoxStr) {
72
- if (!viewBoxStr || viewBoxStr.trim() === "") {
72
+ // Validate input type and content
73
+ if (!viewBoxStr || typeof viewBoxStr !== "string" || viewBoxStr.trim() === "") {
74
+ return null;
75
+ }
76
+
77
+ let parts;
78
+ try {
79
+ parts = viewBoxStr
80
+ .trim()
81
+ .split(/[\s,]+/)
82
+ .map((s) => new Decimal(s));
83
+ } catch (_err) {
84
+ console.warn(`Invalid viewBox (parse error): ${viewBoxStr}`);
73
85
  return null;
74
86
  }
75
87
 
76
- const parts = viewBoxStr
77
- .trim()
78
- .split(/[\s,]+/)
79
- .map((s) => new Decimal(s));
80
88
  if (parts.length !== 4) {
81
- console.warn(`Invalid viewBox: ${viewBoxStr}`);
89
+ console.warn(`Invalid viewBox (expected 4 values): ${viewBoxStr}`);
90
+ return null;
91
+ }
92
+
93
+ const width = parts[2];
94
+ const height = parts[3];
95
+
96
+ // Validate dimensions are positive and finite
97
+ if (width.lte(0) || height.lte(0)) {
98
+ console.warn(`Invalid viewBox (non-positive dimensions): ${viewBoxStr}`);
99
+ return null;
100
+ }
101
+
102
+ if (!width.isFinite() || !height.isFinite() || !parts[0].isFinite() || !parts[1].isFinite()) {
103
+ console.warn(`Invalid viewBox (non-finite values): ${viewBoxStr}`);
82
104
  return null;
83
105
  }
84
106
 
@@ -228,6 +250,20 @@ export function computeViewBoxTransform(
228
250
  const vpW = D(viewportWidth);
229
251
  const vpH = D(viewportHeight);
230
252
 
253
+ // Validate dimensions are positive and finite to prevent division by zero
254
+ if (vbW.lte(0) || vbH.lte(0)) {
255
+ console.warn("Invalid viewBox dimensions (must be positive)");
256
+ return Matrix.identity(3);
257
+ }
258
+ if (vpW.lte(0) || vpH.lte(0)) {
259
+ console.warn("Invalid viewport dimensions (must be positive)");
260
+ return Matrix.identity(3);
261
+ }
262
+ if (!vbW.isFinite() || !vbH.isFinite() || !vpW.isFinite() || !vpH.isFinite()) {
263
+ console.warn("Invalid dimensions (must be finite)");
264
+ return Matrix.identity(3);
265
+ }
266
+
231
267
  // Default preserveAspectRatio
232
268
  const parValue = par || { align: "xMidYMid", meetOrSlice: "meet" };
233
269
 
@@ -340,8 +376,19 @@ export class SVGViewport {
340
376
  preserveAspectRatio = null,
341
377
  transform = null,
342
378
  ) {
343
- this.width = new Decimal(width);
344
- this.height = new Decimal(height);
379
+ // Validate width and height
380
+ const w = new Decimal(width);
381
+ const h = new Decimal(height);
382
+
383
+ if (w.lte(0) || h.lte(0)) {
384
+ throw new Error("SVGViewport dimensions must be positive");
385
+ }
386
+ if (!w.isFinite() || !h.isFinite()) {
387
+ throw new Error("SVGViewport dimensions must be finite");
388
+ }
389
+
390
+ this.width = w;
391
+ this.height = h;
345
392
  this.viewBox = viewBox ? parseViewBox(viewBox) : null;
346
393
  this.preserveAspectRatio = parsePreserveAspectRatio(preserveAspectRatio);
347
394
  this.transform = transform;
@@ -438,9 +485,19 @@ export class SVGViewport {
438
485
  * // Combines two viewBox transforms and a rotation
439
486
  */
440
487
  export function buildFullCTM(hierarchy) {
488
+ // Validate input is an array
489
+ if (!hierarchy || !Array.isArray(hierarchy)) {
490
+ console.warn("buildFullCTM: hierarchy must be an array");
491
+ return Matrix.identity(3);
492
+ }
493
+
441
494
  let ctm = Matrix.identity(3);
442
495
 
443
496
  for (const item of hierarchy) {
497
+ if (!item) {
498
+ continue; // Skip null/undefined items
499
+ }
500
+
444
501
  if (typeof item === "string") {
445
502
  // Backwards compatibility: treat string as transform attribute
446
503
  if (item) {
@@ -448,15 +505,24 @@ export function buildFullCTM(hierarchy) {
448
505
  ctm = ctm.mul(matrix);
449
506
  }
450
507
  } else if (item.type === "svg") {
451
- // SVG viewport with potential viewBox
452
- const viewport = new SVGViewport(
453
- item.width,
454
- item.height,
455
- item.viewBox || null,
456
- item.preserveAspectRatio || null,
457
- item.transform || null,
458
- );
459
- ctm = ctm.mul(viewport.getTransformMatrix());
508
+ // SVG viewport with potential viewBox - validate required properties
509
+ if (item.width === undefined || item.height === undefined) {
510
+ console.warn("buildFullCTM: SVG viewport missing width or height");
511
+ continue;
512
+ }
513
+ try {
514
+ const viewport = new SVGViewport(
515
+ item.width,
516
+ item.height,
517
+ item.viewBox || null,
518
+ item.preserveAspectRatio || null,
519
+ item.transform || null,
520
+ );
521
+ ctm = ctm.mul(viewport.getTransformMatrix());
522
+ } catch (err) {
523
+ console.warn(`buildFullCTM: Failed to create viewport: ${err.message}`);
524
+ continue;
525
+ }
460
526
  } else if (item.type === "g" || item.type === "element") {
461
527
  // Group or element with optional transform
462
528
  if (item.transform) {
@@ -520,6 +586,13 @@ export function buildFullCTM(hierarchy) {
520
586
  export function resolveLength(value, referenceSize, dpi = 96) {
521
587
  const D = (x) => new Decimal(x);
522
588
 
589
+ // Validate dpi parameter
590
+ let validDpi = dpi;
591
+ if (typeof validDpi !== "number" || validDpi <= 0 || !isFinite(validDpi)) {
592
+ console.warn("resolveLength: invalid dpi (must be positive finite number), using default 96");
593
+ validDpi = 96;
594
+ }
595
+
523
596
  if (typeof value === "number") {
524
597
  return D(value);
525
598
  }
@@ -535,6 +608,7 @@ export function resolveLength(value, referenceSize, dpi = 96) {
535
608
  // Extract numeric value and unit
536
609
  const match = str.match(/^([+-]?[\d.]+(?:e[+-]?\d+)?)(.*)?$/i);
537
610
  if (!match) {
611
+ console.warn(`resolveLength: invalid length value "${value}"`);
538
612
  return D(0);
539
613
  }
540
614
 
@@ -551,15 +625,15 @@ export function resolveLength(value, referenceSize, dpi = 96) {
551
625
  case "rem":
552
626
  return num.mul(16);
553
627
  case "pt":
554
- return num.mul(dpi).div(72);
628
+ return num.mul(validDpi).div(72);
555
629
  case "pc":
556
- return num.mul(dpi).div(6);
630
+ return num.mul(validDpi).div(6);
557
631
  case "in":
558
- return num.mul(dpi);
632
+ return num.mul(validDpi);
559
633
  case "cm":
560
- return num.mul(dpi).div(2.54);
634
+ return num.mul(validDpi).div(2.54);
561
635
  case "mm":
562
- return num.mul(dpi).div(25.4);
636
+ return num.mul(validDpi).div(25.4);
563
637
  default:
564
638
  return num; // Unknown unit, treat as px
565
639
  }
@@ -664,10 +738,28 @@ export function objectBoundingBoxTransform(
664
738
  bboxWidth,
665
739
  bboxHeight,
666
740
  ) {
667
- const _D = (x) => new Decimal(x);
741
+ const D = (x) => new Decimal(x);
742
+
743
+ const xD = D(bboxX);
744
+ const yD = D(bboxY);
745
+ const wD = D(bboxWidth);
746
+ const hD = D(bboxHeight);
747
+
748
+ // Validate all parameters are finite
749
+ if (!xD.isFinite() || !yD.isFinite() || !wD.isFinite() || !hD.isFinite()) {
750
+ throw new Error("objectBoundingBoxTransform: all parameters must be finite");
751
+ }
752
+
753
+ // Validate dimensions are positive (zero is technically valid for degenerate case, but warn)
754
+ if (wD.lte(0) || hD.lte(0)) {
755
+ console.warn("objectBoundingBoxTransform: zero or negative dimensions create degenerate transform");
756
+ // Return identity for degenerate case to avoid division by zero
757
+ return Matrix.identity(3);
758
+ }
759
+
668
760
  // Transform: scale(bboxWidth, bboxHeight) then translate(bboxX, bboxY)
669
- const scaleM = Transforms2D.scale(bboxWidth, bboxHeight);
670
- const translateM = Transforms2D.translation(bboxX, bboxY);
761
+ const scaleM = Transforms2D.scale(wD, hD);
762
+ const translateM = Transforms2D.translation(xD, yD);
671
763
  return translateM.mul(scaleM);
672
764
  }
673
765
 
@@ -736,6 +828,14 @@ export function ellipseToPath(cx, cy, rx, ry) {
736
828
  rxD = D(rx),
737
829
  ryD = D(ry);
738
830
 
831
+ // Validate radii are positive
832
+ if (rxD.lte(0) || ryD.lte(0)) {
833
+ throw new Error("Ellipse radii must be positive");
834
+ }
835
+ if (!rxD.isFinite() || !ryD.isFinite() || !cxD.isFinite() || !cyD.isFinite()) {
836
+ throw new Error("Ellipse parameters must be finite");
837
+ }
838
+
739
839
  // Kappa for bezier approximation of circle/ellipse: 4 * (sqrt(2) - 1) / 3
740
840
  const kappa = D("0.5522847498307936");
741
841
  const kx = rxD.mul(kappa);
@@ -810,9 +910,23 @@ export function rectToPath(x, y, width, height, rx = 0, ry = null) {
810
910
  yD = D(y),
811
911
  wD = D(width),
812
912
  hD = D(height);
913
+
914
+ // Validate dimensions are positive
915
+ if (wD.lte(0) || hD.lte(0)) {
916
+ throw new Error("Rectangle dimensions must be positive");
917
+ }
918
+ if (!wD.isFinite() || !hD.isFinite() || !xD.isFinite() || !yD.isFinite()) {
919
+ throw new Error("Rectangle parameters must be finite");
920
+ }
921
+
813
922
  let rxD = D(rx);
814
923
  let ryD = ry !== null ? D(ry) : rxD;
815
924
 
925
+ // Validate radii are non-negative
926
+ if (rxD.lt(0) || ryD.lt(0)) {
927
+ throw new Error("Rectangle corner radii must be non-negative");
928
+ }
929
+
816
930
  // Clamp radii to half dimensions
817
931
  const halfW = wD.div(2);
818
932
  const halfH = hD.div(2);
@@ -857,7 +971,17 @@ export function rectToPath(x, y, width, height, rx = 0, ry = null) {
857
971
  */
858
972
  export function lineToPath(x1, y1, x2, y2) {
859
973
  const D = (n) => new Decimal(n);
860
- return `M ${D(x1).toFixed(6)} ${D(y1).toFixed(6)} L ${D(x2).toFixed(6)} ${D(y2).toFixed(6)}`;
974
+ const x1D = D(x1);
975
+ const y1D = D(y1);
976
+ const x2D = D(x2);
977
+ const y2D = D(y2);
978
+
979
+ // Validate all parameters are finite
980
+ if (!x1D.isFinite() || !y1D.isFinite() || !x2D.isFinite() || !y2D.isFinite()) {
981
+ throw new Error("lineToPath: all parameters must be finite");
982
+ }
983
+
984
+ return `M ${x1D.toFixed(6)} ${y1D.toFixed(6)} L ${x2D.toFixed(6)} ${y2D.toFixed(6)}`;
861
985
  }
862
986
 
863
987
  /**
@@ -946,17 +1070,39 @@ export function polylineToPath(points) {
946
1070
  * @returns {Array<[string, string]>} Array of [x, y] coordinate pairs as strings
947
1071
  */
948
1072
  function parsePointPairs(points) {
1073
+ // Validate input
1074
+ if (!points) {
1075
+ return [];
1076
+ }
1077
+
949
1078
  let coords;
950
- if (Array.isArray(points)) {
951
- coords = points.flat().map((n) => new Decimal(n).toFixed(6));
952
- } else {
953
- coords = points
954
- .trim()
955
- .split(/[\s,]+/)
956
- .map((s) => new Decimal(s).toFixed(6));
1079
+ try {
1080
+ if (Array.isArray(points)) {
1081
+ coords = points.flat().map((n) => new Decimal(n).toFixed(6));
1082
+ } else if (typeof points === "string") {
1083
+ coords = points
1084
+ .trim()
1085
+ .split(/[\s,]+/)
1086
+ .filter((s) => s.length > 0)
1087
+ .map((s) => new Decimal(s).toFixed(6));
1088
+ } else {
1089
+ console.warn("parsePointPairs: invalid input type");
1090
+ return [];
1091
+ }
1092
+ } catch (err) {
1093
+ console.warn(`parsePointPairs: parse error - ${err.message}`);
1094
+ return [];
1095
+ }
1096
+
1097
+ // Validate even number of coordinates
1098
+ if (coords.length % 2 !== 0) {
1099
+ console.warn("parsePointPairs: odd number of coordinates, ignoring last value");
1100
+ coords = coords.slice(0, -1); // Remove last element to ensure even length
957
1101
  }
1102
+
958
1103
  const pairs = [];
959
- for (let i = 0; i < coords.length - 1; i += 2) {
1104
+ // Bounds check: i + 1 must be within array
1105
+ for (let i = 0; i + 1 < coords.length; i += 2) {
960
1106
  pairs.push([coords[i], coords[i + 1]]);
961
1107
  }
962
1108
  return pairs;
@@ -1047,6 +1193,40 @@ export function transformArc(
1047
1193
  const D = (n) => new Decimal(n);
1048
1194
  const NEAR_ZERO = D("1e-16");
1049
1195
 
1196
+ // Validate inputs
1197
+ if (!matrix || !matrix.data || !Array.isArray(matrix.data) || matrix.data.length < 3) {
1198
+ throw new Error("transformArc: invalid matrix");
1199
+ }
1200
+
1201
+ const rxD = D(rx);
1202
+ const ryD = D(ry);
1203
+
1204
+ // Validate radii are positive
1205
+ if (rxD.lte(0) || ryD.lte(0)) {
1206
+ console.warn("transformArc: radii must be positive, returning degenerate arc");
1207
+ return {
1208
+ rx: 0,
1209
+ ry: 0,
1210
+ xAxisRotation: 0,
1211
+ largeArcFlag: largeArcFlag,
1212
+ sweepFlag: sweepFlag,
1213
+ x: D(x).toNumber(),
1214
+ y: D(y).toNumber(),
1215
+ };
1216
+ }
1217
+
1218
+ // Validate and normalize flags to 0 or 1
1219
+ let validLargeArcFlag = largeArcFlag;
1220
+ if (typeof validLargeArcFlag !== "number" || (validLargeArcFlag !== 0 && validLargeArcFlag !== 1)) {
1221
+ console.warn(`transformArc: largeArcFlag must be 0 or 1, got ${largeArcFlag}`);
1222
+ validLargeArcFlag = validLargeArcFlag ? 1 : 0;
1223
+ }
1224
+ let validSweepFlag = sweepFlag;
1225
+ if (typeof validSweepFlag !== "number" || (validSweepFlag !== 0 && validSweepFlag !== 1)) {
1226
+ console.warn(`transformArc: sweepFlag must be 0 or 1, got ${sweepFlag}`);
1227
+ validSweepFlag = validSweepFlag ? 1 : 0;
1228
+ }
1229
+
1050
1230
  // Get matrix components
1051
1231
  const a = matrix.data[0][0];
1052
1232
  const b = matrix.data[1][0];
@@ -1066,8 +1246,7 @@ export function transformArc(
1066
1246
  const sinRot = Decimal.sin(rotRad);
1067
1247
  const cosRot = Decimal.cos(rotRad);
1068
1248
 
1069
- const rxD = D(rx),
1070
- ryD = D(ry);
1249
+ // rxD and ryD already declared in validation section above
1071
1250
 
1072
1251
  // Transform the ellipse axes using the algorithm from lean-svg
1073
1252
  // m0, m1 represent the transformed X-axis direction of the ellipse
@@ -1132,10 +1311,10 @@ export function transformArc(
1132
1311
 
1133
1312
  // Check if matrix flips orientation (negative determinant)
1134
1313
  const det = a.mul(d).minus(b.mul(c));
1135
- let newSweepFlag = sweepFlag;
1314
+ let newSweepFlag = validSweepFlag;
1136
1315
  if (det.lt(0)) {
1137
1316
  // Flip sweep direction
1138
- newSweepFlag = sweepFlag ? 0 : 1;
1317
+ newSweepFlag = validSweepFlag ? 0 : 1;
1139
1318
  }
1140
1319
 
1141
1320
  // Convert rotation back to degrees and normalize to [0, 180)
@@ -1147,7 +1326,7 @@ export function transformArc(
1147
1326
  rx: newRx.toNumber(),
1148
1327
  ry: newRy.toNumber(),
1149
1328
  xAxisRotation: newRotDeg.toNumber(),
1150
- largeArcFlag: largeArcFlag,
1329
+ largeArcFlag: validLargeArcFlag,
1151
1330
  sweepFlag: newSweepFlag,
1152
1331
  x: newX.toNumber(),
1153
1332
  y: newY.toNumber(),
@@ -1210,28 +1389,42 @@ export function transformArc(
1210
1389
  * // Translation by (50, 50) specified in matrix form
1211
1390
  */
1212
1391
  export function parseTransformFunction(func, args) {
1392
+ // Validate inputs
1393
+ if (!func || typeof func !== "string") {
1394
+ console.warn("parseTransformFunction: invalid function name");
1395
+ return Matrix.identity(3);
1396
+ }
1397
+ if (!args || !Array.isArray(args)) {
1398
+ console.warn("parseTransformFunction: invalid arguments array");
1399
+ return Matrix.identity(3);
1400
+ }
1401
+
1213
1402
  const D = (x) => new Decimal(x);
1214
1403
 
1215
1404
  switch (func.toLowerCase()) {
1216
1405
  case "translate": {
1217
- const tx = args[0] || 0;
1218
- const ty = args[1] || 0;
1406
+ const tx = args[0] !== undefined ? args[0] : 0;
1407
+ const ty = args[1] !== undefined ? args[1] : 0;
1219
1408
  return Transforms2D.translation(tx, ty);
1220
1409
  }
1221
1410
 
1222
1411
  case "scale": {
1223
- const sx = args[0] || 1;
1412
+ const sx = args[0] !== undefined ? args[0] : 1;
1224
1413
  const sy = args[1] !== undefined ? args[1] : sx;
1225
1414
  return Transforms2D.scale(sx, sy);
1226
1415
  }
1227
1416
 
1228
1417
  case "rotate": {
1229
1418
  // SVG rotate is in degrees, can have optional cx, cy
1230
- const angleDeg = args[0] || 0;
1419
+ const angleDeg = args[0] !== undefined ? args[0] : 0;
1231
1420
  const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
1232
1421
 
1233
1422
  if (args.length >= 3) {
1234
1423
  // rotate(angle, cx, cy) - rotation around point
1424
+ if (args[1] === undefined || args[2] === undefined) {
1425
+ console.warn("parseTransformFunction: rotate(angle, cx, cy) missing cx or cy");
1426
+ return Transforms2D.rotate(angleRad);
1427
+ }
1235
1428
  const cx = args[1];
1236
1429
  const cy = args[2];
1237
1430
  return Transforms2D.rotateAroundPoint(angleRad, cx, cy);
@@ -1240,14 +1433,14 @@ export function parseTransformFunction(func, args) {
1240
1433
  }
1241
1434
 
1242
1435
  case "skewx": {
1243
- const angleDeg = args[0] || 0;
1436
+ const angleDeg = args[0] !== undefined ? args[0] : 0;
1244
1437
  const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
1245
1438
  const tanVal = Decimal.tan(angleRad);
1246
1439
  return Transforms2D.skew(tanVal, 0);
1247
1440
  }
1248
1441
 
1249
1442
  case "skewy": {
1250
- const angleDeg = args[0] || 0;
1443
+ const angleDeg = args[0] !== undefined ? args[0] : 0;
1251
1444
  const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
1252
1445
  const tanVal = Decimal.tan(angleRad);
1253
1446
  return Transforms2D.skew(0, tanVal);
@@ -1257,7 +1450,11 @@ export function parseTransformFunction(func, args) {
1257
1450
  // matrix(a, b, c, d, e, f) -> | a c e |
1258
1451
  // | b d f |
1259
1452
  // | 0 0 1 |
1260
- const [a, b, c, d, e, f] = args.map((x) => D(x || 0));
1453
+ if (args.length < 6) {
1454
+ console.warn(`parseTransformFunction: matrix() requires 6 arguments, got ${args.length}`);
1455
+ return Matrix.identity(3);
1456
+ }
1457
+ const [a, b, c, d, e, f] = args.slice(0, 6).map((x) => D(x !== undefined ? x : 0));
1261
1458
  return Matrix.from([
1262
1459
  [a, c, e],
1263
1460
  [b, d, f],
@@ -1371,6 +1568,12 @@ export function parseTransformAttribute(transformStr) {
1371
1568
  * // Returns: Identity matrix
1372
1569
  */
1373
1570
  export function buildCTM(transformStack) {
1571
+ // Validate input is an array
1572
+ if (!transformStack || !Array.isArray(transformStack)) {
1573
+ console.warn("buildCTM: transformStack must be an array");
1574
+ return Matrix.identity(3);
1575
+ }
1576
+
1374
1577
  let ctm = Matrix.identity(3);
1375
1578
 
1376
1579
  for (const transformStr of transformStack) {
@@ -1420,6 +1623,23 @@ export function buildCTM(transformStack) {
1420
1623
  * // Point transformed through all operations
1421
1624
  */
1422
1625
  export function applyToPoint(ctm, x, y) {
1626
+ // Validate CTM
1627
+ if (!ctm || !ctm.data || !Array.isArray(ctm.data)) {
1628
+ throw new Error("applyToPoint: invalid CTM matrix");
1629
+ }
1630
+
1631
+ // Validate coordinates are valid numbers
1632
+ const D = (val) => new Decimal(val);
1633
+ try {
1634
+ const xD = D(x);
1635
+ const yD = D(y);
1636
+ if (!xD.isFinite() || !yD.isFinite()) {
1637
+ throw new Error("applyToPoint: coordinates must be finite");
1638
+ }
1639
+ } catch (err) {
1640
+ throw new Error(`applyToPoint: invalid coordinates - ${err.message}`);
1641
+ }
1642
+
1423
1643
  const [tx, ty] = Transforms2D.applyTransform(ctm, x, y);
1424
1644
  return { x: tx, y: ty };
1425
1645
  }
@@ -1466,14 +1686,40 @@ export function applyToPoint(ctm, x, y) {
1466
1686
  * // Returns single matrix() function representing all transforms
1467
1687
  */
1468
1688
  export function toSVGMatrix(ctm, precision = 6) {
1469
- const a = ctm.data[0][0].toFixed(precision);
1470
- const b = ctm.data[1][0].toFixed(precision);
1471
- const c = ctm.data[0][1].toFixed(precision);
1472
- const d = ctm.data[1][1].toFixed(precision);
1473
- const e = ctm.data[0][2].toFixed(precision);
1474
- const f = ctm.data[1][2].toFixed(precision);
1475
-
1476
- return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
1689
+ // Validate CTM structure
1690
+ if (
1691
+ !ctm ||
1692
+ !ctm.data ||
1693
+ !Array.isArray(ctm.data) ||
1694
+ ctm.data.length < 3 ||
1695
+ !Array.isArray(ctm.data[0]) ||
1696
+ !Array.isArray(ctm.data[1]) ||
1697
+ ctm.data[0].length < 3 ||
1698
+ ctm.data[1].length < 3
1699
+ ) {
1700
+ throw new Error("toSVGMatrix: invalid CTM matrix structure");
1701
+ }
1702
+
1703
+ // Validate precision
1704
+ let validPrecision = precision;
1705
+ if (typeof validPrecision !== "number" || validPrecision < 0 || validPrecision > 20) {
1706
+ console.warn("toSVGMatrix: invalid precision, using default 6");
1707
+ validPrecision = 6;
1708
+ }
1709
+
1710
+ // Validate matrix elements have toFixed method
1711
+ try {
1712
+ const a = ctm.data[0][0].toFixed(validPrecision);
1713
+ const b = ctm.data[1][0].toFixed(validPrecision);
1714
+ const c = ctm.data[0][1].toFixed(validPrecision);
1715
+ const d = ctm.data[1][1].toFixed(validPrecision);
1716
+ const e = ctm.data[0][2].toFixed(validPrecision);
1717
+ const f = ctm.data[1][2].toFixed(validPrecision);
1718
+
1719
+ return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
1720
+ } catch (err) {
1721
+ throw new Error(`toSVGMatrix: invalid matrix elements - ${err.message}`);
1722
+ }
1477
1723
  }
1478
1724
 
1479
1725
  /**
@@ -1519,6 +1765,16 @@ export function toSVGMatrix(ctm, precision = 6) {
1519
1765
  * // Returns: false
1520
1766
  */
1521
1767
  export function isIdentity(m, tolerance = "1e-10") {
1768
+ // Validate matrix parameter
1769
+ if (!m || !m.data || !Array.isArray(m.data)) {
1770
+ throw new Error("isIdentity: invalid matrix parameter");
1771
+ }
1772
+
1773
+ // Validate matrix has equals method
1774
+ if (typeof m.equals !== "function") {
1775
+ throw new Error("isIdentity: matrix must have equals method");
1776
+ }
1777
+
1522
1778
  const identity = Matrix.identity(3);
1523
1779
  return m.equals(identity, tolerance);
1524
1780
  }
@@ -1585,11 +1841,31 @@ export function isIdentity(m, tolerance = "1e-10") {
1585
1841
  * // Relative commands preserved in output
1586
1842
  */
1587
1843
  export function transformPathData(pathData, ctm, options = {}) {
1844
+ // Validate inputs
1845
+ if (!pathData || typeof pathData !== "string") {
1846
+ console.warn("transformPathData: invalid pathData (must be string)");
1847
+ return "";
1848
+ }
1849
+ if (!ctm || !ctm.data) {
1850
+ console.warn("transformPathData: invalid CTM matrix");
1851
+ return pathData; // Return unchanged if matrix is invalid
1852
+ }
1853
+
1588
1854
  const { toAbsolute = true, precision = 6 } = options;
1855
+
1856
+ // Validate precision
1857
+ if (typeof precision !== "number" || precision < 0 || precision > 20) {
1858
+ console.warn("transformPathData: invalid precision, using default 6");
1859
+ }
1860
+
1589
1861
  const D = (x) => new Decimal(x);
1590
1862
 
1591
1863
  // Parse path into commands
1592
1864
  const commands = parsePathCommands(pathData);
1865
+ if (commands.length === 0) {
1866
+ return "";
1867
+ }
1868
+
1593
1869
  const result = [];
1594
1870
 
1595
1871
  // Track current position for relative commands
@@ -1605,7 +1881,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1605
1881
  switch (cmdUpper) {
1606
1882
  case "M": {
1607
1883
  const transformed = [];
1608
- for (let i = 0; i < args.length; i += 2) {
1884
+ // Validate args length is multiple of 2
1885
+ if (args.length % 2 !== 0) {
1886
+ console.warn(`transformPathData: M command has ${args.length} args, expected multiple of 2`);
1887
+ }
1888
+ for (let i = 0; i + 1 < args.length; i += 2) {
1609
1889
  let x = D(args[i]),
1610
1890
  y = D(args[i + 1]);
1611
1891
  if (isRelative) {
@@ -1629,7 +1909,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1629
1909
 
1630
1910
  case "L": {
1631
1911
  const transformed = [];
1632
- for (let i = 0; i < args.length; i += 2) {
1912
+ // Validate args length is multiple of 2
1913
+ if (args.length % 2 !== 0) {
1914
+ console.warn(`transformPathData: L command has ${args.length} args, expected multiple of 2`);
1915
+ }
1916
+ for (let i = 0; i + 1 < args.length; i += 2) {
1633
1917
  let x = D(args[i]),
1634
1918
  y = D(args[i + 1]);
1635
1919
  if (isRelative) {
@@ -1649,6 +1933,10 @@ export function transformPathData(pathData, ctm, options = {}) {
1649
1933
 
1650
1934
  case "H": {
1651
1935
  // Horizontal line becomes L after transform (may have Y component)
1936
+ if (args.length < 1) {
1937
+ console.warn("transformPathData: H command requires at least 1 argument");
1938
+ break;
1939
+ }
1652
1940
  let x = D(args[0]);
1653
1941
  if (isRelative) {
1654
1942
  x = x.plus(curX);
@@ -1666,6 +1954,10 @@ export function transformPathData(pathData, ctm, options = {}) {
1666
1954
 
1667
1955
  case "V": {
1668
1956
  // Vertical line becomes L after transform (may have X component)
1957
+ if (args.length < 1) {
1958
+ console.warn("transformPathData: V command requires at least 1 argument");
1959
+ break;
1960
+ }
1669
1961
  const x = curX;
1670
1962
  let y = D(args[0]);
1671
1963
  if (isRelative) {
@@ -1683,7 +1975,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1683
1975
 
1684
1976
  case "C": {
1685
1977
  const transformed = [];
1686
- for (let i = 0; i < args.length; i += 6) {
1978
+ // Validate args length is multiple of 6
1979
+ if (args.length % 6 !== 0) {
1980
+ console.warn(`transformPathData: C command has ${args.length} args, expected multiple of 6`);
1981
+ }
1982
+ for (let i = 0; i + 5 < args.length; i += 6) {
1687
1983
  let x1 = D(args[i]),
1688
1984
  y1 = D(args[i + 1]);
1689
1985
  let x2 = D(args[i + 2]),
@@ -1722,7 +2018,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1722
2018
 
1723
2019
  case "S": {
1724
2020
  const transformed = [];
1725
- for (let i = 0; i < args.length; i += 4) {
2021
+ // Validate args length is multiple of 4
2022
+ if (args.length % 4 !== 0) {
2023
+ console.warn(`transformPathData: S command has ${args.length} args, expected multiple of 4`);
2024
+ }
2025
+ for (let i = 0; i + 3 < args.length; i += 4) {
1726
2026
  let x2 = D(args[i]),
1727
2027
  y2 = D(args[i + 1]);
1728
2028
  let x = D(args[i + 2]),
@@ -1754,7 +2054,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1754
2054
 
1755
2055
  case "Q": {
1756
2056
  const transformed = [];
1757
- for (let i = 0; i < args.length; i += 4) {
2057
+ // Validate args length is multiple of 4
2058
+ if (args.length % 4 !== 0) {
2059
+ console.warn(`transformPathData: Q command has ${args.length} args, expected multiple of 4`);
2060
+ }
2061
+ for (let i = 0; i + 3 < args.length; i += 4) {
1758
2062
  let x1 = D(args[i]),
1759
2063
  y1 = D(args[i + 1]);
1760
2064
  let x = D(args[i + 2]),
@@ -1786,7 +2090,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1786
2090
 
1787
2091
  case "T": {
1788
2092
  const transformed = [];
1789
- for (let i = 0; i < args.length; i += 2) {
2093
+ // Validate args length is multiple of 2
2094
+ if (args.length % 2 !== 0) {
2095
+ console.warn(`transformPathData: T command has ${args.length} args, expected multiple of 2`);
2096
+ }
2097
+ for (let i = 0; i + 1 < args.length; i += 2) {
1790
2098
  let x = D(args[i]),
1791
2099
  y = D(args[i + 1]);
1792
2100
  if (isRelative) {
@@ -1807,7 +2115,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1807
2115
  case "A": {
1808
2116
  // Use proper arc transformation
1809
2117
  const transformed = [];
1810
- for (let i = 0; i < args.length; i += 7) {
2118
+ // Validate args length is multiple of 7
2119
+ if (args.length % 7 !== 0) {
2120
+ console.warn(`transformPathData: Arc command has ${args.length} args, expected multiple of 7`);
2121
+ }
2122
+ for (let i = 0; i + 6 < args.length; i += 7) {
1811
2123
  const rx = args[i];
1812
2124
  const ry = args[i + 1];
1813
2125
  const rotation = args[i + 2];
@@ -1899,6 +2211,11 @@ export function transformPathData(pathData, ctm, options = {}) {
1899
2211
  * // ]
1900
2212
  */
1901
2213
  function parsePathCommands(pathData) {
2214
+ // Validate input
2215
+ if (!pathData || typeof pathData !== "string") {
2216
+ return [];
2217
+ }
2218
+
1902
2219
  const commands = [];
1903
2220
  const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
1904
2221
  let match;
@@ -1911,7 +2228,14 @@ function parsePathCommands(pathData) {
1911
2228
  ? argsStr
1912
2229
  .split(/[\s,]+/)
1913
2230
  .filter((s) => s.length > 0)
1914
- .map((s) => parseFloat(s))
2231
+ .map((s) => {
2232
+ const val = parseFloat(s);
2233
+ if (isNaN(val)) {
2234
+ console.warn(`parsePathCommands: invalid number '${s}'`);
2235
+ return 0;
2236
+ }
2237
+ return val;
2238
+ })
1915
2239
  : [];
1916
2240
  commands.push({ cmd, args });
1917
2241
  }