@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
@@ -59,6 +59,9 @@ export class BrowserVerifier {
59
59
  * @throws {Error} If Playwright is not installed
60
60
  */
61
61
  async init(options = {}) {
62
+ if (options !== null && typeof options !== "object") {
63
+ throw new Error("options must be an object");
64
+ }
62
65
  await loadPlaywright();
63
66
  this.browser = await chromium.launch(options);
64
67
  this.page = await this.browser.newPage();
@@ -93,6 +96,23 @@ export class BrowserVerifier {
93
96
  if (!this.page) {
94
97
  throw new Error("BrowserVerifier not initialized. Call init() first.");
95
98
  }
99
+ if (!config || typeof config !== "object") {
100
+ throw new Error("config must be a valid object");
101
+ }
102
+ if (
103
+ typeof config.width !== "number" ||
104
+ !isFinite(config.width) ||
105
+ config.width <= 0
106
+ ) {
107
+ throw new Error("config.width must be a positive finite number");
108
+ }
109
+ if (
110
+ typeof config.height !== "number" ||
111
+ !isFinite(config.height) ||
112
+ config.height <= 0
113
+ ) {
114
+ throw new Error("config.height must be a positive finite number");
115
+ }
96
116
 
97
117
  return await this.page.evaluate((cfg) => {
98
118
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -117,6 +137,9 @@ export class BrowserVerifier {
117
137
  const ctm = rect.getCTM();
118
138
  document.body.removeChild(svg);
119
139
 
140
+ if (!ctm) {
141
+ throw new Error("getCTM() returned null");
142
+ }
120
143
  return { a: ctm.a, b: ctm.b, c: ctm.c, d: ctm.d, e: ctm.e, f: ctm.f };
121
144
  }, config);
122
145
  }
@@ -131,6 +154,23 @@ export class BrowserVerifier {
131
154
  if (!this.page) {
132
155
  throw new Error("BrowserVerifier not initialized. Call init() first.");
133
156
  }
157
+ if (!config || typeof config !== "object") {
158
+ throw new Error("config must be a valid object");
159
+ }
160
+ if (
161
+ typeof config.width !== "number" ||
162
+ !isFinite(config.width) ||
163
+ config.width <= 0
164
+ ) {
165
+ throw new Error("config.width must be a positive finite number");
166
+ }
167
+ if (
168
+ typeof config.height !== "number" ||
169
+ !isFinite(config.height) ||
170
+ config.height <= 0
171
+ ) {
172
+ throw new Error("config.height must be a positive finite number");
173
+ }
134
174
 
135
175
  return await this.page.evaluate((cfg) => {
136
176
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -155,6 +195,9 @@ export class BrowserVerifier {
155
195
  const ctm = rect.getScreenCTM();
156
196
  document.body.removeChild(svg);
157
197
 
198
+ if (!ctm) {
199
+ throw new Error("getScreenCTM() returned null");
200
+ }
158
201
  return { a: ctm.a, b: ctm.b, c: ctm.c, d: ctm.d, e: ctm.e, f: ctm.f };
159
202
  }, config);
160
203
  }
@@ -171,6 +214,29 @@ export class BrowserVerifier {
171
214
  if (!this.page) {
172
215
  throw new Error("BrowserVerifier not initialized. Call init() first.");
173
216
  }
217
+ if (!config || typeof config !== "object") {
218
+ throw new Error("config must be a valid object");
219
+ }
220
+ if (
221
+ typeof config.width !== "number" ||
222
+ !isFinite(config.width) ||
223
+ config.width <= 0
224
+ ) {
225
+ throw new Error("config.width must be a positive finite number");
226
+ }
227
+ if (
228
+ typeof config.height !== "number" ||
229
+ !isFinite(config.height) ||
230
+ config.height <= 0
231
+ ) {
232
+ throw new Error("config.height must be a positive finite number");
233
+ }
234
+ if (typeof x !== "number" || !isFinite(x)) {
235
+ throw new Error("x must be a finite number");
236
+ }
237
+ if (typeof y !== "number" || !isFinite(y)) {
238
+ throw new Error("y must be a finite number");
239
+ }
174
240
 
175
241
  return await this.page.evaluate(
176
242
  ({ cfg, px, py }) => {
@@ -194,6 +260,9 @@ export class BrowserVerifier {
194
260
  document.body.appendChild(svg);
195
261
 
196
262
  const ctm = rect.getCTM();
263
+ if (!ctm) {
264
+ throw new Error("getCTM() returned null");
265
+ }
197
266
  const point = svg.createSVGPoint();
198
267
  point.x = px;
199
268
  point.y = py;
@@ -216,6 +285,40 @@ export class BrowserVerifier {
216
285
  * @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
217
286
  */
218
287
  async verifyMatrix(matrix, config, tolerance = 1e-10) {
288
+ if (!matrix || typeof matrix !== "object") {
289
+ throw new Error("matrix must be a valid Matrix object");
290
+ }
291
+ if (!matrix.data || !Array.isArray(matrix.data)) {
292
+ throw new Error("matrix.data must be a valid array");
293
+ }
294
+ if (
295
+ matrix.data.length < 2 ||
296
+ !Array.isArray(matrix.data[0]) ||
297
+ matrix.data[0].length < 3 ||
298
+ !Array.isArray(matrix.data[1]) ||
299
+ matrix.data[1].length < 3
300
+ ) {
301
+ throw new Error(
302
+ "matrix.data must have at least 2 rows with at least 3 columns each",
303
+ );
304
+ }
305
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
306
+ throw new Error("tolerance must be a non-negative finite number");
307
+ }
308
+ // Validate that matrix.data contains Decimal objects with toNumber() method
309
+ if (
310
+ typeof matrix.data[0][0].toNumber !== "function" ||
311
+ typeof matrix.data[1][0].toNumber !== "function" ||
312
+ typeof matrix.data[0][1].toNumber !== "function" ||
313
+ typeof matrix.data[1][1].toNumber !== "function" ||
314
+ typeof matrix.data[0][2].toNumber !== "function" ||
315
+ typeof matrix.data[1][2].toNumber !== "function"
316
+ ) {
317
+ throw new Error(
318
+ "matrix.data elements must have toNumber() method (Decimal objects)",
319
+ );
320
+ }
321
+
219
322
  const browserCTM = await this.getBrowserCTM(config);
220
323
 
221
324
  const libraryCTM = {
@@ -258,9 +361,41 @@ export class BrowserVerifier {
258
361
  preserveAspectRatio = "xMidYMid meet",
259
362
  tolerance = 1e-10,
260
363
  ) {
261
- const vb = SVGFlatten.parseViewBox(viewBox);
262
- const par = SVGFlatten.parsePreserveAspectRatio(preserveAspectRatio);
263
- const matrix = SVGFlatten.computeViewBoxTransform(vb, width, height, par);
364
+ if (typeof width !== "number" || !isFinite(width) || width <= 0) {
365
+ throw new Error("width must be a positive finite number");
366
+ }
367
+ if (typeof height !== "number" || !isFinite(height) || height <= 0) {
368
+ throw new Error("height must be a positive finite number");
369
+ }
370
+ if (typeof viewBox !== "string" || viewBox.trim().length === 0) {
371
+ throw new Error("viewBox must be a non-empty string");
372
+ }
373
+ if (
374
+ typeof preserveAspectRatio !== "string" ||
375
+ preserveAspectRatio.trim().length === 0
376
+ ) {
377
+ throw new Error("preserveAspectRatio must be a non-empty string");
378
+ }
379
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
380
+ throw new Error("tolerance must be a non-negative finite number");
381
+ }
382
+
383
+ let vb, par, matrix;
384
+ try {
385
+ vb = SVGFlatten.parseViewBox(viewBox);
386
+ } catch (e) {
387
+ throw new Error(`Failed to parse viewBox: ${e.message}`);
388
+ }
389
+ try {
390
+ par = SVGFlatten.parsePreserveAspectRatio(preserveAspectRatio);
391
+ } catch (e) {
392
+ throw new Error(`Failed to parse preserveAspectRatio: ${e.message}`);
393
+ }
394
+ try {
395
+ matrix = SVGFlatten.computeViewBoxTransform(vb, width, height, par);
396
+ } catch (e) {
397
+ throw new Error(`Failed to compute viewBox transform: ${e.message}`);
398
+ }
264
399
 
265
400
  return await this.verifyMatrix(
266
401
  matrix,
@@ -282,7 +417,19 @@ export class BrowserVerifier {
282
417
  * @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
283
418
  */
284
419
  async verifyTransformAttribute(transform, tolerance = 1e-10) {
285
- const matrix = SVGFlatten.parseTransformAttribute(transform);
420
+ if (typeof transform !== "string" || transform.trim().length === 0) {
421
+ throw new Error("transform must be a non-empty string");
422
+ }
423
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
424
+ throw new Error("tolerance must be a non-negative finite number");
425
+ }
426
+
427
+ let matrix;
428
+ try {
429
+ matrix = SVGFlatten.parseTransformAttribute(transform);
430
+ } catch (e) {
431
+ throw new Error(`Failed to parse transform attribute: ${e.message}`);
432
+ }
286
433
 
287
434
  // Use a simple 100x100 SVG without viewBox to test just the transform
288
435
  return await this.verifyMatrix(
@@ -307,9 +454,35 @@ export class BrowserVerifier {
307
454
  * @returns {Promise<{matches: boolean, browserPoint: Object, libraryPoint: Object, difference: number}>}
308
455
  */
309
456
  async verifyPointTransform(matrix, x, y, config, tolerance = 1e-10) {
457
+ if (!matrix || typeof matrix !== "object") {
458
+ throw new Error("matrix must be a valid Matrix object");
459
+ }
460
+ if (typeof x !== "number" || !isFinite(x)) {
461
+ throw new Error("x must be a finite number");
462
+ }
463
+ if (typeof y !== "number" || !isFinite(y)) {
464
+ throw new Error("y must be a finite number");
465
+ }
466
+ if (!config || typeof config !== "object") {
467
+ throw new Error("config must be a valid object");
468
+ }
469
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
470
+ throw new Error("tolerance must be a non-negative finite number");
471
+ }
472
+
310
473
  const browserPoint = await this.transformPoint(config, x, y);
311
474
  const libraryPoint = SVGFlatten.applyToPoint(matrix, x, y);
312
475
 
476
+ if (
477
+ !libraryPoint ||
478
+ !libraryPoint.x ||
479
+ !libraryPoint.y ||
480
+ typeof libraryPoint.x.toNumber !== "function" ||
481
+ typeof libraryPoint.y.toNumber !== "function"
482
+ ) {
483
+ throw new Error("applyToPoint returned invalid result");
484
+ }
485
+
313
486
  const libPt = {
314
487
  x: libraryPoint.x.toNumber(),
315
488
  y: libraryPoint.y.toNumber(),
@@ -336,11 +509,38 @@ export class BrowserVerifier {
336
509
  * @returns {Promise<{passed: number, failed: number, results: Array}>}
337
510
  */
338
511
  async runBatch(testCases, tolerance = 1e-10) {
512
+ if (!Array.isArray(testCases)) {
513
+ throw new Error("testCases must be an array");
514
+ }
515
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
516
+ throw new Error("tolerance must be a non-negative finite number");
517
+ }
518
+
339
519
  const results = [];
340
520
  let passed = 0;
341
521
  let failed = 0;
342
522
 
343
523
  for (const tc of testCases) {
524
+ if (!tc || typeof tc !== "object") {
525
+ throw new Error("Each test case must be a valid object");
526
+ }
527
+ if (
528
+ typeof tc.width !== "number" ||
529
+ !isFinite(tc.width) ||
530
+ tc.width <= 0
531
+ ) {
532
+ throw new Error("Each test case must have a positive finite width");
533
+ }
534
+ if (
535
+ typeof tc.height !== "number" ||
536
+ !isFinite(tc.height) ||
537
+ tc.height <= 0
538
+ ) {
539
+ throw new Error("Each test case must have a positive finite height");
540
+ }
541
+ if (typeof tc.viewBox !== "string" || tc.viewBox.trim().length === 0) {
542
+ throw new Error("Each test case must have a non-empty viewBox string");
543
+ }
344
544
  const result = await this.verifyViewBoxTransform(
345
545
  tc.width,
346
546
  tc.height,
@@ -372,6 +572,16 @@ export class BrowserVerifier {
372
572
  * @returns {Promise<boolean>} True if matrix matches browser's CTM
373
573
  */
374
574
  export async function quickVerify(matrix, config, tolerance = 1e-10) {
575
+ if (!matrix || typeof matrix !== "object") {
576
+ throw new Error("matrix must be a valid Matrix object");
577
+ }
578
+ if (!config || typeof config !== "object") {
579
+ throw new Error("config must be a valid object");
580
+ }
581
+ if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
582
+ throw new Error("tolerance must be a non-negative finite number");
583
+ }
584
+
375
585
  const verifier = new BrowserVerifier();
376
586
  await verifier.init({ headless: true });
377
587
 
@@ -398,6 +608,22 @@ export async function verifyViewBox(
398
608
  viewBox,
399
609
  preserveAspectRatio = "xMidYMid meet",
400
610
  ) {
611
+ if (typeof width !== "number" || !isFinite(width) || width <= 0) {
612
+ throw new Error("width must be a positive finite number");
613
+ }
614
+ if (typeof height !== "number" || !isFinite(height) || height <= 0) {
615
+ throw new Error("height must be a positive finite number");
616
+ }
617
+ if (typeof viewBox !== "string" || viewBox.trim().length === 0) {
618
+ throw new Error("viewBox must be a non-empty string");
619
+ }
620
+ if (
621
+ typeof preserveAspectRatio !== "string" ||
622
+ preserveAspectRatio.trim().length === 0
623
+ ) {
624
+ throw new Error("preserveAspectRatio must be a non-empty string");
625
+ }
626
+
401
627
  const verifier = new BrowserVerifier();
402
628
  await verifier.init({ headless: true });
403
629
 
@@ -420,6 +646,10 @@ export async function verifyViewBox(
420
646
  * @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object}>}
421
647
  */
422
648
  export async function verifyTransform(transform) {
649
+ if (typeof transform !== "string" || transform.trim().length === 0) {
650
+ throw new Error("transform must be a non-empty string");
651
+ }
652
+
423
653
  const verifier = new BrowserVerifier();
424
654
  await verifier.init({ headless: true });
425
655
 
@@ -439,6 +669,9 @@ export async function verifyTransform(transform) {
439
669
  * @returns {Promise<{passed: number, failed: number, results: Array}>}
440
670
  */
441
671
  export async function runStandardTests(options = { verbose: true }) {
672
+ if (!options || typeof options !== "object") {
673
+ throw new Error("options must be a valid object");
674
+ }
442
675
  const testCases = [
443
676
  // W3C SVG WG issue #215 cases
444
677
  {