@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
@@ -5,8 +5,20 @@ import { Matrix } from "./matrix.js";
5
5
  * Helper to convert any numeric input to Decimal.
6
6
  * @param {number|string|Decimal} x - The value to convert
7
7
  * @returns {Decimal} The Decimal representation
8
+ * @throws {Error} If value cannot be converted to a valid Decimal
8
9
  */
9
- const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
10
+ const D = (x) => {
11
+ if (x instanceof Decimal) return x;
12
+ try {
13
+ const result = new Decimal(x);
14
+ if (!result.isFinite()) {
15
+ throw new Error(`Value must be finite, got ${x}`);
16
+ }
17
+ return result;
18
+ } catch (_err) {
19
+ throw new Error(`Invalid numeric value: ${x}`);
20
+ }
21
+ };
10
22
 
11
23
  /**
12
24
  * Validates that a value is a valid numeric type (number, string, or Decimal).
@@ -198,9 +210,14 @@ export function scale(sx, sy = null, sz = null) {
198
210
  * const pitch = rotateX(-0.1); // Slight downward tilt
199
211
  */
200
212
  export function rotateX(theta) {
213
+ validateNumeric(theta, 'theta');
201
214
  const t = D(theta);
202
- const c = new Decimal(Math.cos(t.toNumber()));
203
- const s = new Decimal(Math.sin(t.toNumber()));
215
+ const tNum = t.toNumber();
216
+ if (!Number.isFinite(tNum)) {
217
+ throw new Error(`theta must produce a finite angle, got ${theta}`);
218
+ }
219
+ const c = new Decimal(Math.cos(tNum));
220
+ const s = new Decimal(Math.sin(tNum));
204
221
  return Matrix.from([
205
222
  [new Decimal(1), new Decimal(0), new Decimal(0), new Decimal(0)],
206
223
  [new Decimal(0), c, s.negated(), new Decimal(0)],
@@ -242,9 +259,14 @@ export function rotateX(theta) {
242
259
  * const yaw = rotateY(0.5); // Turn right by ~28.6°
243
260
  */
244
261
  export function rotateY(theta) {
262
+ validateNumeric(theta, 'theta');
245
263
  const t = D(theta);
246
- const c = new Decimal(Math.cos(t.toNumber()));
247
- const s = new Decimal(Math.sin(t.toNumber()));
264
+ const tNum = t.toNumber();
265
+ if (!Number.isFinite(tNum)) {
266
+ throw new Error(`theta must produce a finite angle, got ${theta}`);
267
+ }
268
+ const c = new Decimal(Math.cos(tNum));
269
+ const s = new Decimal(Math.sin(tNum));
248
270
  return Matrix.from([
249
271
  [c, new Decimal(0), s, new Decimal(0)],
250
272
  [new Decimal(0), new Decimal(1), new Decimal(0), new Decimal(0)],
@@ -287,9 +309,14 @@ export function rotateY(theta) {
287
309
  * const roll = rotateZ(0.2); // Slight clockwise tilt from viewer perspective
288
310
  */
289
311
  export function rotateZ(theta) {
312
+ validateNumeric(theta, 'theta');
290
313
  const t = D(theta);
291
- const c = new Decimal(Math.cos(t.toNumber()));
292
- const s = new Decimal(Math.sin(t.toNumber()));
314
+ const tNum = t.toNumber();
315
+ if (!Number.isFinite(tNum)) {
316
+ throw new Error(`theta must produce a finite angle, got ${theta}`);
317
+ }
318
+ const c = new Decimal(Math.cos(tNum));
319
+ const s = new Decimal(Math.sin(tNum));
293
320
  return Matrix.from([
294
321
  [c, s.negated(), new Decimal(0), new Decimal(0)],
295
322
  [s, c, new Decimal(0), new Decimal(0)],
@@ -366,8 +393,12 @@ export function rotateAroundAxis(ux, uy, uz, theta) {
366
393
  u[2] = u[2].div(norm);
367
394
 
368
395
  const t = D(theta);
369
- const c = new Decimal(Math.cos(t.toNumber()));
370
- const s = new Decimal(Math.sin(t.toNumber()));
396
+ const tNum = t.toNumber();
397
+ if (!Number.isFinite(tNum)) {
398
+ throw new Error(`theta must produce a finite angle, got ${theta}`);
399
+ }
400
+ const c = new Decimal(Math.cos(tNum));
401
+ const s = new Decimal(Math.sin(tNum));
371
402
  const one = new Decimal(1);
372
403
 
373
404
  // Rodrigues' rotation formula components
@@ -435,6 +466,13 @@ export function rotateAroundAxis(ux, uy, uz, theta) {
435
466
  * // Complex rotation around axis (1,1,1) passing through (10,20,30)
436
467
  */
437
468
  export function rotateAroundPoint(ux, uy, uz, theta, px, py, pz) {
469
+ validateNumeric(ux, 'ux');
470
+ validateNumeric(uy, 'uy');
471
+ validateNumeric(uz, 'uz');
472
+ validateNumeric(theta, 'theta');
473
+ validateNumeric(px, 'px');
474
+ validateNumeric(py, 'py');
475
+ validateNumeric(pz, 'pz');
438
476
  const pxD = D(px),
439
477
  pyD = D(py),
440
478
  pzD = D(pz);
@@ -489,13 +527,27 @@ export function rotateAroundPoint(ux, uy, uz, theta, px, py, pz) {
489
527
  * const transformed = vertices.map(([x,y,z]) => applyTransform(R, x, y, z));
490
528
  */
491
529
  export function applyTransform(M, x, y, z) {
530
+ if (!(M instanceof Matrix)) {
531
+ throw new Error('M must be a Matrix instance');
532
+ }
533
+ if (M.rows !== 4 || M.cols !== 4) {
534
+ throw new Error(`M must be a 4x4 matrix, got ${M.rows}x${M.cols}`);
535
+ }
536
+ validateNumeric(x, 'x');
537
+ validateNumeric(y, 'y');
538
+ validateNumeric(z, 'z');
539
+
492
540
  const P = Matrix.from([[D(x)], [D(y)], [D(z)], [new Decimal(1)]]);
493
541
  const R = M.mul(P);
494
542
  const rx = R.data[0][0],
495
543
  ry = R.data[1][0],
496
544
  rz = R.data[2][0],
497
545
  rw = R.data[3][0];
498
- // Perspective division (for affine transforms, rw is always 1)
546
+
547
+ if (rw.isZero()) {
548
+ throw new Error('Perspective division by zero: transformation results in point at infinity');
549
+ }
550
+
499
551
  return [rx.div(rw), ry.div(rw), rz.div(rw)];
500
552
  }
501
553
 
@@ -35,6 +35,19 @@ Decimal.set({ precision: 80 });
35
35
  * @returns {boolean} True if circular reference detected
36
36
  */
37
37
  function hasCircularReference(startId, getNextId, maxDepth = 100) {
38
+ // Parameter validation: startId must be a non-empty string
39
+ if (!startId || typeof startId !== 'string') {
40
+ throw new Error('hasCircularReference: startId must be a non-empty string');
41
+ }
42
+ // Parameter validation: getNextId must be a function
43
+ if (typeof getNextId !== 'function') {
44
+ throw new Error('hasCircularReference: getNextId must be a function');
45
+ }
46
+ // Parameter validation: maxDepth must be a positive finite number
47
+ if (typeof maxDepth !== 'number' || maxDepth <= 0 || !isFinite(maxDepth)) {
48
+ throw new Error('hasCircularReference: maxDepth must be a positive finite number');
49
+ }
50
+
38
51
  const visited = new Set();
39
52
  let currentId = startId;
40
53
  let depth = 0;
@@ -86,21 +99,49 @@ function hasCircularReference(startId, getNextId, maxDepth = 100) {
86
99
  * // }
87
100
  */
88
101
  export function parseUseElement(useElement) {
102
+ // Parameter validation: useElement must be defined
103
+ if (!useElement) throw new Error('parseUseElement: useElement is required');
104
+
89
105
  const href =
90
106
  useElement.getAttribute("href") ||
91
107
  useElement.getAttribute("xlink:href") ||
92
108
  "";
93
109
 
110
+ const parsedHref = href.startsWith("#") ? href.slice(1) : href;
111
+
112
+ // Parse numeric attributes and validate for NaN
113
+ const x = parseFloat(useElement.getAttribute("x") || "0");
114
+ const y = parseFloat(useElement.getAttribute("y") || "0");
115
+
116
+ // Validate that x and y are not NaN
117
+ if (isNaN(x) || isNaN(y)) {
118
+ throw new Error('parseUseElement: x and y attributes must be valid numbers');
119
+ }
120
+
121
+ // Parse width and height if present, validate for NaN
122
+ let width = null;
123
+ let height = null;
124
+
125
+ if (useElement.getAttribute("width")) {
126
+ width = parseFloat(useElement.getAttribute("width"));
127
+ if (isNaN(width)) {
128
+ throw new Error('parseUseElement: width attribute must be a valid number');
129
+ }
130
+ }
131
+
132
+ if (useElement.getAttribute("height")) {
133
+ height = parseFloat(useElement.getAttribute("height"));
134
+ if (isNaN(height)) {
135
+ throw new Error('parseUseElement: height attribute must be a valid number');
136
+ }
137
+ }
138
+
94
139
  return {
95
- href: href.startsWith("#") ? href.slice(1) : href,
96
- x: parseFloat(useElement.getAttribute("x") || "0"),
97
- y: parseFloat(useElement.getAttribute("y") || "0"),
98
- width: useElement.getAttribute("width")
99
- ? parseFloat(useElement.getAttribute("width"))
100
- : null,
101
- height: useElement.getAttribute("height")
102
- ? parseFloat(useElement.getAttribute("height"))
103
- : null,
140
+ href: parsedHref,
141
+ x,
142
+ y,
143
+ width,
144
+ height,
104
145
  transform: useElement.getAttribute("transform") || null,
105
146
  style: extractStyleAttributes(useElement),
106
147
  };
@@ -146,14 +187,25 @@ export function parseUseElement(useElement) {
146
187
  * // }
147
188
  */
148
189
  export function parseSymbolElement(symbolElement) {
190
+ // Parameter validation: symbolElement must be defined
191
+ if (!symbolElement) throw new Error('parseSymbolElement: symbolElement is required');
192
+
193
+ // Parse refX and refY with NaN validation
194
+ const refX = parseFloat(symbolElement.getAttribute("refX") || "0");
195
+ const refY = parseFloat(symbolElement.getAttribute("refY") || "0");
196
+
197
+ if (isNaN(refX) || isNaN(refY)) {
198
+ throw new Error('parseSymbolElement: refX and refY must be valid numbers');
199
+ }
200
+
149
201
  const data = {
150
202
  id: symbolElement.getAttribute("id") || "",
151
203
  viewBox: symbolElement.getAttribute("viewBox") || null,
152
204
  preserveAspectRatio:
153
205
  symbolElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
154
206
  children: [],
155
- refX: parseFloat(symbolElement.getAttribute("refX") || "0"),
156
- refY: parseFloat(symbolElement.getAttribute("refY") || "0"),
207
+ refX,
208
+ refY,
157
209
  };
158
210
 
159
211
  // Parse viewBox
@@ -162,7 +214,8 @@ export function parseSymbolElement(symbolElement) {
162
214
  .trim()
163
215
  .split(/[\s,]+/)
164
216
  .map(Number);
165
- if (parts.length === 4) {
217
+ // Validate viewBox has exactly 4 parts and all are valid numbers
218
+ if (parts.length === 4 && parts.every((p) => !isNaN(p) && isFinite(p))) {
166
219
  data.viewBoxParsed = {
167
220
  x: parts[0],
168
221
  y: parts[1],
@@ -242,6 +295,11 @@ export function parseSymbolElement(symbolElement) {
242
295
  * // }
243
296
  */
244
297
  export function parseChildElement(element) {
298
+ // Parameter validation: element must be defined and have a tagName
299
+ if (!element || !element.tagName) {
300
+ throw new Error('parseChildElement: element with tagName is required');
301
+ }
302
+
245
303
  const tagName = element.tagName.toLowerCase();
246
304
 
247
305
  const data = {
@@ -251,25 +309,34 @@ export function parseChildElement(element) {
251
309
  style: extractStyleAttributes(element),
252
310
  };
253
311
 
312
+ // Helper to safely parse float with NaN check
313
+ const safeParseFloat = (attrName, defaultValue = "0") => {
314
+ const value = parseFloat(element.getAttribute(attrName) || defaultValue);
315
+ if (isNaN(value)) {
316
+ throw new Error(`parseChildElement: ${attrName} must be a valid number in ${tagName} element`);
317
+ }
318
+ return value;
319
+ };
320
+
254
321
  switch (tagName) {
255
322
  case "rect":
256
- data.x = parseFloat(element.getAttribute("x") || "0");
257
- data.y = parseFloat(element.getAttribute("y") || "0");
258
- data.width = parseFloat(element.getAttribute("width") || "0");
259
- data.height = parseFloat(element.getAttribute("height") || "0");
260
- data.rx = parseFloat(element.getAttribute("rx") || "0");
261
- data.ry = parseFloat(element.getAttribute("ry") || "0");
323
+ data.x = safeParseFloat("x");
324
+ data.y = safeParseFloat("y");
325
+ data.width = safeParseFloat("width");
326
+ data.height = safeParseFloat("height");
327
+ data.rx = safeParseFloat("rx");
328
+ data.ry = safeParseFloat("ry");
262
329
  break;
263
330
  case "circle":
264
- data.cx = parseFloat(element.getAttribute("cx") || "0");
265
- data.cy = parseFloat(element.getAttribute("cy") || "0");
266
- data.r = parseFloat(element.getAttribute("r") || "0");
331
+ data.cx = safeParseFloat("cx");
332
+ data.cy = safeParseFloat("cy");
333
+ data.r = safeParseFloat("r");
267
334
  break;
268
335
  case "ellipse":
269
- data.cx = parseFloat(element.getAttribute("cx") || "0");
270
- data.cy = parseFloat(element.getAttribute("cy") || "0");
271
- data.rx = parseFloat(element.getAttribute("rx") || "0");
272
- data.ry = parseFloat(element.getAttribute("ry") || "0");
336
+ data.cx = safeParseFloat("cx");
337
+ data.cy = safeParseFloat("cy");
338
+ data.rx = safeParseFloat("rx");
339
+ data.ry = safeParseFloat("ry");
273
340
  break;
274
341
  case "path":
275
342
  data.d = element.getAttribute("d") || "";
@@ -281,10 +348,10 @@ export function parseChildElement(element) {
281
348
  data.points = element.getAttribute("points") || "";
282
349
  break;
283
350
  case "line":
284
- data.x1 = parseFloat(element.getAttribute("x1") || "0");
285
- data.y1 = parseFloat(element.getAttribute("y1") || "0");
286
- data.x2 = parseFloat(element.getAttribute("x2") || "0");
287
- data.y2 = parseFloat(element.getAttribute("y2") || "0");
351
+ data.x1 = safeParseFloat("x1");
352
+ data.y1 = safeParseFloat("y1");
353
+ data.x2 = safeParseFloat("x2");
354
+ data.y2 = safeParseFloat("y2");
288
355
  break;
289
356
  case "g":
290
357
  data.children = [];
@@ -298,15 +365,18 @@ export function parseChildElement(element) {
298
365
  element.getAttribute("xlink:href") ||
299
366
  ""
300
367
  ).replace("#", "");
301
- data.x = parseFloat(element.getAttribute("x") || "0");
302
- data.y = parseFloat(element.getAttribute("y") || "0");
368
+ data.x = safeParseFloat("x");
369
+ data.y = safeParseFloat("y");
370
+ // Width and height can be null
303
371
  data.width = element.getAttribute("width")
304
- ? parseFloat(element.getAttribute("width"))
372
+ ? safeParseFloat("width")
305
373
  : null;
306
374
  data.height = element.getAttribute("height")
307
- ? parseFloat(element.getAttribute("height"))
375
+ ? safeParseFloat("height")
308
376
  : null;
309
377
  break;
378
+ default:
379
+ break;
310
380
  }
311
381
 
312
382
  return data;
@@ -351,6 +421,11 @@ export function parseChildElement(element) {
351
421
  * // }
352
422
  */
353
423
  export function extractStyleAttributes(element) {
424
+ // Parameter validation: element must be defined
425
+ if (!element) {
426
+ throw new Error('extractStyleAttributes: element is required');
427
+ }
428
+
354
429
  return {
355
430
  fill: element.getAttribute("fill"),
356
431
  stroke: element.getAttribute("stroke"),
@@ -416,16 +491,23 @@ export function calculateViewBoxTransform(
416
491
  targetHeight,
417
492
  preserveAspectRatio = "xMidYMid meet",
418
493
  ) {
494
+ // Parameter validation: viewBox must have required properties
419
495
  if (!viewBox || !targetWidth || !targetHeight) {
420
496
  return Matrix.identity(3);
421
497
  }
422
498
 
499
+ // Validate targetWidth and targetHeight are finite positive numbers
500
+ if (!isFinite(targetWidth) || !isFinite(targetHeight) || targetWidth <= 0 || targetHeight <= 0) {
501
+ return Matrix.identity(3);
502
+ }
503
+
423
504
  const vbW = viewBox.width;
424
505
  const vbH = viewBox.height;
425
506
  const vbX = viewBox.x;
426
507
  const vbY = viewBox.y;
427
508
 
428
- if (vbW <= 0 || vbH <= 0) {
509
+ // Validate viewBox dimensions are finite and positive
510
+ if (!isFinite(vbW) || !isFinite(vbH) || !isFinite(vbX) || !isFinite(vbY) || vbW <= 0 || vbH <= 0) {
429
511
  return Matrix.identity(3);
430
512
  }
431
513
 
@@ -549,15 +631,59 @@ export function calculateViewBoxTransform(
549
631
  * // Recursively resolves ref1 → shape, composing transforms
550
632
  */
551
633
  export function resolveUse(useData, defs, options = {}) {
634
+ // Parameter validation: useData must be defined with href property
635
+ if (!useData || !useData.href) {
636
+ throw new Error('resolveUse: useData with href property is required');
637
+ }
638
+
639
+ // Parameter validation: defs must be defined
640
+ if (!defs) {
641
+ throw new Error('resolveUse: defs map is required');
642
+ }
643
+
644
+ // Validate useData.x and useData.y are valid numbers
645
+ if (typeof useData.x !== 'number' || isNaN(useData.x) || !isFinite(useData.x)) {
646
+ throw new Error('resolveUse: useData.x must be a valid finite number');
647
+ }
648
+ if (typeof useData.y !== 'number' || isNaN(useData.y) || !isFinite(useData.y)) {
649
+ throw new Error('resolveUse: useData.y must be a valid finite number');
650
+ }
651
+
652
+ // Validate useData.width and useData.height are null or valid numbers
653
+ if (useData.width !== null && (typeof useData.width !== 'number' || isNaN(useData.width) || !isFinite(useData.width) || useData.width <= 0)) {
654
+ throw new Error('resolveUse: useData.width must be null or a positive finite number');
655
+ }
656
+ if (useData.height !== null && (typeof useData.height !== 'number' || isNaN(useData.height) || !isFinite(useData.height) || useData.height <= 0)) {
657
+ throw new Error('resolveUse: useData.height must be null or a positive finite number');
658
+ }
659
+
660
+ // Validate useData.style is an object or null/undefined
661
+ if (useData.style !== null && useData.style !== undefined) {
662
+ if (typeof useData.style !== 'object' || Array.isArray(useData.style)) {
663
+ throw new Error('resolveUse: useData.style must be null, undefined, or a valid non-array object');
664
+ }
665
+ }
666
+
667
+ // Validate useData.transform is a string or null/undefined
668
+ if (useData.transform !== null && useData.transform !== undefined && typeof useData.transform !== 'string') {
669
+ throw new Error('resolveUse: useData.transform must be null, undefined, or a string');
670
+ }
671
+
672
+ // Validate options parameter
673
+ if (options && typeof options !== 'object') {
674
+ throw new Error('resolveUse: options must be an object or undefined');
675
+ }
676
+
552
677
  const { maxDepth = 10 } = options;
553
678
 
554
- if (maxDepth <= 0) {
555
- return null; // Prevent infinite recursion
679
+ // Validate maxDepth is a positive finite number
680
+ if (typeof maxDepth !== 'number' || maxDepth <= 0 || !isFinite(maxDepth)) {
681
+ throw new Error('resolveUse: maxDepth must be a positive finite number');
556
682
  }
557
683
 
558
684
  const target = defs[useData.href];
559
685
  if (!target) {
560
- return null;
686
+ return null; // Target element not found
561
687
  }
562
688
 
563
689
  // CORRECT ORDER per SVG spec:
@@ -581,6 +707,11 @@ export function resolveUse(useData, defs, options = {}) {
581
707
  // Handle symbol with viewBox (step 3)
582
708
  // ViewBox transform applies LAST (after translation and useTransform)
583
709
  if (target.type === "symbol" && target.viewBoxParsed) {
710
+ // Validate viewBoxParsed has required properties
711
+ if (typeof target.viewBoxParsed.width !== 'number' || typeof target.viewBoxParsed.height !== 'number') {
712
+ throw new Error('resolveUse: target.viewBoxParsed must have valid width and height properties');
713
+ }
714
+
584
715
  const width = useData.width || target.viewBoxParsed.width;
585
716
  const height = useData.height || target.viewBoxParsed.height;
586
717
 
@@ -588,7 +719,7 @@ export function resolveUse(useData, defs, options = {}) {
588
719
  target.viewBoxParsed,
589
720
  width,
590
721
  height,
591
- target.preserveAspectRatio,
722
+ target.preserveAspectRatio || "xMidYMid meet",
592
723
  );
593
724
 
594
725
  // ViewBox transform is applied LAST, so it's the leftmost in multiplication
@@ -677,9 +808,25 @@ export function resolveUse(useData, defs, options = {}) {
677
808
  export function flattenResolvedUse(resolved, samples = 20) {
678
809
  const results = [];
679
810
 
811
+ // Edge case: resolved is null or undefined
680
812
  if (!resolved) return results;
681
813
 
814
+ // Parameter validation: samples must be a positive number
815
+ if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
816
+ throw new Error('flattenResolvedUse: samples must be a positive finite number');
817
+ }
818
+
819
+ // Validate required properties exist
820
+ if (!resolved.children || !resolved.transform) {
821
+ return results;
822
+ }
823
+
682
824
  for (const child of resolved.children) {
825
+ // Validate child has required properties
826
+ if (!child || !child.transform) {
827
+ continue;
828
+ }
829
+
683
830
  const childTransform = resolved.transform.mul(child.transform);
684
831
  const element = child.element;
685
832
 
@@ -747,10 +894,20 @@ export function flattenResolvedUse(resolved, samples = 20) {
747
894
  * // [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 30 }, { x: 0, y: 30 }]
748
895
  */
749
896
  export function elementToPolygon(element, transform, samples = 20) {
897
+ // Parameter validation: element must be defined
898
+ if (!element) {
899
+ throw new Error('elementToPolygon: element is required');
900
+ }
901
+
902
+ // Parameter validation: samples must be a positive finite number
903
+ if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
904
+ throw new Error('elementToPolygon: samples must be a positive finite number');
905
+ }
906
+
750
907
  // Use ClipPathResolver's shapeToPolygon
751
908
  let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
752
909
 
753
- // Apply transform
910
+ // Apply transform if provided and polygon is not empty
754
911
  if (transform && polygon.length > 0) {
755
912
  polygon = polygon.map((p) => {
756
913
  const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
@@ -806,8 +963,21 @@ export function elementToPolygon(element, transform, samples = 20) {
806
963
  * // { fill: 'blue' }
807
964
  */
808
965
  export function mergeStyles(inherited, element) {
966
+ // Parameter validation: element must be defined (inherited can be null)
967
+ if (!element || typeof element !== 'object' || Array.isArray(element)) {
968
+ throw new Error('mergeStyles: element must be a valid non-array object');
969
+ }
970
+
809
971
  const result = { ...element };
810
972
 
973
+ // Inherited can be null/undefined, handle gracefully
974
+ // Also validate inherited is an object if not null/undefined
975
+ if (inherited !== null && inherited !== undefined) {
976
+ if (typeof inherited !== 'object' || Array.isArray(inherited)) {
977
+ throw new Error('mergeStyles: inherited must be null, undefined, or a valid non-array object');
978
+ }
979
+ }
980
+
811
981
  for (const [key, value] of Object.entries(inherited || {})) {
812
982
  // Inherit if value is not null and element doesn't have a value (null or undefined)
813
983
  if (value !== null && (result[key] === null || result[key] === undefined)) {
@@ -862,6 +1032,11 @@ export function mergeStyles(inherited, element) {
862
1032
  * // Axis-aligned bbox enclosing the rotated rectangle (wider than original)
863
1033
  */
864
1034
  export function getResolvedBBox(resolved, samples = 20) {
1035
+ // Parameter validation: samples must be a positive finite number
1036
+ if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
1037
+ throw new Error('getResolvedBBox: samples must be a positive finite number');
1038
+ }
1039
+
865
1040
  const polygons = flattenResolvedUse(resolved, samples);
866
1041
 
867
1042
  let minX = Infinity;
@@ -873,6 +1048,12 @@ export function getResolvedBBox(resolved, samples = 20) {
873
1048
  for (const p of polygon) {
874
1049
  const x = Number(p.x);
875
1050
  const y = Number(p.y);
1051
+
1052
+ // Skip NaN values to prevent corrupting bounding box
1053
+ if (isNaN(x) || isNaN(y)) {
1054
+ continue;
1055
+ }
1056
+
876
1057
  minX = Math.min(minX, x);
877
1058
  minY = Math.min(minY, y);
878
1059
  maxX = Math.max(maxX, x);
@@ -880,6 +1061,7 @@ export function getResolvedBBox(resolved, samples = 20) {
880
1061
  }
881
1062
  }
882
1063
 
1064
+ // Return empty bbox if no valid points found
883
1065
  if (minX === Infinity) {
884
1066
  return { x: 0, y: 0, width: 0, height: 0 };
885
1067
  }
@@ -940,6 +1122,21 @@ export function getResolvedBBox(resolved, samples = 20) {
940
1122
  * // May produce multiple disjoint polygons if use element spans outside triangle
941
1123
  */
942
1124
  export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
1125
+ // Parameter validation: clipPolygon must be defined and be an array
1126
+ if (!clipPolygon || !Array.isArray(clipPolygon)) {
1127
+ throw new Error('clipResolvedUse: clipPolygon must be a valid array');
1128
+ }
1129
+
1130
+ // Validate clipPolygon has at least 3 points
1131
+ if (clipPolygon.length < 3) {
1132
+ throw new Error('clipResolvedUse: clipPolygon must have at least 3 vertices');
1133
+ }
1134
+
1135
+ // Parameter validation: samples must be a positive finite number
1136
+ if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
1137
+ throw new Error('clipResolvedUse: samples must be a positive finite number');
1138
+ }
1139
+
943
1140
  const polygons = flattenResolvedUse(resolved, samples);
944
1141
  const result = [];
945
1142
 
@@ -1009,6 +1206,11 @@ export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
1009
1206
  * // Two closed subpaths (rectangle + circle)
1010
1207
  */
1011
1208
  export function resolvedUseToPathData(resolved, samples = 20) {
1209
+ // Parameter validation: samples must be a positive finite number
1210
+ if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
1211
+ throw new Error('resolvedUseToPathData: samples must be a positive finite number');
1212
+ }
1213
+
1012
1214
  const polygons = flattenResolvedUse(resolved, samples);
1013
1215
  const paths = [];
1014
1216
 
@@ -1017,9 +1219,15 @@ export function resolvedUseToPathData(resolved, samples = 20) {
1017
1219
  let d = "";
1018
1220
  for (let i = 0; i < polygon.length; i++) {
1019
1221
  const p = polygon[i];
1020
- const x = Number(p.x).toFixed(6);
1021
- const y = Number(p.y).toFixed(6);
1022
- d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
1222
+ const x = Number(p.x);
1223
+ const y = Number(p.y);
1224
+
1225
+ // Skip invalid points with NaN coordinates
1226
+ if (isNaN(x) || isNaN(y)) {
1227
+ continue;
1228
+ }
1229
+
1230
+ d += i === 0 ? `M ${x.toFixed(6)} ${y.toFixed(6)}` : ` L ${x.toFixed(6)} ${y.toFixed(6)}`;
1023
1231
  }
1024
1232
  d += " Z";
1025
1233
  paths.push(d);
@@ -1081,6 +1289,11 @@ export function resolvedUseToPathData(resolved, samples = 20) {
1081
1289
  * const resolved = resolveUse(useData, defs);
1082
1290
  */
1083
1291
  export function buildDefsMap(svgRoot) {
1292
+ // Parameter validation: svgRoot must be defined and have querySelectorAll method
1293
+ if (!svgRoot || typeof svgRoot.querySelectorAll !== 'function') {
1294
+ throw new Error('buildDefsMap: svgRoot must be a valid DOM element');
1295
+ }
1296
+
1084
1297
  const defs = {};
1085
1298
 
1086
1299
  // Find all elements with id
@@ -1088,6 +1301,17 @@ export function buildDefsMap(svgRoot) {
1088
1301
 
1089
1302
  for (const element of elementsWithId) {
1090
1303
  const id = element.getAttribute("id");
1304
+
1305
+ // Skip elements without a valid id
1306
+ if (!id) {
1307
+ continue;
1308
+ }
1309
+
1310
+ // Validate element has tagName
1311
+ if (!element.tagName) {
1312
+ continue;
1313
+ }
1314
+
1091
1315
  const tagName = element.tagName.toLowerCase();
1092
1316
 
1093
1317
  if (tagName === "symbol") {
@@ -1166,12 +1390,27 @@ export function buildDefsMap(svgRoot) {
1166
1390
  * }
1167
1391
  */
1168
1392
  export function resolveAllUses(svgRoot, options = {}) {
1393
+ // Parameter validation: svgRoot must be defined and have querySelectorAll method
1394
+ if (!svgRoot || typeof svgRoot.querySelectorAll !== 'function') {
1395
+ throw new Error('resolveAllUses: svgRoot must be a valid DOM element');
1396
+ }
1397
+
1398
+ // Validate options parameter
1399
+ if (options && typeof options !== 'object') {
1400
+ throw new Error('resolveAllUses: options must be an object or undefined');
1401
+ }
1402
+
1169
1403
  const defs = buildDefsMap(svgRoot);
1170
1404
  const useElements = svgRoot.querySelectorAll("use");
1171
1405
  const resolved = [];
1172
1406
 
1173
1407
  // Helper to get the next use reference from a definition
1174
1408
  const getUseRef = (id) => {
1409
+ // Validate id parameter
1410
+ if (!id || typeof id !== 'string') {
1411
+ return null;
1412
+ }
1413
+
1175
1414
  const target = defs[id];
1176
1415
  if (!target) return null;
1177
1416
 
@@ -1195,6 +1434,11 @@ export function resolveAllUses(svgRoot, options = {}) {
1195
1434
  for (const useEl of useElements) {
1196
1435
  const useData = parseUseElement(useEl);
1197
1436
 
1437
+ // Skip use elements without valid href
1438
+ if (!useData.href) {
1439
+ continue;
1440
+ }
1441
+
1198
1442
  // Check for circular reference before attempting to resolve
1199
1443
  if (hasCircularReference(useData.href, getUseRef)) {
1200
1444
  console.warn(