@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/browser-verify.js
CHANGED
|
@@ -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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
{
|