@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.
- package/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/svg-flatten.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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(
|
|
628
|
+
return num.mul(validDpi).div(72);
|
|
555
629
|
case "pc":
|
|
556
|
-
return num.mul(
|
|
630
|
+
return num.mul(validDpi).div(6);
|
|
557
631
|
case "in":
|
|
558
|
-
return num.mul(
|
|
632
|
+
return num.mul(validDpi);
|
|
559
633
|
case "cm":
|
|
560
|
-
return num.mul(
|
|
634
|
+
return num.mul(validDpi).div(2.54);
|
|
561
635
|
case "mm":
|
|
562
|
-
return num.mul(
|
|
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
|
|
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(
|
|
670
|
-
const translateM = Transforms2D.translation(
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1314
|
+
let newSweepFlag = validSweepFlag;
|
|
1136
1315
|
if (det.lt(0)) {
|
|
1137
1316
|
// Flip sweep direction
|
|
1138
|
-
newSweepFlag =
|
|
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:
|
|
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]
|
|
1218
|
-
const ty = args[1]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
}
|