@emasoft/svg-matrix 1.0.28 → 1.0.30

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 (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -13,9 +13,9 @@
13
13
  * @module browser-verify
14
14
  */
15
15
 
16
- import Decimal from 'decimal.js';
17
- import { Matrix } from './matrix.js';
18
- import * as SVGFlatten from './svg-flatten.js';
16
+ import Decimal from "decimal.js";
17
+ import { Matrix as _Matrix } from "./matrix.js";
18
+ import * as SVGFlatten from "./svg-flatten.js";
19
19
 
20
20
  // Playwright is loaded dynamically to avoid crashes when not installed
21
21
  let chromium = null;
@@ -27,12 +27,12 @@ let chromium = null;
27
27
  async function loadPlaywright() {
28
28
  if (chromium) return;
29
29
  try {
30
- const playwright = await import('playwright');
30
+ const playwright = await import("playwright");
31
31
  chromium = playwright.chromium;
32
- } catch (e) {
32
+ } catch (_e) {
33
33
  throw new Error(
34
- 'Playwright is required for browser verification but not installed.\n' +
35
- 'Install with: npm install playwright && npx playwright install chromium'
34
+ "Playwright is required for browser verification but not installed.\n" +
35
+ "Install with: npm install playwright && npx playwright install chromium",
36
36
  );
37
37
  }
38
38
  }
@@ -91,22 +91,26 @@ export class BrowserVerifier {
91
91
  */
92
92
  async getBrowserCTM(config) {
93
93
  if (!this.page) {
94
- throw new Error('BrowserVerifier not initialized. Call init() first.');
94
+ throw new Error("BrowserVerifier not initialized. Call init() first.");
95
95
  }
96
96
 
97
97
  return await this.page.evaluate((cfg) => {
98
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
99
- svg.setAttribute('width', cfg.width);
100
- svg.setAttribute('height', cfg.height);
101
- if (cfg.viewBox) svg.setAttribute('viewBox', cfg.viewBox);
102
- if (cfg.preserveAspectRatio) svg.setAttribute('preserveAspectRatio', cfg.preserveAspectRatio);
103
-
104
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
105
- rect.setAttribute('x', '0');
106
- rect.setAttribute('y', '0');
107
- rect.setAttribute('width', '10');
108
- rect.setAttribute('height', '10');
109
- if (cfg.transform) rect.setAttribute('transform', cfg.transform);
98
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
99
+ svg.setAttribute("width", cfg.width);
100
+ svg.setAttribute("height", cfg.height);
101
+ if (cfg.viewBox) svg.setAttribute("viewBox", cfg.viewBox);
102
+ if (cfg.preserveAspectRatio)
103
+ svg.setAttribute("preserveAspectRatio", cfg.preserveAspectRatio);
104
+
105
+ const rect = document.createElementNS(
106
+ "http://www.w3.org/2000/svg",
107
+ "rect",
108
+ );
109
+ rect.setAttribute("x", "0");
110
+ rect.setAttribute("y", "0");
111
+ rect.setAttribute("width", "10");
112
+ rect.setAttribute("height", "10");
113
+ if (cfg.transform) rect.setAttribute("transform", cfg.transform);
110
114
  svg.appendChild(rect);
111
115
 
112
116
  document.body.appendChild(svg);
@@ -125,22 +129,26 @@ export class BrowserVerifier {
125
129
  */
126
130
  async getBrowserScreenCTM(config) {
127
131
  if (!this.page) {
128
- throw new Error('BrowserVerifier not initialized. Call init() first.');
132
+ throw new Error("BrowserVerifier not initialized. Call init() first.");
129
133
  }
130
134
 
131
135
  return await this.page.evaluate((cfg) => {
132
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
133
- svg.setAttribute('width', cfg.width);
134
- svg.setAttribute('height', cfg.height);
135
- if (cfg.viewBox) svg.setAttribute('viewBox', cfg.viewBox);
136
- if (cfg.preserveAspectRatio) svg.setAttribute('preserveAspectRatio', cfg.preserveAspectRatio);
137
-
138
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
139
- rect.setAttribute('x', '0');
140
- rect.setAttribute('y', '0');
141
- rect.setAttribute('width', '10');
142
- rect.setAttribute('height', '10');
143
- if (cfg.transform) rect.setAttribute('transform', cfg.transform);
136
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
137
+ svg.setAttribute("width", cfg.width);
138
+ svg.setAttribute("height", cfg.height);
139
+ if (cfg.viewBox) svg.setAttribute("viewBox", cfg.viewBox);
140
+ if (cfg.preserveAspectRatio)
141
+ svg.setAttribute("preserveAspectRatio", cfg.preserveAspectRatio);
142
+
143
+ const rect = document.createElementNS(
144
+ "http://www.w3.org/2000/svg",
145
+ "rect",
146
+ );
147
+ rect.setAttribute("x", "0");
148
+ rect.setAttribute("y", "0");
149
+ rect.setAttribute("width", "10");
150
+ rect.setAttribute("height", "10");
151
+ if (cfg.transform) rect.setAttribute("transform", cfg.transform);
144
152
  svg.appendChild(rect);
145
153
 
146
154
  document.body.appendChild(svg);
@@ -161,32 +169,42 @@ export class BrowserVerifier {
161
169
  */
162
170
  async transformPoint(config, x, y) {
163
171
  if (!this.page) {
164
- throw new Error('BrowserVerifier not initialized. Call init() first.');
172
+ throw new Error("BrowserVerifier not initialized. Call init() first.");
165
173
  }
166
174
 
167
- return await this.page.evaluate(({ cfg, px, py }) => {
168
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
169
- svg.setAttribute('width', cfg.width);
170
- svg.setAttribute('height', cfg.height);
171
- if (cfg.viewBox) svg.setAttribute('viewBox', cfg.viewBox);
172
- if (cfg.preserveAspectRatio) svg.setAttribute('preserveAspectRatio', cfg.preserveAspectRatio);
173
-
174
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
175
- if (cfg.transform) rect.setAttribute('transform', cfg.transform);
176
- svg.appendChild(rect);
177
-
178
- document.body.appendChild(svg);
179
-
180
- const ctm = rect.getCTM();
181
- const point = svg.createSVGPoint();
182
- point.x = px;
183
- point.y = py;
184
- const transformed = point.matrixTransform(ctm);
185
-
186
- document.body.removeChild(svg);
187
-
188
- return { x: transformed.x, y: transformed.y };
189
- }, { cfg: config, px: x, py: y });
175
+ return await this.page.evaluate(
176
+ ({ cfg, px, py }) => {
177
+ const svg = document.createElementNS(
178
+ "http://www.w3.org/2000/svg",
179
+ "svg",
180
+ );
181
+ svg.setAttribute("width", cfg.width);
182
+ svg.setAttribute("height", cfg.height);
183
+ if (cfg.viewBox) svg.setAttribute("viewBox", cfg.viewBox);
184
+ if (cfg.preserveAspectRatio)
185
+ svg.setAttribute("preserveAspectRatio", cfg.preserveAspectRatio);
186
+
187
+ const rect = document.createElementNS(
188
+ "http://www.w3.org/2000/svg",
189
+ "rect",
190
+ );
191
+ if (cfg.transform) rect.setAttribute("transform", cfg.transform);
192
+ svg.appendChild(rect);
193
+
194
+ document.body.appendChild(svg);
195
+
196
+ const ctm = rect.getCTM();
197
+ const point = svg.createSVGPoint();
198
+ point.x = px;
199
+ point.y = py;
200
+ const transformed = point.matrixTransform(ctm);
201
+
202
+ document.body.removeChild(svg);
203
+
204
+ return { x: transformed.x, y: transformed.y };
205
+ },
206
+ { cfg: config, px: x, py: y },
207
+ );
190
208
  }
191
209
 
192
210
  /**
@@ -218,7 +236,7 @@ export class BrowserVerifier {
218
236
  f: Math.abs(browserCTM.f - libraryCTM.f),
219
237
  };
220
238
 
221
- const matches = Object.values(differences).every(d => d < tolerance);
239
+ const matches = Object.values(differences).every((d) => d < tolerance);
222
240
 
223
241
  return { matches, browserCTM, libraryCTM, differences };
224
242
  }
@@ -233,17 +251,27 @@ export class BrowserVerifier {
233
251
  * @param {number} [tolerance=1e-10] - Tolerance for comparison
234
252
  * @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
235
253
  */
236
- async verifyViewBoxTransform(width, height, viewBox, preserveAspectRatio = 'xMidYMid meet', tolerance = 1e-10) {
254
+ async verifyViewBoxTransform(
255
+ width,
256
+ height,
257
+ viewBox,
258
+ preserveAspectRatio = "xMidYMid meet",
259
+ tolerance = 1e-10,
260
+ ) {
237
261
  const vb = SVGFlatten.parseViewBox(viewBox);
238
262
  const par = SVGFlatten.parsePreserveAspectRatio(preserveAspectRatio);
239
263
  const matrix = SVGFlatten.computeViewBoxTransform(vb, width, height, par);
240
264
 
241
- return await this.verifyMatrix(matrix, {
242
- width,
243
- height,
244
- viewBox,
245
- preserveAspectRatio
246
- }, tolerance);
265
+ return await this.verifyMatrix(
266
+ matrix,
267
+ {
268
+ width,
269
+ height,
270
+ viewBox,
271
+ preserveAspectRatio,
272
+ },
273
+ tolerance,
274
+ );
247
275
  }
248
276
 
249
277
  /**
@@ -257,11 +285,15 @@ export class BrowserVerifier {
257
285
  const matrix = SVGFlatten.parseTransformAttribute(transform);
258
286
 
259
287
  // Use a simple 100x100 SVG without viewBox to test just the transform
260
- return await this.verifyMatrix(matrix, {
261
- width: 100,
262
- height: 100,
263
- transform
264
- }, tolerance);
288
+ return await this.verifyMatrix(
289
+ matrix,
290
+ {
291
+ width: 100,
292
+ height: 100,
293
+ transform,
294
+ },
295
+ tolerance,
296
+ );
265
297
  }
266
298
 
267
299
  /**
@@ -280,19 +312,19 @@ export class BrowserVerifier {
280
312
 
281
313
  const libPt = {
282
314
  x: libraryPoint.x.toNumber(),
283
- y: libraryPoint.y.toNumber()
315
+ y: libraryPoint.y.toNumber(),
284
316
  };
285
317
 
286
318
  const difference = Math.sqrt(
287
319
  Math.pow(browserPoint.x - libPt.x, 2) +
288
- Math.pow(browserPoint.y - libPt.y, 2)
320
+ Math.pow(browserPoint.y - libPt.y, 2),
289
321
  );
290
322
 
291
323
  return {
292
324
  matches: difference < tolerance,
293
325
  browserPoint,
294
326
  libraryPoint: libPt,
295
- difference
327
+ difference,
296
328
  };
297
329
  }
298
330
 
@@ -313,13 +345,13 @@ export class BrowserVerifier {
313
345
  tc.width,
314
346
  tc.height,
315
347
  tc.viewBox,
316
- tc.preserveAspectRatio || 'xMidYMid meet',
317
- tolerance
348
+ tc.preserveAspectRatio || "xMidYMid meet",
349
+ tolerance,
318
350
  );
319
351
 
320
352
  results.push({
321
353
  name: tc.name || `${tc.width}x${tc.height} viewBox="${tc.viewBox}"`,
322
- ...result
354
+ ...result,
323
355
  });
324
356
 
325
357
  if (result.matches) passed++;
@@ -360,12 +392,22 @@ export async function quickVerify(matrix, config, tolerance = 1e-10) {
360
392
  * @param {string} [preserveAspectRatio='xMidYMid meet'] - preserveAspectRatio
361
393
  * @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object}>}
362
394
  */
363
- export async function verifyViewBox(width, height, viewBox, preserveAspectRatio = 'xMidYMid meet') {
395
+ export async function verifyViewBox(
396
+ width,
397
+ height,
398
+ viewBox,
399
+ preserveAspectRatio = "xMidYMid meet",
400
+ ) {
364
401
  const verifier = new BrowserVerifier();
365
402
  await verifier.init({ headless: true });
366
403
 
367
404
  try {
368
- return await verifier.verifyViewBoxTransform(width, height, viewBox, preserveAspectRatio);
405
+ return await verifier.verifyViewBoxTransform(
406
+ width,
407
+ height,
408
+ viewBox,
409
+ preserveAspectRatio,
410
+ );
369
411
  } finally {
370
412
  await verifier.close();
371
413
  }
@@ -399,30 +441,126 @@ export async function verifyTransform(transform) {
399
441
  export async function runStandardTests(options = { verbose: true }) {
400
442
  const testCases = [
401
443
  // W3C SVG WG issue #215 cases
402
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'none', name: 'Issue #215: none' },
403
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMinYMin meet', name: 'Issue #215: xMinYMin meet' },
404
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMidYMid meet', name: 'Issue #215: xMidYMid meet' },
405
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMaxYMax meet', name: 'Issue #215: xMaxYMax meet' },
406
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMinYMin slice', name: 'Issue #215: xMinYMin slice' },
407
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMidYMid slice', name: 'Issue #215: xMidYMid slice' },
408
- { width: 21, height: 10, viewBox: '11 13 3 2', preserveAspectRatio: 'xMaxYMax slice', name: 'Issue #215: xMaxYMax slice' },
444
+ {
445
+ width: 21,
446
+ height: 10,
447
+ viewBox: "11 13 3 2",
448
+ preserveAspectRatio: "none",
449
+ name: "Issue #215: none",
450
+ },
451
+ {
452
+ width: 21,
453
+ height: 10,
454
+ viewBox: "11 13 3 2",
455
+ preserveAspectRatio: "xMinYMin meet",
456
+ name: "Issue #215: xMinYMin meet",
457
+ },
458
+ {
459
+ width: 21,
460
+ height: 10,
461
+ viewBox: "11 13 3 2",
462
+ preserveAspectRatio: "xMidYMid meet",
463
+ name: "Issue #215: xMidYMid meet",
464
+ },
465
+ {
466
+ width: 21,
467
+ height: 10,
468
+ viewBox: "11 13 3 2",
469
+ preserveAspectRatio: "xMaxYMax meet",
470
+ name: "Issue #215: xMaxYMax meet",
471
+ },
472
+ {
473
+ width: 21,
474
+ height: 10,
475
+ viewBox: "11 13 3 2",
476
+ preserveAspectRatio: "xMinYMin slice",
477
+ name: "Issue #215: xMinYMin slice",
478
+ },
479
+ {
480
+ width: 21,
481
+ height: 10,
482
+ viewBox: "11 13 3 2",
483
+ preserveAspectRatio: "xMidYMid slice",
484
+ name: "Issue #215: xMidYMid slice",
485
+ },
486
+ {
487
+ width: 21,
488
+ height: 10,
489
+ viewBox: "11 13 3 2",
490
+ preserveAspectRatio: "xMaxYMax slice",
491
+ name: "Issue #215: xMaxYMax slice",
492
+ },
409
493
 
410
494
  // Standard cases
411
- { width: 200, height: 200, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid meet', name: 'Square 2x scale' },
412
- { width: 100, height: 100, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid meet', name: '1:1 identity' },
413
- { width: 400, height: 200, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid meet', name: 'Wide viewport' },
414
- { width: 200, height: 400, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid meet', name: 'Tall viewport' },
495
+ {
496
+ width: 200,
497
+ height: 200,
498
+ viewBox: "0 0 100 100",
499
+ preserveAspectRatio: "xMidYMid meet",
500
+ name: "Square 2x scale",
501
+ },
502
+ {
503
+ width: 100,
504
+ height: 100,
505
+ viewBox: "0 0 100 100",
506
+ preserveAspectRatio: "xMidYMid meet",
507
+ name: "1:1 identity",
508
+ },
509
+ {
510
+ width: 400,
511
+ height: 200,
512
+ viewBox: "0 0 100 100",
513
+ preserveAspectRatio: "xMidYMid meet",
514
+ name: "Wide viewport",
515
+ },
516
+ {
517
+ width: 200,
518
+ height: 400,
519
+ viewBox: "0 0 100 100",
520
+ preserveAspectRatio: "xMidYMid meet",
521
+ name: "Tall viewport",
522
+ },
415
523
 
416
524
  // Non-zero origins
417
- { width: 300, height: 200, viewBox: '50 50 100 100', preserveAspectRatio: 'xMidYMid meet', name: 'Offset origin' },
418
- { width: 300, height: 200, viewBox: '-50 -50 200 200', preserveAspectRatio: 'xMidYMid meet', name: 'Negative origin' },
525
+ {
526
+ width: 300,
527
+ height: 200,
528
+ viewBox: "50 50 100 100",
529
+ preserveAspectRatio: "xMidYMid meet",
530
+ name: "Offset origin",
531
+ },
532
+ {
533
+ width: 300,
534
+ height: 200,
535
+ viewBox: "-50 -50 200 200",
536
+ preserveAspectRatio: "xMidYMid meet",
537
+ name: "Negative origin",
538
+ },
419
539
 
420
540
  // Stretch
421
- { width: 200, height: 100, viewBox: '0 0 100 100', preserveAspectRatio: 'none', name: 'Stretch non-uniform' },
541
+ {
542
+ width: 200,
543
+ height: 100,
544
+ viewBox: "0 0 100 100",
545
+ preserveAspectRatio: "none",
546
+ name: "Stretch non-uniform",
547
+ },
422
548
 
423
549
  // Slice modes
424
- { width: 200, height: 400, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid slice', name: 'Tall slice' },
425
- { width: 400, height: 200, viewBox: '0 0 100 100', preserveAspectRatio: 'xMidYMid slice', name: 'Wide slice' },
550
+ {
551
+ width: 200,
552
+ height: 400,
553
+ viewBox: "0 0 100 100",
554
+ preserveAspectRatio: "xMidYMid slice",
555
+ name: "Tall slice",
556
+ },
557
+ {
558
+ width: 400,
559
+ height: 200,
560
+ viewBox: "0 0 100 100",
561
+ preserveAspectRatio: "xMidYMid slice",
562
+ name: "Wide slice",
563
+ },
426
564
  ];
427
565
 
428
566
  const verifier = new BrowserVerifier();
@@ -432,19 +570,21 @@ export async function runStandardTests(options = { verbose: true }) {
432
570
  const { passed, failed, results } = await verifier.runBatch(testCases);
433
571
 
434
572
  if (options.verbose) {
435
- console.log('\n=== SVG-Matrix Browser Verification ===\n');
573
+ console.log("\n=== SVG-Matrix Browser Verification ===\n");
436
574
 
437
575
  for (const r of results) {
438
- const icon = r.matches ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
576
+ const icon = r.matches ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
439
577
  console.log(` ${icon} ${r.name}`);
440
578
  }
441
579
 
442
- console.log('\n' + ''.repeat(50));
580
+ console.log("\n" + "".repeat(50));
443
581
  if (failed === 0) {
444
582
  console.log(`\x1b[32mAll ${passed} tests PASSED!\x1b[0m`);
445
- console.log('Library matches browser\'s W3C SVG2 implementation.\n');
583
+ console.log("Library matches browser's W3C SVG2 implementation.\n");
446
584
  } else {
447
- console.log(`\x1b[31m${failed} tests FAILED\x1b[0m, ${passed} passed\n`);
585
+ console.log(
586
+ `\x1b[31m${failed} tests FAILED\x1b[0m, ${passed} passed\n`,
587
+ );
448
588
  }
449
589
  }
450
590
 
@@ -459,5 +599,5 @@ export default {
459
599
  quickVerify,
460
600
  verifyViewBox,
461
601
  verifyTransform,
462
- runStandardTests
602
+ runStandardTests,
463
603
  };