@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
@@ -44,6 +44,22 @@ export const STANDARD_EASINGS = {
44
44
  * @returns {string} Optimized number string
45
45
  */
46
46
  export function formatSplineValue(value, precision = 3) {
47
+ // Validate value parameter
48
+ if (value === null || value === undefined) {
49
+ throw new Error('formatSplineValue: value parameter is required');
50
+ }
51
+
52
+ // Validate precision parameter
53
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
54
+ throw new Error('formatSplineValue: precision must be a non-negative finite number');
55
+ }
56
+
57
+ // Check for NaN/Infinity before creating Decimal
58
+ const numValue = typeof value === 'number' ? value : parseFloat(value);
59
+ if (!Number.isFinite(numValue)) {
60
+ throw new Error(`formatSplineValue: value must be a finite number, got ${value}`);
61
+ }
62
+
47
63
  const num = new Decimal(value);
48
64
 
49
65
  // Round to precision
@@ -82,11 +98,20 @@ export function parseKeySplines(keySplines) {
82
98
  // Split by semicolon to get individual splines
83
99
  const splines = keySplines.split(';').map(s => s.trim()).filter(s => s);
84
100
 
85
- return splines.map(spline => {
86
- // Split by whitespace or comma to get control points
87
- const values = spline.split(/[\s,]+/).map(v => parseFloat(v));
88
- return values;
89
- });
101
+ return splines
102
+ .map(spline => {
103
+ // Split by whitespace or comma to get control points
104
+ const values = spline.split(/[\s,]+/)
105
+ .map(v => parseFloat(v))
106
+ .filter(v => Number.isFinite(v)); // Filter out NaN and Infinity
107
+
108
+ // Each spline must have exactly 4 control points
109
+ if (values.length !== 4) {
110
+ throw new Error(`parseKeySplines: invalid spline "${spline}", expected 4 values, got ${values.length}`);
111
+ }
112
+
113
+ return values;
114
+ });
90
115
  }
91
116
 
92
117
  /**
@@ -96,7 +121,25 @@ export function parseKeySplines(keySplines) {
96
121
  * @returns {string} keySplines attribute value
97
122
  */
98
123
  export function serializeKeySplines(splines, precision = 3) {
99
- return splines.map(spline => {
124
+ // Validate splines parameter
125
+ if (!Array.isArray(splines)) {
126
+ throw new Error('serializeKeySplines: splines must be an array');
127
+ }
128
+
129
+ // Validate precision parameter
130
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
131
+ throw new Error('serializeKeySplines: precision must be a non-negative finite number');
132
+ }
133
+
134
+ return splines.map((spline, index) => {
135
+ // Validate each spline is an array with 4 values
136
+ if (!Array.isArray(spline)) {
137
+ throw new Error(`serializeKeySplines: spline at index ${index} must be an array`);
138
+ }
139
+ if (spline.length !== 4) {
140
+ throw new Error(`serializeKeySplines: spline at index ${index} must have exactly 4 values, got ${spline.length}`);
141
+ }
142
+
100
143
  return spline.map(v => formatSplineValue(v, precision)).join(' ');
101
144
  }).join('; ');
102
145
  }
@@ -108,9 +151,21 @@ export function serializeKeySplines(splines, precision = 3) {
108
151
  * @returns {boolean} True if spline is linear
109
152
  */
110
153
  export function isLinearSpline(spline, tolerance = 0.001) {
111
- if (!spline || spline.length !== 4) return false;
154
+ // Validate spline parameter
155
+ if (!Array.isArray(spline) || spline.length !== 4) return false;
156
+
157
+ // Validate tolerance parameter
158
+ if (typeof tolerance !== 'number' || tolerance < 0 || !Number.isFinite(tolerance)) {
159
+ throw new Error('isLinearSpline: tolerance must be a non-negative finite number');
160
+ }
112
161
 
113
162
  const [x1, y1, x2, y2] = spline;
163
+
164
+ // Check for NaN values in spline
165
+ if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) {
166
+ return false;
167
+ }
168
+
114
169
  return (
115
170
  Math.abs(x1) < tolerance &&
116
171
  Math.abs(y1) < tolerance &&
@@ -138,7 +193,16 @@ export function areAllSplinesLinear(keySplines) {
138
193
  * @returns {string|null} Easing name or null if not standard
139
194
  */
140
195
  export function identifyStandardEasing(spline, tolerance = 0.01) {
141
- if (!spline || spline.length !== 4) return null;
196
+ // Validate spline parameter
197
+ if (!Array.isArray(spline) || spline.length !== 4) return null;
198
+
199
+ // Validate tolerance parameter
200
+ if (typeof tolerance !== 'number' || tolerance < 0 || !Number.isFinite(tolerance)) {
201
+ throw new Error('identifyStandardEasing: tolerance must be a non-negative finite number');
202
+ }
203
+
204
+ // Check for NaN values in spline
205
+ if (!spline.every(val => Number.isFinite(val))) return null;
142
206
 
143
207
  for (const [name, standard] of Object.entries(STANDARD_EASINGS)) {
144
208
  const matches = spline.every((val, i) => Math.abs(val - standard[i]) < tolerance);
@@ -189,7 +253,7 @@ export function optimizeKeySplines(keySplines, options = {}) {
189
253
  */
190
254
  export function parseKeyTimes(keyTimes) {
191
255
  if (!keyTimes || typeof keyTimes !== 'string') return [];
192
- return keyTimes.split(';').map(s => parseFloat(s.trim())).filter(v => !isNaN(v));
256
+ return keyTimes.split(';').map(s => parseFloat(s.trim())).filter(v => Number.isFinite(v));
193
257
  }
194
258
 
195
259
  /**
@@ -199,6 +263,19 @@ export function parseKeyTimes(keyTimes) {
199
263
  * @returns {string} keyTimes attribute value
200
264
  */
201
265
  export function serializeKeyTimes(times, precision = 3) {
266
+ // Validate times parameter
267
+ if (!Array.isArray(times)) {
268
+ throw new Error('serializeKeyTimes: times must be an array');
269
+ }
270
+
271
+ // Validate precision parameter
272
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
273
+ throw new Error('serializeKeyTimes: precision must be a non-negative finite number');
274
+ }
275
+
276
+ // Return empty string for empty array
277
+ if (times.length === 0) return '';
278
+
202
279
  return times.map(t => formatSplineValue(t, precision)).join('; ');
203
280
  }
204
281
 
@@ -209,6 +286,16 @@ export function serializeKeyTimes(times, precision = 3) {
209
286
  * @returns {string} Optimized keyTimes value
210
287
  */
211
288
  export function optimizeKeyTimes(keyTimes, precision = 3) {
289
+ // Validate keyTimes parameter
290
+ if (keyTimes === null || keyTimes === undefined) {
291
+ throw new Error('optimizeKeyTimes: keyTimes parameter is required');
292
+ }
293
+
294
+ // Validate precision parameter
295
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
296
+ throw new Error('optimizeKeyTimes: precision must be a non-negative finite number');
297
+ }
298
+
212
299
  const times = parseKeyTimes(keyTimes);
213
300
  if (times.length === 0) return keyTimes;
214
301
  return serializeKeyTimes(times, precision);
@@ -224,9 +311,17 @@ export function optimizeKeyTimes(keyTimes, precision = 3) {
224
311
  export function optimizeAnimationValues(values, precision = 3) {
225
312
  if (!values || typeof values !== 'string') return values;
226
313
 
314
+ // Validate precision parameter
315
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
316
+ throw new Error('optimizeAnimationValues: precision must be a non-negative finite number');
317
+ }
318
+
227
319
  // Split by semicolon
228
320
  const parts = values.split(';');
229
321
 
322
+ // Handle empty values string
323
+ if (parts.length === 0) return values;
324
+
230
325
  const optimized = parts.map(part => {
231
326
  const trimmed = part.trim();
232
327
 
@@ -236,10 +331,10 @@ export function optimizeAnimationValues(values, precision = 3) {
236
331
  }
237
332
 
238
333
  // Try to parse as numbers (could be space-separated like "0 0" for translate)
239
- const nums = trimmed.split(/[\s,]+/);
334
+ const nums = trimmed.split(/[\s,]+/).filter(n => n); // Filter empty strings
240
335
  const optimizedNums = nums.map(n => {
241
336
  const num = parseFloat(n);
242
- if (isNaN(num)) return n; // Not a number, preserve as-is
337
+ if (!Number.isFinite(num)) return n; // Not a finite number, preserve as-is
243
338
  return formatSplineValue(num, precision);
244
339
  });
245
340
 
@@ -256,10 +351,23 @@ export function optimizeAnimationValues(values, precision = 3) {
256
351
  * @returns {{modified: boolean, changes: string[]}}
257
352
  */
258
353
  export function optimizeElementTiming(el, options = {}) {
354
+ // Validate el parameter
355
+ if (!el || typeof el !== 'object') {
356
+ throw new Error('optimizeElementTiming: el parameter must be a valid element');
357
+ }
358
+ if (typeof el.getAttribute !== 'function' || typeof el.setAttribute !== 'function' || typeof el.removeAttribute !== 'function') {
359
+ throw new Error('optimizeElementTiming: el parameter must have getAttribute, setAttribute, and removeAttribute methods');
360
+ }
361
+
259
362
  const precision = options.precision ?? 3;
260
363
  const removeLinearSplines = options.removeLinearSplines !== false;
261
364
  const optimizeValues = options.optimizeValues !== false;
262
365
 
366
+ // Validate precision from options
367
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
368
+ throw new Error('optimizeElementTiming: options.precision must be a non-negative finite number');
369
+ }
370
+
263
371
  const changes = [];
264
372
  let modified = false;
265
373
 
@@ -295,11 +403,10 @@ export function optimizeElementTiming(el, options = {}) {
295
403
  }
296
404
  }
297
405
 
298
- // Optimize values (only numeric, preserve ID refs)
406
+ // Optimize values (optimizeAnimationValues internally preserves ID refs)
299
407
  if (optimizeValues) {
300
408
  const values = el.getAttribute('values');
301
- if (values && !values.includes('#')) {
302
- // Only optimize if no ID references
409
+ if (values) {
303
410
  const optimized = optimizeAnimationValues(values, precision);
304
411
  if (optimized !== values) {
305
412
  el.setAttribute('values', optimized);
@@ -309,10 +416,10 @@ export function optimizeElementTiming(el, options = {}) {
309
416
  }
310
417
  }
311
418
 
312
- // Optimize from/to
419
+ // Optimize from/to/by (optimizeAnimationValues internally preserves ID refs)
313
420
  for (const attr of ['from', 'to', 'by']) {
314
421
  const val = el.getAttribute(attr);
315
- if (val && !val.includes('#')) {
422
+ if (val) {
316
423
  const optimized = optimizeAnimationValues(val, precision);
317
424
  if (optimized !== val) {
318
425
  el.setAttribute(attr, optimized);
@@ -338,13 +445,24 @@ export const ANIMATION_ELEMENTS = ['animate', 'animatetransform', 'animatemotion
338
445
  * @returns {{elementsModified: number, totalChanges: number, details: Array}}
339
446
  */
340
447
  export function optimizeDocumentAnimationTiming(root, options = {}) {
448
+ // Validate root parameter
449
+ if (!root || typeof root !== 'object') {
450
+ throw new Error('optimizeDocumentAnimationTiming: root parameter must be a valid element');
451
+ }
452
+
341
453
  let elementsModified = 0;
342
454
  let totalChanges = 0;
343
455
  const details = [];
344
456
 
345
457
  const processElement = (el) => {
458
+ // Skip if not a valid element
459
+ if (!el || typeof el !== 'object') return;
460
+
346
461
  const tagName = el.tagName?.toLowerCase();
347
462
 
463
+ // Skip if no tagName
464
+ if (!tagName) return;
465
+
348
466
  if (ANIMATION_ELEMENTS.includes(tagName)) {
349
467
  const result = optimizeElementTiming(el, options);
350
468
  if (result.modified) {
@@ -358,7 +476,9 @@ export function optimizeDocumentAnimationTiming(root, options = {}) {
358
476
  }
359
477
  }
360
478
 
361
- for (const child of el.children || []) {
479
+ // Process children safely
480
+ const children = el.children || [];
481
+ for (const child of children) {
362
482
  processElement(child);
363
483
  }
364
484
  };
@@ -197,6 +197,20 @@ export function parseJavaScriptIds(js) {
197
197
  * @returns {{static: Set<string>, animation: Set<string>, css: Set<string>, js: Set<string>}}
198
198
  */
199
199
  export function collectElementReferences(el) {
200
+ // Validate input parameter
201
+ if (!el || typeof el !== "object" || !(el instanceof SVGElement)) {
202
+ throw new TypeError(
203
+ "collectElementReferences: el must be a valid SVGElement",
204
+ );
205
+ }
206
+
207
+ // Validate element has required methods
208
+ if (typeof el.getAttributeNames !== "function") {
209
+ throw new TypeError(
210
+ "collectElementReferences: el must have getAttributeNames method",
211
+ );
212
+ }
213
+
200
214
  const refs = {
201
215
  static: new Set(),
202
216
  animation: new Set(),
@@ -207,7 +221,14 @@ export function collectElementReferences(el) {
207
221
  const tagName = el.tagName?.toLowerCase() || "";
208
222
  const isAnimationElement = ANIMATION_ELEMENTS.includes(tagName);
209
223
 
210
- for (const attrName of el.getAttributeNames()) {
224
+ const attributeNames = el.getAttributeNames();
225
+ if (!Array.isArray(attributeNames)) {
226
+ throw new TypeError(
227
+ "collectElementReferences: getAttributeNames must return an array",
228
+ );
229
+ }
230
+
231
+ for (const attrName of attributeNames) {
211
232
  const value = el.getAttribute(attrName);
212
233
  if (!value) continue;
213
234
 
@@ -253,8 +274,22 @@ export function collectElementReferences(el) {
253
274
 
254
275
  // Check <mpath> element inside <animateMotion>
255
276
  if (tagName === "animatemotion") {
277
+ if (typeof el.getElementsByTagName !== "function") {
278
+ throw new TypeError(
279
+ "collectElementReferences: el must have getElementsByTagName method",
280
+ );
281
+ }
256
282
  const mpaths = el.getElementsByTagName("mpath");
283
+ // Validate mpaths is iterable
284
+ if (!mpaths || typeof mpaths[Symbol.iterator] !== "function") {
285
+ throw new TypeError(
286
+ "collectElementReferences: getElementsByTagName must return an iterable",
287
+ );
288
+ }
257
289
  for (const mpath of mpaths) {
290
+ // Validate mpath has getAttribute method
291
+ if (!mpath || typeof mpath.getAttribute !== "function") continue;
292
+
258
293
  for (const attr of HREF_ATTRIBUTES) {
259
294
  const value = mpath.getAttribute(attr);
260
295
  const id = parseHrefId(value);
@@ -281,6 +316,13 @@ export function collectElementReferences(el) {
281
316
  * }}
282
317
  */
283
318
  export function collectAllReferences(root) {
319
+ // Validate input parameter
320
+ if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
321
+ throw new TypeError(
322
+ "collectAllReferences: root must be a valid SVGElement",
323
+ );
324
+ }
325
+
284
326
  const result = {
285
327
  all: new Set(),
286
328
  static: new Set(),
@@ -339,9 +381,11 @@ export function collectAllReferences(root) {
339
381
  }
340
382
 
341
383
  // Recurse to children
342
- for (const child of el.children) {
343
- if (child instanceof SVGElement) {
344
- processElement(child, currentPath);
384
+ if (el.children && typeof el.children[Symbol.iterator] === "function") {
385
+ for (const child of el.children) {
386
+ if (child instanceof SVGElement) {
387
+ processElement(child, currentPath);
388
+ }
345
389
  }
346
390
  }
347
391
  };
@@ -358,6 +402,14 @@ export function collectAllReferences(root) {
358
402
  * @returns {boolean} True if the ID is referenced
359
403
  */
360
404
  export function isIdReferenced(root, id) {
405
+ // Validate input parameters
406
+ if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
407
+ throw new TypeError("isIdReferenced: root must be a valid SVGElement");
408
+ }
409
+ if (!id || typeof id !== "string") {
410
+ throw new TypeError("isIdReferenced: id must be a non-empty string");
411
+ }
412
+
361
413
  const refs = collectAllReferences(root);
362
414
  return refs.all.has(id);
363
415
  }
@@ -369,6 +421,14 @@ export function isIdReferenced(root, id) {
369
421
  * @returns {{referenced: boolean, type: string|null, sources: string[]}}
370
422
  */
371
423
  export function getIdReferenceInfo(root, id) {
424
+ // Validate input parameters
425
+ if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
426
+ throw new TypeError("getIdReferenceInfo: root must be a valid SVGElement");
427
+ }
428
+ if (!id || typeof id !== "string") {
429
+ throw new TypeError("getIdReferenceInfo: id must be a non-empty string");
430
+ }
431
+
372
432
  const refs = collectAllReferences(root);
373
433
  const details = refs.details.get(id);
374
434
 
@@ -387,6 +447,11 @@ export function getIdReferenceInfo(root, id) {
387
447
  * @returns {{safeToRemove: string[], referenced: string[], animationReferenced: string[]}}
388
448
  */
389
449
  export function findUnreferencedDefs(root) {
450
+ // Validate input parameter
451
+ if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
452
+ throw new TypeError("findUnreferencedDefs: root must be a valid SVGElement");
453
+ }
454
+
390
455
  const refs = collectAllReferences(root);
391
456
 
392
457
  const safeToRemove = [];
@@ -394,8 +459,26 @@ export function findUnreferencedDefs(root) {
394
459
  const animationReferenced = [];
395
460
 
396
461
  // Scan all defs
462
+ if (typeof root.getElementsByTagName !== "function") {
463
+ throw new TypeError(
464
+ "findUnreferencedDefs: root must have getElementsByTagName method",
465
+ );
466
+ }
467
+
397
468
  const defsElements = root.getElementsByTagName("defs");
469
+ // Validate defsElements is iterable
470
+ if (!defsElements || typeof defsElements[Symbol.iterator] !== "function") {
471
+ throw new TypeError(
472
+ "findUnreferencedDefs: getElementsByTagName must return an iterable",
473
+ );
474
+ }
475
+
398
476
  for (const defs of defsElements) {
477
+ // Validate defs.children exists and is iterable
478
+ if (!defs.children || typeof defs.children[Symbol.iterator] !== "function") {
479
+ continue; // Skip this defs element if children is not iterable
480
+ }
481
+
399
482
  for (const child of defs.children) {
400
483
  if (child instanceof SVGElement) {
401
484
  const id = child.getAttribute("id");
@@ -423,20 +506,54 @@ export function findUnreferencedDefs(root) {
423
506
  * @returns {{removed: string[], kept: string[], keptForAnimation: string[]}}
424
507
  */
425
508
  export function removeUnreferencedDefsSafe(root) {
509
+ // Validate input parameter
510
+ if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
511
+ throw new TypeError(
512
+ "removeUnreferencedDefsSafe: root must be a valid SVGElement",
513
+ );
514
+ }
515
+
426
516
  const { safeToRemove, referenced, animationReferenced } =
427
517
  findUnreferencedDefs(root);
428
518
 
429
519
  const removed = [];
430
520
 
431
521
  // Only remove elements that are truly unreferenced
522
+ if (typeof root.getElementsByTagName !== "function") {
523
+ throw new TypeError(
524
+ "removeUnreferencedDefsSafe: root must have getElementsByTagName method",
525
+ );
526
+ }
527
+
432
528
  const defsElements = root.getElementsByTagName("defs");
529
+ // Validate defsElements is iterable
530
+ if (!defsElements || typeof defsElements[Symbol.iterator] !== "function") {
531
+ throw new TypeError(
532
+ "removeUnreferencedDefsSafe: getElementsByTagName must return an iterable",
533
+ );
534
+ }
535
+
433
536
  for (const defs of defsElements) {
537
+ // Validate defs.children exists and is iterable before spreading
538
+ if (!defs.children || typeof defs.children[Symbol.iterator] !== "function") {
539
+ continue; // Skip this defs element if children is not iterable
540
+ }
541
+
434
542
  for (const child of [...defs.children]) {
435
543
  if (child instanceof SVGElement) {
436
544
  const id = child.getAttribute("id");
437
545
  if (id && safeToRemove.includes(id)) {
438
- defs.removeChild(child);
439
- removed.push(id);
546
+ try {
547
+ defs.removeChild(child);
548
+ removed.push(id);
549
+ } catch (error) {
550
+ // If removeChild fails (child not a direct child of defs), skip it
551
+ // This prevents the function from crashing on edge cases
552
+ console.warn(
553
+ `removeUnreferencedDefsSafe: Failed to remove child with id="${id}":`,
554
+ error.message,
555
+ );
556
+ }
440
557
  }
441
558
  }
442
559
  }