@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
@@ -71,6 +71,15 @@ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
71
71
  * // }
72
72
  */
73
73
  export function parsePatternElement(patternElement) {
74
+ // Validate patternElement is a DOM element with getAttribute method
75
+ if (!patternElement)
76
+ throw new Error("parsePatternElement: patternElement is required");
77
+ if (typeof patternElement.getAttribute !== "function") {
78
+ throw new Error(
79
+ "parsePatternElement: patternElement must be a DOM element with getAttribute method",
80
+ );
81
+ }
82
+
74
83
  const data = {
75
84
  id: patternElement.getAttribute("id") || "",
76
85
  patternUnits:
@@ -92,11 +101,18 @@ export function parsePatternElement(patternElement) {
92
101
  children: [],
93
102
  };
94
103
 
104
+ // Helper to parse numeric values with NaN validation
105
+ const parseValidFloat = (val, defaultVal) => {
106
+ if (val === null || val === undefined) return defaultVal;
107
+ const parsed = parseFloat(val);
108
+ return isNaN(parsed) ? defaultVal : parsed;
109
+ };
110
+
95
111
  // Parse numeric values
96
- data.x = data.x !== null ? parseFloat(data.x) : 0;
97
- data.y = data.y !== null ? parseFloat(data.y) : 0;
98
- data.width = data.width !== null ? parseFloat(data.width) : 0;
99
- data.height = data.height !== null ? parseFloat(data.height) : 0;
112
+ data.x = parseValidFloat(data.x, 0);
113
+ data.y = parseValidFloat(data.y, 0);
114
+ data.width = parseValidFloat(data.width, 0);
115
+ data.height = parseValidFloat(data.height, 0);
100
116
 
101
117
  // Parse viewBox if present
102
118
  if (data.viewBox) {
@@ -104,7 +120,8 @@ export function parsePatternElement(patternElement) {
104
120
  .trim()
105
121
  .split(/[\s,]+/)
106
122
  .map(Number);
107
- if (parts.length === 4) {
123
+ // Validate all parts are valid numbers
124
+ if (parts.length === 4 && parts.every((p) => !isNaN(p) && isFinite(p))) {
108
125
  data.viewBoxParsed = {
109
126
  x: parts[0],
110
127
  y: parts[1],
@@ -114,73 +131,94 @@ export function parsePatternElement(patternElement) {
114
131
  }
115
132
  }
116
133
 
117
- // Parse child elements
118
- for (const child of patternElement.children) {
119
- const tagName = child.tagName.toLowerCase();
120
- const childData = {
121
- type: tagName,
122
- fill: child.getAttribute("fill") || "black",
123
- stroke: child.getAttribute("stroke") || "none",
124
- strokeWidth: parseFloat(child.getAttribute("stroke-width") || "1"),
125
- opacity: parseFloat(child.getAttribute("opacity") || "1"),
126
- transform: child.getAttribute("transform") || null,
127
- };
134
+ // Parse child elements - validate children exists and is iterable
135
+ if (
136
+ patternElement.children &&
137
+ typeof patternElement.children[Symbol.iterator] === "function"
138
+ ) {
139
+ for (const child of patternElement.children) {
140
+ // Validate child is an Element before accessing properties
141
+ if (!child || typeof child.getAttribute !== "function") continue;
142
+
143
+ const tagName = child.tagName ? child.tagName.toLowerCase() : "";
144
+ if (!tagName) continue;
145
+
146
+ const childData = {
147
+ type: tagName,
148
+ fill: child.getAttribute("fill") || "black",
149
+ stroke: child.getAttribute("stroke") || "none",
150
+ strokeWidth: parseValidFloat(child.getAttribute("stroke-width"), 1),
151
+ opacity: parseValidFloat(child.getAttribute("opacity"), 1),
152
+ transform: child.getAttribute("transform") || null,
153
+ };
128
154
 
129
- // Parse shape-specific attributes
130
- switch (tagName) {
131
- case "rect":
132
- childData.x = parseFloat(child.getAttribute("x") || "0");
133
- childData.y = parseFloat(child.getAttribute("y") || "0");
134
- childData.width = parseFloat(child.getAttribute("width") || "0");
135
- childData.height = parseFloat(child.getAttribute("height") || "0");
136
- childData.rx = parseFloat(child.getAttribute("rx") || "0");
137
- childData.ry = parseFloat(child.getAttribute("ry") || "0");
138
- break;
139
- case "circle":
140
- childData.cx = parseFloat(child.getAttribute("cx") || "0");
141
- childData.cy = parseFloat(child.getAttribute("cy") || "0");
142
- childData.r = parseFloat(child.getAttribute("r") || "0");
143
- break;
144
- case "ellipse":
145
- childData.cx = parseFloat(child.getAttribute("cx") || "0");
146
- childData.cy = parseFloat(child.getAttribute("cy") || "0");
147
- childData.rx = parseFloat(child.getAttribute("rx") || "0");
148
- childData.ry = parseFloat(child.getAttribute("ry") || "0");
149
- break;
150
- case "path":
151
- childData.d = child.getAttribute("d") || "";
152
- break;
153
- case "polygon":
154
- childData.points = child.getAttribute("points") || "";
155
- break;
156
- case "polyline":
157
- childData.points = child.getAttribute("points") || "";
158
- break;
159
- case "line":
160
- childData.x1 = parseFloat(child.getAttribute("x1") || "0");
161
- childData.y1 = parseFloat(child.getAttribute("y1") || "0");
162
- childData.x2 = parseFloat(child.getAttribute("x2") || "0");
163
- childData.y2 = parseFloat(child.getAttribute("y2") || "0");
164
- break;
165
- case "use":
166
- childData.href =
167
- child.getAttribute("href") || child.getAttribute("xlink:href") || "";
168
- childData.x = parseFloat(child.getAttribute("x") || "0");
169
- childData.y = parseFloat(child.getAttribute("y") || "0");
170
- break;
171
- case "g":
172
- // Groups can contain nested shapes
173
- childData.children = [];
174
- for (const gc of child.children) {
175
- childData.children.push({
176
- type: gc.tagName.toLowerCase(),
177
- fill: gc.getAttribute("fill") || "inherit",
178
- });
179
- }
180
- break;
181
- }
155
+ // Parse shape-specific attributes
156
+ switch (tagName) {
157
+ case "rect":
158
+ childData.x = parseValidFloat(child.getAttribute("x"), 0);
159
+ childData.y = parseValidFloat(child.getAttribute("y"), 0);
160
+ childData.width = parseValidFloat(child.getAttribute("width"), 0);
161
+ childData.height = parseValidFloat(child.getAttribute("height"), 0);
162
+ childData.rx = parseValidFloat(child.getAttribute("rx"), 0);
163
+ childData.ry = parseValidFloat(child.getAttribute("ry"), 0);
164
+ break;
165
+ case "circle":
166
+ childData.cx = parseValidFloat(child.getAttribute("cx"), 0);
167
+ childData.cy = parseValidFloat(child.getAttribute("cy"), 0);
168
+ childData.r = parseValidFloat(child.getAttribute("r"), 0);
169
+ break;
170
+ case "ellipse":
171
+ childData.cx = parseValidFloat(child.getAttribute("cx"), 0);
172
+ childData.cy = parseValidFloat(child.getAttribute("cy"), 0);
173
+ childData.rx = parseValidFloat(child.getAttribute("rx"), 0);
174
+ childData.ry = parseValidFloat(child.getAttribute("ry"), 0);
175
+ break;
176
+ case "path":
177
+ childData.d = child.getAttribute("d") || "";
178
+ break;
179
+ case "polygon":
180
+ childData.points = child.getAttribute("points") || "";
181
+ break;
182
+ case "polyline":
183
+ childData.points = child.getAttribute("points") || "";
184
+ break;
185
+ case "line":
186
+ childData.x1 = parseValidFloat(child.getAttribute("x1"), 0);
187
+ childData.y1 = parseValidFloat(child.getAttribute("y1"), 0);
188
+ childData.x2 = parseValidFloat(child.getAttribute("x2"), 0);
189
+ childData.y2 = parseValidFloat(child.getAttribute("y2"), 0);
190
+ break;
191
+ case "use":
192
+ childData.href =
193
+ child.getAttribute("href") ||
194
+ child.getAttribute("xlink:href") ||
195
+ "";
196
+ childData.x = parseValidFloat(child.getAttribute("x"), 0);
197
+ childData.y = parseValidFloat(child.getAttribute("y"), 0);
198
+ break;
199
+ case "g":
200
+ // Groups can contain nested shapes - validate children exists
201
+ childData.children = [];
202
+ if (
203
+ child.children &&
204
+ typeof child.children[Symbol.iterator] === "function"
205
+ ) {
206
+ for (const gc of child.children) {
207
+ if (gc && gc.tagName && typeof gc.getAttribute === "function") {
208
+ childData.children.push({
209
+ type: gc.tagName.toLowerCase(),
210
+ fill: gc.getAttribute("fill") || "inherit",
211
+ });
212
+ }
213
+ }
214
+ }
215
+ break;
216
+ default:
217
+ break;
218
+ }
182
219
 
183
- data.children.push(childData);
220
+ data.children.push(childData);
221
+ }
184
222
  }
185
223
 
186
224
  return data;
@@ -233,6 +271,36 @@ export function parsePatternElement(patternElement) {
233
271
  * // Tile uses absolute coordinates, bbox is ignored
234
272
  */
235
273
  export function getPatternTile(patternData, targetBBox) {
274
+ // Validate parameters
275
+ if (!patternData) {
276
+ throw new Error("getPatternTile: patternData is required");
277
+ }
278
+ if (!targetBBox) {
279
+ throw new Error("getPatternTile: targetBBox is required");
280
+ }
281
+ // Validate targetBBox has required numeric properties
282
+ if (
283
+ typeof targetBBox.x !== "number" ||
284
+ typeof targetBBox.y !== "number" ||
285
+ typeof targetBBox.width !== "number" ||
286
+ typeof targetBBox.height !== "number"
287
+ ) {
288
+ throw new Error(
289
+ "getPatternTile: targetBBox must have numeric x, y, width, and height properties",
290
+ );
291
+ }
292
+ // Validate patternData has required numeric properties
293
+ if (
294
+ typeof patternData.x !== "number" ||
295
+ typeof patternData.y !== "number" ||
296
+ typeof patternData.width !== "number" ||
297
+ typeof patternData.height !== "number"
298
+ ) {
299
+ throw new Error(
300
+ "getPatternTile: patternData must have numeric x, y, width, and height properties",
301
+ );
302
+ }
303
+
236
304
  if (patternData.patternUnits === "objectBoundingBox") {
237
305
  // Dimensions are fractions of target bbox
238
306
  return {
@@ -308,14 +376,61 @@ export function getPatternTile(patternData, targetBBox) {
308
376
  * // M translates to bbox origin and scales by bbox dimensions
309
377
  */
310
378
  export function getPatternContentTransform(patternData, tile, targetBBox) {
379
+ // Validate parameters
380
+ if (!patternData) {
381
+ throw new Error("getPatternContentTransform: patternData is required");
382
+ }
383
+ if (!tile) {
384
+ throw new Error("getPatternContentTransform: tile is required");
385
+ }
386
+ if (!targetBBox) {
387
+ throw new Error("getPatternContentTransform: targetBBox is required");
388
+ }
389
+ // Validate tile has required properties
390
+ if (!tile.width || !tile.height) {
391
+ throw new Error(
392
+ "getPatternContentTransform: tile must have width and height properties",
393
+ );
394
+ }
395
+ // Validate targetBBox has required numeric properties
396
+ if (
397
+ typeof targetBBox.x !== "number" ||
398
+ typeof targetBBox.y !== "number" ||
399
+ typeof targetBBox.width !== "number" ||
400
+ typeof targetBBox.height !== "number"
401
+ ) {
402
+ throw new Error(
403
+ "getPatternContentTransform: targetBBox must have numeric x, y, width, and height properties",
404
+ );
405
+ }
406
+
311
407
  let M = Matrix.identity(3);
312
408
 
313
409
  // Apply viewBox transform if present
314
410
  if (patternData.viewBoxParsed) {
315
411
  const vb = patternData.viewBoxParsed;
412
+ // Validate viewBox has required numeric properties
413
+ if (
414
+ typeof vb.x !== "number" ||
415
+ typeof vb.y !== "number" ||
416
+ typeof vb.width !== "number" ||
417
+ typeof vb.height !== "number"
418
+ ) {
419
+ throw new Error(
420
+ "getPatternContentTransform: viewBoxParsed must have numeric x, y, width, and height properties",
421
+ );
422
+ }
423
+
316
424
  const tileWidth = Number(tile.width);
317
425
  const tileHeight = Number(tile.height);
318
426
 
427
+ // Check for division by zero in viewBox dimensions
428
+ if (vb.width === 0 || vb.height === 0) {
429
+ throw new Error(
430
+ "getPatternContentTransform: viewBox width and height must be non-zero",
431
+ );
432
+ }
433
+
319
434
  // Scale from viewBox to tile
320
435
  const scaleX = tileWidth / vb.width;
321
436
  const scaleY = tileHeight / vb.height;
@@ -323,6 +438,13 @@ export function getPatternContentTransform(patternData, tile, targetBBox) {
323
438
  // For 'xMidYMid meet', use uniform scale
324
439
  const scale = Math.min(scaleX, scaleY);
325
440
 
441
+ // Validate scale is finite
442
+ if (!isFinite(scale)) {
443
+ throw new Error(
444
+ "getPatternContentTransform: computed scale is not finite",
445
+ );
446
+ }
447
+
326
448
  // Center the content
327
449
  const offsetX = (tileWidth - vb.width * scale) / 2;
328
450
  const offsetY = (tileHeight - vb.height * scale) / 2;
@@ -367,6 +489,20 @@ export function getPatternContentTransform(patternData, tile, targetBBox) {
367
489
  * // Returns rectangle vertices translated to (100, 50)
368
490
  */
369
491
  export function patternChildToPolygon(child, transform = null, samples = 20) {
492
+ // Validate child parameter
493
+ if (!child) {
494
+ throw new Error("patternChildToPolygon: child is required");
495
+ }
496
+ if (!child.type) {
497
+ throw new Error("patternChildToPolygon: child must have a type property");
498
+ }
499
+ // Validate samples is a positive number
500
+ if (typeof samples !== "number" || samples <= 0 || !isFinite(samples)) {
501
+ throw new Error(
502
+ "patternChildToPolygon: samples must be a positive finite number",
503
+ );
504
+ }
505
+
370
506
  // Create element-like object for ClipPathResolver
371
507
  const element = {
372
508
  type: child.type,
@@ -377,9 +513,20 @@ export function patternChildToPolygon(child, transform = null, samples = 20) {
377
513
  // Get polygon using ClipPathResolver
378
514
  let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
379
515
 
516
+ // Validate polygon is an array
517
+ if (!Array.isArray(polygon)) {
518
+ return [];
519
+ }
520
+
380
521
  // Apply additional transform if provided
381
522
  if (transform && polygon.length > 0) {
382
523
  polygon = polygon.map((p) => {
524
+ // Validate point has x and y properties
525
+ if (!p || typeof p.x !== "number" || typeof p.y !== "number") {
526
+ throw new Error(
527
+ "patternChildToPolygon: invalid polygon point - must have numeric x and y properties",
528
+ );
529
+ }
383
530
  const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
384
531
  return { x, y };
385
532
  });
@@ -423,6 +570,36 @@ export function patternChildToPolygon(child, transform = null, samples = 20) {
423
570
  * // { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 100, y: 50 }, { x: 150, y: 50 }]
424
571
  */
425
572
  export function getTilePositions(tile, coverBBox) {
573
+ // Validate parameters
574
+ if (!tile) {
575
+ throw new Error("getTilePositions: tile is required");
576
+ }
577
+ if (!coverBBox) {
578
+ throw new Error("getTilePositions: coverBBox is required");
579
+ }
580
+ // Validate tile has required properties
581
+ if (
582
+ !("x" in tile) ||
583
+ !("y" in tile) ||
584
+ !("width" in tile) ||
585
+ !("height" in tile)
586
+ ) {
587
+ throw new Error(
588
+ "getTilePositions: tile must have x, y, width, and height properties",
589
+ );
590
+ }
591
+ // Validate coverBBox has required numeric properties
592
+ if (
593
+ typeof coverBBox.x !== "number" ||
594
+ typeof coverBBox.y !== "number" ||
595
+ typeof coverBBox.width !== "number" ||
596
+ typeof coverBBox.height !== "number"
597
+ ) {
598
+ throw new Error(
599
+ "getTilePositions: coverBBox must have numeric x, y, width, and height properties",
600
+ );
601
+ }
602
+
426
603
  const positions = [];
427
604
 
428
605
  const tileX = Number(tile.x);
@@ -430,6 +607,17 @@ export function getTilePositions(tile, coverBBox) {
430
607
  const tileW = Number(tile.width);
431
608
  const tileH = Number(tile.height);
432
609
 
610
+ // Validate tile dimensions are finite
611
+ if (
612
+ !isFinite(tileX) ||
613
+ !isFinite(tileY) ||
614
+ !isFinite(tileW) ||
615
+ !isFinite(tileH)
616
+ ) {
617
+ throw new Error("getTilePositions: tile dimensions must be finite numbers");
618
+ }
619
+
620
+ // Return empty array if tile dimensions are not positive
433
621
  if (tileW <= 0 || tileH <= 0) return positions;
434
622
 
435
623
  // Calculate start and end indices
@@ -438,6 +626,16 @@ export function getTilePositions(tile, coverBBox) {
438
626
  const startJ = Math.floor((coverBBox.y - tileY) / tileH);
439
627
  const endJ = Math.ceil((coverBBox.y + coverBBox.height - tileY) / tileH);
440
628
 
629
+ // Validate indices are finite
630
+ if (
631
+ !isFinite(startI) ||
632
+ !isFinite(endI) ||
633
+ !isFinite(startJ) ||
634
+ !isFinite(endJ)
635
+ ) {
636
+ throw new Error("getTilePositions: computed tile indices are not finite");
637
+ }
638
+
441
639
  for (let i = startI; i < endI; i++) {
442
640
  for (let j = startJ; j < endJ; j++) {
443
641
  positions.push({
@@ -488,6 +686,18 @@ export function getTilePositions(tile, coverBBox) {
488
686
  * // Each polygon can be rendered with its associated fill and stroke
489
687
  */
490
688
  export function resolvePattern(patternData, targetBBox, options = {}) {
689
+ // Validate parameters
690
+ if (!patternData) {
691
+ throw new Error("resolvePattern: patternData is required");
692
+ }
693
+ if (!targetBBox) {
694
+ throw new Error("resolvePattern: targetBBox is required");
695
+ }
696
+ // Validate patternData has children array
697
+ if (!Array.isArray(patternData.children)) {
698
+ throw new Error("resolvePattern: patternData.children must be an array");
699
+ }
700
+
491
701
  const { samples = 20, maxTiles = 1000 } = options;
492
702
  const result = [];
493
703
 
@@ -578,6 +788,20 @@ export function applyPattern(
578
788
  targetBBox,
579
789
  options = {},
580
790
  ) {
791
+ // Validate parameters
792
+ if (!targetPolygon) {
793
+ throw new Error("applyPattern: targetPolygon is required");
794
+ }
795
+ if (!Array.isArray(targetPolygon)) {
796
+ throw new Error("applyPattern: targetPolygon must be an array");
797
+ }
798
+ if (!patternData) {
799
+ throw new Error("applyPattern: patternData is required");
800
+ }
801
+ if (!targetBBox) {
802
+ throw new Error("applyPattern: targetBBox is required");
803
+ }
804
+
581
805
  const patternPolygons = resolvePattern(patternData, targetBBox, options);
582
806
  const result = [];
583
807
 
@@ -589,13 +813,16 @@ export function applyPattern(
589
813
  polygon,
590
814
  );
591
815
 
592
- for (const clippedPoly of intersection) {
593
- if (clippedPoly.length >= 3) {
594
- result.push({
595
- polygon: clippedPoly,
596
- fill,
597
- opacity,
598
- });
816
+ // Validate intersection is an array
817
+ if (Array.isArray(intersection)) {
818
+ for (const clippedPoly of intersection) {
819
+ if (clippedPoly.length >= 3) {
820
+ result.push({
821
+ polygon: clippedPoly,
822
+ fill,
823
+ opacity,
824
+ });
825
+ }
599
826
  }
600
827
  }
601
828
  }
@@ -629,6 +856,14 @@ export function applyPattern(
629
856
  * // Returns single polygon that is the union of all dots in the pattern
630
857
  */
631
858
  export function patternToClipPath(patternData, targetBBox, options = {}) {
859
+ // Validate parameters
860
+ if (!patternData) {
861
+ throw new Error("patternToClipPath: patternData is required");
862
+ }
863
+ if (!targetBBox) {
864
+ throw new Error("patternToClipPath: targetBBox is required");
865
+ }
866
+
632
867
  const patternPolygons = resolvePattern(patternData, targetBBox, options);
633
868
 
634
869
  // Union all polygons
@@ -640,7 +875,13 @@ export function patternToClipPath(patternData, targetBBox, options = {}) {
640
875
  result = polygon;
641
876
  } else {
642
877
  const unionResult = PolygonClip.polygonUnion(result, polygon);
643
- if (unionResult.length > 0 && unionResult[0].length >= 3) {
878
+ // Validate unionResult is an array
879
+ if (
880
+ Array.isArray(unionResult) &&
881
+ unionResult.length > 0 &&
882
+ Array.isArray(unionResult[0]) &&
883
+ unionResult[0].length >= 3
884
+ ) {
644
885
  result = unionResult[0];
645
886
  }
646
887
  }
@@ -675,13 +916,38 @@ export function patternToClipPath(patternData, targetBBox, options = {}) {
675
916
  * // Can be used in: <path d={pathData} />
676
917
  */
677
918
  export function patternToPathData(patternData, targetBBox, options = {}) {
919
+ // Validate parameters
920
+ if (!patternData) {
921
+ throw new Error("patternToPathData: patternData is required");
922
+ }
923
+ if (!targetBBox) {
924
+ throw new Error("patternToPathData: targetBBox is required");
925
+ }
926
+
678
927
  const polygon = patternToClipPath(patternData, targetBBox, options);
679
928
 
929
+ // Validate polygon is an array
930
+ if (!Array.isArray(polygon)) {
931
+ return "";
932
+ }
933
+
680
934
  if (polygon.length < 3) return "";
681
935
 
682
936
  let d = "";
683
937
  for (let i = 0; i < polygon.length; i++) {
684
938
  const p = polygon[i];
939
+ // Validate point has x and y properties
940
+ if (!p || typeof p.x !== "number" || typeof p.y !== "number") {
941
+ throw new Error(
942
+ "patternToPathData: invalid polygon point - must have numeric x and y properties",
943
+ );
944
+ }
945
+ // Validate x and y are finite
946
+ if (!isFinite(p.x) || !isFinite(p.y)) {
947
+ throw new Error(
948
+ "patternToPathData: polygon point coordinates must be finite numbers",
949
+ );
950
+ }
685
951
  const x = Number(p.x).toFixed(6);
686
952
  const y = Number(p.y).toFixed(6);
687
953
  d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
@@ -714,18 +980,46 @@ export function patternToPathData(patternData, targetBBox, options = {}) {
714
980
  * // Result: { columns: 4, rows: 4, total: 16 }
715
981
  */
716
982
  export function getPatternTileCount(patternData, targetBBox) {
983
+ // Validate parameters
984
+ if (!patternData) {
985
+ throw new Error("getPatternTileCount: patternData is required");
986
+ }
987
+ if (!targetBBox) {
988
+ throw new Error("getPatternTileCount: targetBBox is required");
989
+ }
990
+ // Validate targetBBox has required numeric properties
991
+ if (
992
+ typeof targetBBox.width !== "number" ||
993
+ typeof targetBBox.height !== "number"
994
+ ) {
995
+ throw new Error(
996
+ "getPatternTileCount: targetBBox must have numeric width and height properties",
997
+ );
998
+ }
999
+
717
1000
  const tile = getPatternTile(patternData, targetBBox);
718
1001
 
719
1002
  const tileW = Number(tile.width);
720
1003
  const tileH = Number(tile.height);
721
1004
 
1005
+ // Check for zero or negative dimensions
722
1006
  if (tileW <= 0 || tileH <= 0) {
723
1007
  return { columns: 0, rows: 0, total: 0 };
724
1008
  }
725
1009
 
1010
+ // Check for zero targetBBox dimensions
1011
+ if (targetBBox.width === 0 || targetBBox.height === 0) {
1012
+ return { columns: 0, rows: 0, total: 0 };
1013
+ }
1014
+
726
1015
  const columns = Math.ceil(targetBBox.width / tileW);
727
1016
  const rows = Math.ceil(targetBBox.height / tileH);
728
1017
 
1018
+ // Validate computed values are finite
1019
+ if (!isFinite(columns) || !isFinite(rows)) {
1020
+ throw new Error("getPatternTileCount: computed tile counts are not finite");
1021
+ }
1022
+
729
1023
  return {
730
1024
  columns,
731
1025
  rows,
@@ -764,54 +1058,108 @@ export function getPatternTileCount(patternData, targetBBox) {
764
1058
  * // (from x:10 to x:90, y:20 to y:60)
765
1059
  */
766
1060
  export function getPatternContentBBox(patternData) {
1061
+ // Validate parameter
1062
+ if (!patternData) {
1063
+ throw new Error("getPatternContentBBox: patternData is required");
1064
+ }
1065
+ // Validate patternData has children array
1066
+ if (!Array.isArray(patternData.children)) {
1067
+ throw new Error(
1068
+ "getPatternContentBBox: patternData.children must be an array",
1069
+ );
1070
+ }
1071
+
767
1072
  let minX = Infinity;
768
1073
  let minY = Infinity;
769
1074
  let maxX = -Infinity;
770
1075
  let maxY = -Infinity;
771
1076
 
772
1077
  for (const child of patternData.children) {
1078
+ if (!child || !child.type) continue;
1079
+
773
1080
  let childBBox = null;
774
1081
 
775
1082
  switch (child.type) {
776
1083
  case "rect":
777
- childBBox = {
778
- x: child.x,
779
- y: child.y,
780
- width: child.width,
781
- height: child.height,
782
- };
1084
+ // Validate rect has required numeric properties
1085
+ if (
1086
+ typeof child.x === "number" &&
1087
+ typeof child.y === "number" &&
1088
+ typeof child.width === "number" &&
1089
+ typeof child.height === "number"
1090
+ ) {
1091
+ childBBox = {
1092
+ x: child.x,
1093
+ y: child.y,
1094
+ width: child.width,
1095
+ height: child.height,
1096
+ };
1097
+ }
783
1098
  break;
784
1099
  case "circle":
785
- childBBox = {
786
- x: child.cx - child.r,
787
- y: child.cy - child.r,
788
- width: child.r * 2,
789
- height: child.r * 2,
790
- };
1100
+ // Validate circle has required numeric properties
1101
+ if (
1102
+ typeof child.cx === "number" &&
1103
+ typeof child.cy === "number" &&
1104
+ typeof child.r === "number"
1105
+ ) {
1106
+ childBBox = {
1107
+ x: child.cx - child.r,
1108
+ y: child.cy - child.r,
1109
+ width: child.r * 2,
1110
+ height: child.r * 2,
1111
+ };
1112
+ }
791
1113
  break;
792
1114
  case "ellipse":
793
- childBBox = {
794
- x: child.cx - child.rx,
795
- y: child.cy - child.ry,
796
- width: child.rx * 2,
797
- height: child.ry * 2,
798
- };
1115
+ // Validate ellipse has required numeric properties
1116
+ if (
1117
+ typeof child.cx === "number" &&
1118
+ typeof child.cy === "number" &&
1119
+ typeof child.rx === "number" &&
1120
+ typeof child.ry === "number"
1121
+ ) {
1122
+ childBBox = {
1123
+ x: child.cx - child.rx,
1124
+ y: child.cy - child.ry,
1125
+ width: child.rx * 2,
1126
+ height: child.ry * 2,
1127
+ };
1128
+ }
799
1129
  break;
800
1130
  case "line":
801
- childBBox = {
802
- x: Math.min(child.x1, child.x2),
803
- y: Math.min(child.y1, child.y2),
804
- width: Math.abs(child.x2 - child.x1),
805
- height: Math.abs(child.y2 - child.y1),
806
- };
1131
+ // Validate line has required numeric properties
1132
+ if (
1133
+ typeof child.x1 === "number" &&
1134
+ typeof child.y1 === "number" &&
1135
+ typeof child.x2 === "number" &&
1136
+ typeof child.y2 === "number"
1137
+ ) {
1138
+ childBBox = {
1139
+ x: Math.min(child.x1, child.x2),
1140
+ y: Math.min(child.y1, child.y2),
1141
+ width: Math.abs(child.x2 - child.x1),
1142
+ height: Math.abs(child.y2 - child.y1),
1143
+ };
1144
+ }
1145
+ break;
1146
+ default:
807
1147
  break;
808
1148
  }
809
1149
 
810
1150
  if (childBBox) {
811
- minX = Math.min(minX, childBBox.x);
812
- minY = Math.min(minY, childBBox.y);
813
- maxX = Math.max(maxX, childBBox.x + childBBox.width);
814
- maxY = Math.max(maxY, childBBox.y + childBBox.height);
1151
+ // Validate bbox values are finite
1152
+ if (
1153
+ isFinite(childBBox.x) &&
1154
+ isFinite(childBBox.y) &&
1155
+ isFinite(childBBox.width) &&
1156
+ isFinite(childBBox.height)
1157
+ ) {
1158
+ minX = Math.min(minX, childBBox.x);
1159
+ minY = Math.min(minY, childBBox.y);
1160
+ maxX = Math.max(maxX, childBBox.x + childBBox.width);
1161
+ maxY = Math.max(maxY, childBBox.y + childBBox.height);
1162
+ }
815
1163
  }
816
1164
  }
817
1165