@emasoft/svg-matrix 1.0.5 → 1.0.6
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/README.md +317 -396
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1264 -105
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/preserveAspectRatio_SVG.svg +0 -63
- package/samples/test.svg +0 -39
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node test/examples.js",
|
|
9
|
+
"test:browser": "node test/browser-verify.mjs",
|
|
10
|
+
"test:playwright": "node test/playwright-diagnose.js",
|
|
9
11
|
"ci-test": "npm ci && npm test",
|
|
10
12
|
"prepublishOnly": "npm test"
|
|
11
13
|
},
|
|
@@ -40,7 +42,23 @@
|
|
|
40
42
|
},
|
|
41
43
|
"author": "Emasoft",
|
|
42
44
|
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=24.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"src/",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"README.md"
|
|
52
|
+
],
|
|
43
53
|
"dependencies": {
|
|
44
54
|
"decimal.js": "^10.6.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"playwright": "^1.57.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"playwright": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Verification Module
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to verify SVG coordinate transformations against
|
|
5
|
+
* Chrome's native implementation using Playwright.
|
|
6
|
+
*
|
|
7
|
+
* This is the authoritative way to verify correctness since browsers
|
|
8
|
+
* implement the W3C SVG2 specification.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: This module requires Playwright as an optional peer dependency.
|
|
11
|
+
* Install with: npm install playwright && npx playwright install chromium
|
|
12
|
+
*
|
|
13
|
+
* @module browser-verify
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import Decimal from 'decimal.js';
|
|
17
|
+
import { Matrix } from './matrix.js';
|
|
18
|
+
import * as SVGFlatten from './svg-flatten.js';
|
|
19
|
+
|
|
20
|
+
// Playwright is loaded dynamically to avoid crashes when not installed
|
|
21
|
+
let chromium = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load Playwright dynamically. Throws helpful error if not installed.
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async function loadPlaywright() {
|
|
28
|
+
if (chromium) return;
|
|
29
|
+
try {
|
|
30
|
+
const playwright = await import('playwright');
|
|
31
|
+
chromium = playwright.chromium;
|
|
32
|
+
} catch (e) {
|
|
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'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set high precision
|
|
41
|
+
Decimal.set({ precision: 80 });
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Browser verification session.
|
|
45
|
+
* Manages a Chromium browser instance for CTM verification.
|
|
46
|
+
*/
|
|
47
|
+
export class BrowserVerifier {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.browser = null;
|
|
50
|
+
this.page = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the browser session.
|
|
55
|
+
* Must be called before using verification methods.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} options - Playwright launch options
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
* @throws {Error} If Playwright is not installed
|
|
60
|
+
*/
|
|
61
|
+
async init(options = {}) {
|
|
62
|
+
await loadPlaywright();
|
|
63
|
+
this.browser = await chromium.launch(options);
|
|
64
|
+
this.page = await this.browser.newPage();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Close the browser session.
|
|
69
|
+
* Should be called when done with verification.
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*/
|
|
73
|
+
async close() {
|
|
74
|
+
if (this.browser) {
|
|
75
|
+
await this.browser.close();
|
|
76
|
+
this.browser = null;
|
|
77
|
+
this.page = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the browser's native CTM for an SVG element configuration.
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} config - SVG configuration
|
|
85
|
+
* @param {number} config.width - Viewport width
|
|
86
|
+
* @param {number} config.height - Viewport height
|
|
87
|
+
* @param {string} [config.viewBox] - viewBox attribute
|
|
88
|
+
* @param {string} [config.preserveAspectRatio] - preserveAspectRatio attribute
|
|
89
|
+
* @param {string} [config.transform] - transform attribute on child element
|
|
90
|
+
* @returns {Promise<{a: number, b: number, c: number, d: number, e: number, f: number}>}
|
|
91
|
+
*/
|
|
92
|
+
async getBrowserCTM(config) {
|
|
93
|
+
if (!this.page) {
|
|
94
|
+
throw new Error('BrowserVerifier not initialized. Call init() first.');
|
|
95
|
+
}
|
|
96
|
+
|
|
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);
|
|
110
|
+
svg.appendChild(rect);
|
|
111
|
+
|
|
112
|
+
document.body.appendChild(svg);
|
|
113
|
+
const ctm = rect.getCTM();
|
|
114
|
+
document.body.removeChild(svg);
|
|
115
|
+
|
|
116
|
+
return { a: ctm.a, b: ctm.b, c: ctm.c, d: ctm.d, e: ctm.e, f: ctm.f };
|
|
117
|
+
}, config);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the browser's screen CTM (includes page scroll/zoom).
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} config - SVG configuration (same as getBrowserCTM)
|
|
124
|
+
* @returns {Promise<{a: number, b: number, c: number, d: number, e: number, f: number}>}
|
|
125
|
+
*/
|
|
126
|
+
async getBrowserScreenCTM(config) {
|
|
127
|
+
if (!this.page) {
|
|
128
|
+
throw new Error('BrowserVerifier not initialized. Call init() first.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
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);
|
|
144
|
+
svg.appendChild(rect);
|
|
145
|
+
|
|
146
|
+
document.body.appendChild(svg);
|
|
147
|
+
const ctm = rect.getScreenCTM();
|
|
148
|
+
document.body.removeChild(svg);
|
|
149
|
+
|
|
150
|
+
return { a: ctm.a, b: ctm.b, c: ctm.c, d: ctm.d, e: ctm.e, f: ctm.f };
|
|
151
|
+
}, config);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Transform a point using the browser's native SVG transformation.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} config - SVG configuration
|
|
158
|
+
* @param {number} x - X coordinate in local space
|
|
159
|
+
* @param {number} y - Y coordinate in local space
|
|
160
|
+
* @returns {Promise<{x: number, y: number}>} Transformed point
|
|
161
|
+
*/
|
|
162
|
+
async transformPoint(config, x, y) {
|
|
163
|
+
if (!this.page) {
|
|
164
|
+
throw new Error('BrowserVerifier not initialized. Call init() first.');
|
|
165
|
+
}
|
|
166
|
+
|
|
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 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verify a matrix against the browser's CTM.
|
|
194
|
+
*
|
|
195
|
+
* @param {Matrix} matrix - Our computed matrix
|
|
196
|
+
* @param {Object} config - SVG configuration to compare against
|
|
197
|
+
* @param {number} [tolerance=1e-10] - Tolerance for comparison
|
|
198
|
+
* @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
|
|
199
|
+
*/
|
|
200
|
+
async verifyMatrix(matrix, config, tolerance = 1e-10) {
|
|
201
|
+
const browserCTM = await this.getBrowserCTM(config);
|
|
202
|
+
|
|
203
|
+
const libraryCTM = {
|
|
204
|
+
a: matrix.data[0][0].toNumber(),
|
|
205
|
+
b: matrix.data[1][0].toNumber(),
|
|
206
|
+
c: matrix.data[0][1].toNumber(),
|
|
207
|
+
d: matrix.data[1][1].toNumber(),
|
|
208
|
+
e: matrix.data[0][2].toNumber(),
|
|
209
|
+
f: matrix.data[1][2].toNumber(),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const differences = {
|
|
213
|
+
a: Math.abs(browserCTM.a - libraryCTM.a),
|
|
214
|
+
b: Math.abs(browserCTM.b - libraryCTM.b),
|
|
215
|
+
c: Math.abs(browserCTM.c - libraryCTM.c),
|
|
216
|
+
d: Math.abs(browserCTM.d - libraryCTM.d),
|
|
217
|
+
e: Math.abs(browserCTM.e - libraryCTM.e),
|
|
218
|
+
f: Math.abs(browserCTM.f - libraryCTM.f),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const matches = Object.values(differences).every(d => d < tolerance);
|
|
222
|
+
|
|
223
|
+
return { matches, browserCTM, libraryCTM, differences };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Verify a viewBox transform computation.
|
|
228
|
+
*
|
|
229
|
+
* @param {number} width - Viewport width
|
|
230
|
+
* @param {number} height - Viewport height
|
|
231
|
+
* @param {string} viewBox - viewBox attribute
|
|
232
|
+
* @param {string} [preserveAspectRatio='xMidYMid meet'] - preserveAspectRatio
|
|
233
|
+
* @param {number} [tolerance=1e-10] - Tolerance for comparison
|
|
234
|
+
* @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
|
|
235
|
+
*/
|
|
236
|
+
async verifyViewBoxTransform(width, height, viewBox, preserveAspectRatio = 'xMidYMid meet', tolerance = 1e-10) {
|
|
237
|
+
const vb = SVGFlatten.parseViewBox(viewBox);
|
|
238
|
+
const par = SVGFlatten.parsePreserveAspectRatio(preserveAspectRatio);
|
|
239
|
+
const matrix = SVGFlatten.computeViewBoxTransform(vb, width, height, par);
|
|
240
|
+
|
|
241
|
+
return await this.verifyMatrix(matrix, {
|
|
242
|
+
width,
|
|
243
|
+
height,
|
|
244
|
+
viewBox,
|
|
245
|
+
preserveAspectRatio
|
|
246
|
+
}, tolerance);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Verify a transform attribute parsing.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} transform - SVG transform attribute string
|
|
253
|
+
* @param {number} [tolerance=1e-10] - Tolerance for comparison
|
|
254
|
+
* @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object, differences: Object}>}
|
|
255
|
+
*/
|
|
256
|
+
async verifyTransformAttribute(transform, tolerance = 1e-10) {
|
|
257
|
+
const matrix = SVGFlatten.parseTransformAttribute(transform);
|
|
258
|
+
|
|
259
|
+
// 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);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Verify a point transformation.
|
|
269
|
+
*
|
|
270
|
+
* @param {Matrix} matrix - Our computed matrix
|
|
271
|
+
* @param {number} x - X coordinate
|
|
272
|
+
* @param {number} y - Y coordinate
|
|
273
|
+
* @param {Object} config - SVG configuration
|
|
274
|
+
* @param {number} [tolerance=1e-10] - Tolerance for comparison
|
|
275
|
+
* @returns {Promise<{matches: boolean, browserPoint: Object, libraryPoint: Object, difference: number}>}
|
|
276
|
+
*/
|
|
277
|
+
async verifyPointTransform(matrix, x, y, config, tolerance = 1e-10) {
|
|
278
|
+
const browserPoint = await this.transformPoint(config, x, y);
|
|
279
|
+
const libraryPoint = SVGFlatten.applyToPoint(matrix, x, y);
|
|
280
|
+
|
|
281
|
+
const libPt = {
|
|
282
|
+
x: libraryPoint.x.toNumber(),
|
|
283
|
+
y: libraryPoint.y.toNumber()
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const difference = Math.sqrt(
|
|
287
|
+
Math.pow(browserPoint.x - libPt.x, 2) +
|
|
288
|
+
Math.pow(browserPoint.y - libPt.y, 2)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
matches: difference < tolerance,
|
|
293
|
+
browserPoint,
|
|
294
|
+
libraryPoint: libPt,
|
|
295
|
+
difference
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Run a batch of verification tests.
|
|
301
|
+
*
|
|
302
|
+
* @param {Array<Object>} testCases - Array of test configurations
|
|
303
|
+
* @param {number} [tolerance=1e-10] - Tolerance for comparison
|
|
304
|
+
* @returns {Promise<{passed: number, failed: number, results: Array}>}
|
|
305
|
+
*/
|
|
306
|
+
async runBatch(testCases, tolerance = 1e-10) {
|
|
307
|
+
const results = [];
|
|
308
|
+
let passed = 0;
|
|
309
|
+
let failed = 0;
|
|
310
|
+
|
|
311
|
+
for (const tc of testCases) {
|
|
312
|
+
const result = await this.verifyViewBoxTransform(
|
|
313
|
+
tc.width,
|
|
314
|
+
tc.height,
|
|
315
|
+
tc.viewBox,
|
|
316
|
+
tc.preserveAspectRatio || 'xMidYMid meet',
|
|
317
|
+
tolerance
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
results.push({
|
|
321
|
+
name: tc.name || `${tc.width}x${tc.height} viewBox="${tc.viewBox}"`,
|
|
322
|
+
...result
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (result.matches) passed++;
|
|
326
|
+
else failed++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { passed, failed, results };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Quick verification function - creates a temporary browser session.
|
|
335
|
+
* For multiple verifications, use BrowserVerifier class instead.
|
|
336
|
+
*
|
|
337
|
+
* @param {Matrix} matrix - Matrix to verify
|
|
338
|
+
* @param {Object} config - SVG configuration
|
|
339
|
+
* @param {number} [tolerance=1e-10] - Tolerance
|
|
340
|
+
* @returns {Promise<boolean>} True if matrix matches browser's CTM
|
|
341
|
+
*/
|
|
342
|
+
export async function quickVerify(matrix, config, tolerance = 1e-10) {
|
|
343
|
+
const verifier = new BrowserVerifier();
|
|
344
|
+
await verifier.init({ headless: true });
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const result = await verifier.verifyMatrix(matrix, config, tolerance);
|
|
348
|
+
return result.matches;
|
|
349
|
+
} finally {
|
|
350
|
+
await verifier.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Verify a viewBox transform with a quick one-off browser session.
|
|
356
|
+
*
|
|
357
|
+
* @param {number} width - Viewport width
|
|
358
|
+
* @param {number} height - Viewport height
|
|
359
|
+
* @param {string} viewBox - viewBox attribute
|
|
360
|
+
* @param {string} [preserveAspectRatio='xMidYMid meet'] - preserveAspectRatio
|
|
361
|
+
* @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object}>}
|
|
362
|
+
*/
|
|
363
|
+
export async function verifyViewBox(width, height, viewBox, preserveAspectRatio = 'xMidYMid meet') {
|
|
364
|
+
const verifier = new BrowserVerifier();
|
|
365
|
+
await verifier.init({ headless: true });
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
return await verifier.verifyViewBoxTransform(width, height, viewBox, preserveAspectRatio);
|
|
369
|
+
} finally {
|
|
370
|
+
await verifier.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Verify a transform attribute parsing with a quick one-off browser session.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} transform - SVG transform attribute string
|
|
378
|
+
* @returns {Promise<{matches: boolean, browserCTM: Object, libraryCTM: Object}>}
|
|
379
|
+
*/
|
|
380
|
+
export async function verifyTransform(transform) {
|
|
381
|
+
const verifier = new BrowserVerifier();
|
|
382
|
+
await verifier.init({ headless: true });
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
return await verifier.verifyTransformAttribute(transform);
|
|
386
|
+
} finally {
|
|
387
|
+
await verifier.close();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Run the standard verification test suite.
|
|
393
|
+
* Tests all common viewBox/preserveAspectRatio combinations.
|
|
394
|
+
*
|
|
395
|
+
* @param {Object} [options] - Options
|
|
396
|
+
* @param {boolean} [options.verbose=true] - Print results to console
|
|
397
|
+
* @returns {Promise<{passed: number, failed: number, results: Array}>}
|
|
398
|
+
*/
|
|
399
|
+
export async function runStandardTests(options = { verbose: true }) {
|
|
400
|
+
const testCases = [
|
|
401
|
+
// 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' },
|
|
409
|
+
|
|
410
|
+
// 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' },
|
|
415
|
+
|
|
416
|
+
// 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' },
|
|
419
|
+
|
|
420
|
+
// Stretch
|
|
421
|
+
{ width: 200, height: 100, viewBox: '0 0 100 100', preserveAspectRatio: 'none', name: 'Stretch non-uniform' },
|
|
422
|
+
|
|
423
|
+
// 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' },
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
const verifier = new BrowserVerifier();
|
|
429
|
+
await verifier.init({ headless: true });
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const { passed, failed, results } = await verifier.runBatch(testCases);
|
|
433
|
+
|
|
434
|
+
if (options.verbose) {
|
|
435
|
+
console.log('\n=== SVG-Matrix Browser Verification ===\n');
|
|
436
|
+
|
|
437
|
+
for (const r of results) {
|
|
438
|
+
const icon = r.matches ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
439
|
+
console.log(` ${icon} ${r.name}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log('\n' + '─'.repeat(50));
|
|
443
|
+
if (failed === 0) {
|
|
444
|
+
console.log(`\x1b[32mAll ${passed} tests PASSED!\x1b[0m`);
|
|
445
|
+
console.log('Library matches browser\'s W3C SVG2 implementation.\n');
|
|
446
|
+
} else {
|
|
447
|
+
console.log(`\x1b[31m${failed} tests FAILED\x1b[0m, ${passed} passed\n`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { passed, failed, results };
|
|
452
|
+
} finally {
|
|
453
|
+
await verifier.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export default {
|
|
458
|
+
BrowserVerifier,
|
|
459
|
+
quickVerify,
|
|
460
|
+
verifyViewBox,
|
|
461
|
+
verifyTransform,
|
|
462
|
+
runStandardTests
|
|
463
|
+
};
|