@emasoft/svg-matrix 1.0.26 → 1.0.28

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.
@@ -0,0 +1,444 @@
1
+ /**
2
+ * SVG 2.0 Polyfill Generator
3
+ *
4
+ * Detects SVG 2.0 features (mesh gradients, hatches) and generates inline
5
+ * JavaScript polyfills for browser compatibility.
6
+ *
7
+ * Uses the existing mesh-gradient.js math for Coons patch evaluation.
8
+ * All polyfills are embedded inline in the SVG for self-contained output.
9
+ *
10
+ * @module svg2-polyfills
11
+ */
12
+
13
+ import { SVGElement } from './svg-parser.js';
14
+
15
+ /**
16
+ * SVG 2.0 features that can be polyfilled
17
+ */
18
+ export const SVG2_FEATURES = {
19
+ MESH_GRADIENT: 'meshGradient',
20
+ HATCH: 'hatch',
21
+ CONTEXT_PAINT: 'context-paint',
22
+ AUTO_START_REVERSE: 'auto-start-reverse'
23
+ };
24
+
25
+ /**
26
+ * Detect SVG 2.0 features that need polyfills in a document.
27
+ *
28
+ * @param {Object} doc - Parsed SVG document
29
+ * @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
30
+ */
31
+ export function detectSVG2Features(doc) {
32
+ const features = {
33
+ meshGradients: [],
34
+ hatches: [],
35
+ contextPaint: false,
36
+ autoStartReverse: false
37
+ };
38
+
39
+ const walk = (el) => {
40
+ if (!el) return;
41
+
42
+ // Check tag name for mesh gradient (case-insensitive)
43
+ const tagName = el.tagName?.toLowerCase();
44
+ if (tagName === 'meshgradient') {
45
+ const id = el.getAttribute('id');
46
+ if (id) features.meshGradients.push(id);
47
+ }
48
+
49
+ // Check for hatch element
50
+ if (tagName === 'hatch') {
51
+ const id = el.getAttribute('id');
52
+ if (id) features.hatches.push(id);
53
+ }
54
+
55
+ // Check for context-paint in fill/stroke
56
+ const fill = el.getAttribute('fill');
57
+ const stroke = el.getAttribute('stroke');
58
+ if (fill === 'context-fill' || fill === 'context-stroke' ||
59
+ stroke === 'context-fill' || stroke === 'context-stroke') {
60
+ features.contextPaint = true;
61
+ }
62
+
63
+ // Check for auto-start-reverse in markers
64
+ const orient = el.getAttribute('orient');
65
+ if (orient === 'auto-start-reverse') {
66
+ features.autoStartReverse = true;
67
+ }
68
+
69
+ // Recurse into children
70
+ if (el.children) {
71
+ for (const child of el.children) {
72
+ walk(child);
73
+ }
74
+ }
75
+ };
76
+
77
+ walk(doc);
78
+ return features;
79
+ }
80
+
81
+ /**
82
+ * Check if document needs any SVG 2 polyfills.
83
+ *
84
+ * @param {Object} doc - Parsed SVG document
85
+ * @returns {boolean} True if polyfills are needed
86
+ */
87
+ export function needsPolyfills(doc) {
88
+ const features = detectSVG2Features(doc);
89
+ return features.meshGradients.length > 0 ||
90
+ features.hatches.length > 0 ||
91
+ features.contextPaint ||
92
+ features.autoStartReverse;
93
+ }
94
+
95
+ /**
96
+ * Generate the mesh gradient polyfill code.
97
+ * This polyfill renders mesh gradients to canvas and uses them as image fills.
98
+ *
99
+ * @returns {string} JavaScript polyfill code
100
+ */
101
+ function generateMeshPolyfillCode() {
102
+ return `
103
+ // Mesh Gradient Polyfill - SVG 2.0 to Canvas fallback
104
+ // Generated by svg-matrix
105
+ (function() {
106
+ 'use strict';
107
+
108
+ // Skip if browser supports mesh gradients natively
109
+ if (typeof document.createElementNS('http://www.w3.org/2000/svg', 'meshGradient').x !== 'undefined') {
110
+ return;
111
+ }
112
+
113
+ // Find all mesh gradients
114
+ var meshes = document.querySelectorAll('meshGradient, meshgradient');
115
+ if (!meshes.length) return;
116
+
117
+ // Parse color string to RGBA
118
+ function parseColor(str) {
119
+ if (!str) return {r: 0, g: 0, b: 0, a: 255};
120
+ var m = str.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)/);
121
+ if (m) return {r: +m[1], g: +m[2], b: +m[3], a: m[4] ? Math.round(+m[4] * 255) : 255};
122
+ if (str[0] === '#') {
123
+ var hex = str.slice(1);
124
+ if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
125
+ return {r: parseInt(hex.slice(0,2), 16), g: parseInt(hex.slice(2,4), 16), b: parseInt(hex.slice(4,6), 16), a: 255};
126
+ }
127
+ return {r: 0, g: 0, b: 0, a: 255};
128
+ }
129
+
130
+ // Bilinear color interpolation
131
+ function bilinearColor(c00, c10, c01, c11, u, v) {
132
+ var mu = 1 - u, mv = 1 - v;
133
+ return {
134
+ r: Math.round(mu*mv*c00.r + u*mv*c10.r + mu*v*c01.r + u*v*c11.r),
135
+ g: Math.round(mu*mv*c00.g + u*mv*c10.g + mu*v*c01.g + u*v*c11.g),
136
+ b: Math.round(mu*mv*c00.b + u*mv*c10.b + mu*v*c01.b + u*v*c11.b),
137
+ a: Math.round(mu*mv*c00.a + u*mv*c10.a + mu*v*c01.a + u*v*c11.a)
138
+ };
139
+ }
140
+
141
+ // Evaluate cubic Bezier at t
142
+ function evalBezier(p0, p1, p2, p3, t) {
143
+ var mt = 1 - t, mt2 = mt * mt, mt3 = mt2 * mt;
144
+ var t2 = t * t, t3 = t2 * t;
145
+ return {
146
+ x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
147
+ y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
148
+ };
149
+ }
150
+
151
+ // Process each mesh gradient
152
+ meshes.forEach(function(mesh) {
153
+ var id = mesh.getAttribute('id');
154
+ if (!id) return;
155
+
156
+ // Create canvas for rasterization
157
+ var canvas = document.createElement('canvas');
158
+ var ctx = canvas.getContext('2d');
159
+
160
+ // Get bounding box from referencing elements
161
+ var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
162
+ if (!refs.length) return;
163
+
164
+ var bbox = refs[0].getBBox();
165
+ var size = Math.max(bbox.width, bbox.height, 256);
166
+ canvas.width = canvas.height = size;
167
+
168
+ // Parse mesh patches
169
+ var patches = [];
170
+ var rows = mesh.querySelectorAll('meshrow');
171
+ rows.forEach(function(row) {
172
+ var rowPatches = row.querySelectorAll('meshpatch');
173
+ rowPatches.forEach(function(patch) {
174
+ var stops = patch.querySelectorAll('stop');
175
+ if (stops.length >= 4) {
176
+ patches.push({
177
+ colors: [
178
+ parseColor(stops[0].getAttribute('stop-color') || stops[0].style.stopColor),
179
+ parseColor(stops[1].getAttribute('stop-color') || stops[1].style.stopColor),
180
+ parseColor(stops[2].getAttribute('stop-color') || stops[2].style.stopColor),
181
+ parseColor(stops[3].getAttribute('stop-color') || stops[3].style.stopColor)
182
+ ]
183
+ });
184
+ }
185
+ });
186
+ });
187
+
188
+ // Render patches with bilinear interpolation
189
+ if (patches.length > 0) {
190
+ var imgData = ctx.createImageData(size, size);
191
+ var data = imgData.data;
192
+ var patch = patches[0];
193
+ for (var y = 0; y < size; y++) {
194
+ for (var x = 0; x < size; x++) {
195
+ var u = x / (size - 1);
196
+ var v = y / (size - 1);
197
+ var c = bilinearColor(patch.colors[0], patch.colors[1], patch.colors[2], patch.colors[3], u, v);
198
+ var i = (y * size + x) * 4;
199
+ data[i] = c.r;
200
+ data[i+1] = c.g;
201
+ data[i+2] = c.b;
202
+ data[i+3] = c.a;
203
+ }
204
+ }
205
+ ctx.putImageData(imgData, 0, 0);
206
+ }
207
+
208
+ // Create pattern from canvas
209
+ var dataUrl = canvas.toDataURL('image/png');
210
+ var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
211
+ pattern.setAttribute('id', id + '_polyfill');
212
+ pattern.setAttribute('patternUnits', 'objectBoundingBox');
213
+ pattern.setAttribute('width', '1');
214
+ pattern.setAttribute('height', '1');
215
+
216
+ var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
217
+ img.setAttribute('href', dataUrl);
218
+ img.setAttribute('width', '1');
219
+ img.setAttribute('height', '1');
220
+ img.setAttribute('preserveAspectRatio', 'none');
221
+ pattern.appendChild(img);
222
+
223
+ // Add pattern to defs
224
+ var defs = mesh.closest('svg').querySelector('defs') || (function() {
225
+ var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
226
+ mesh.closest('svg').insertBefore(d, mesh.closest('svg').firstChild);
227
+ return d;
228
+ })();
229
+ defs.appendChild(pattern);
230
+
231
+ // Update references to use polyfill pattern
232
+ refs.forEach(function(ref) {
233
+ if (ref.getAttribute('fill') === 'url(#' + id + ')') {
234
+ ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
235
+ }
236
+ if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
237
+ ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
238
+ }
239
+ });
240
+ });
241
+ })();
242
+ `;
243
+ }
244
+
245
+ /**
246
+ * Generate the hatch pattern polyfill code.
247
+ * Converts SVG 2 hatch elements to SVG 1.1 pattern elements.
248
+ *
249
+ * @returns {string} JavaScript polyfill code
250
+ */
251
+ function generateHatchPolyfillCode() {
252
+ return `
253
+ // Hatch Pattern Polyfill - SVG 2.0 to SVG 1.1 pattern conversion
254
+ // Generated by svg-matrix
255
+ (function() {
256
+ 'use strict';
257
+
258
+ // Find all hatch elements
259
+ var hatches = document.querySelectorAll('hatch');
260
+ if (!hatches.length) return;
261
+
262
+ hatches.forEach(function(hatch) {
263
+ var id = hatch.getAttribute('id');
264
+ if (!id) return;
265
+
266
+ // Get hatch properties
267
+ var href = hatch.getAttribute('href') || hatch.getAttribute('xlink:href');
268
+ var hatchUnits = hatch.getAttribute('hatchUnits') || 'objectBoundingBox';
269
+ var hatchContentUnits = hatch.getAttribute('hatchContentUnits') || 'userSpaceOnUse';
270
+ var pitch = parseFloat(hatch.getAttribute('pitch')) || 8;
271
+ var rotate = parseFloat(hatch.getAttribute('rotate')) || 0;
272
+ var transform = hatch.getAttribute('transform') || '';
273
+
274
+ // Create equivalent pattern
275
+ var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
276
+ pattern.setAttribute('id', id + '_polyfill');
277
+ pattern.setAttribute('patternUnits', hatchUnits);
278
+ pattern.setAttribute('width', pitch);
279
+ pattern.setAttribute('height', pitch);
280
+
281
+ if (transform || rotate) {
282
+ var fullTransform = transform;
283
+ if (rotate) fullTransform += ' rotate(' + rotate + ')';
284
+ pattern.setAttribute('patternTransform', fullTransform.trim());
285
+ }
286
+
287
+ // Copy hatchpath children as lines
288
+ var hatchpaths = hatch.querySelectorAll('hatchpath, hatchPath');
289
+ hatchpaths.forEach(function(hp) {
290
+ var d = hp.getAttribute('d');
291
+ var strokeColor = hp.getAttribute('stroke') || 'black';
292
+ var strokeWidth = hp.getAttribute('stroke-width') || '1';
293
+
294
+ var line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
295
+ line.setAttribute('d', d || 'M0,0 L' + pitch + ',0');
296
+ line.setAttribute('stroke', strokeColor);
297
+ line.setAttribute('stroke-width', strokeWidth);
298
+ line.setAttribute('fill', 'none');
299
+ pattern.appendChild(line);
300
+ });
301
+
302
+ // If no hatchpaths, create default diagonal line
303
+ if (!hatchpaths.length) {
304
+ var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
305
+ line.setAttribute('x1', '0');
306
+ line.setAttribute('y1', '0');
307
+ line.setAttribute('x2', pitch);
308
+ line.setAttribute('y2', pitch);
309
+ line.setAttribute('stroke', 'black');
310
+ line.setAttribute('stroke-width', '1');
311
+ pattern.appendChild(line);
312
+ }
313
+
314
+ // Add pattern to defs
315
+ var defs = hatch.closest('svg').querySelector('defs') || (function() {
316
+ var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
317
+ hatch.closest('svg').insertBefore(d, hatch.closest('svg').firstChild);
318
+ return d;
319
+ })();
320
+ defs.appendChild(pattern);
321
+
322
+ // Update references
323
+ var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
324
+ refs.forEach(function(ref) {
325
+ if (ref.getAttribute('fill') === 'url(#' + id + ')') {
326
+ ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
327
+ }
328
+ if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
329
+ ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
330
+ }
331
+ });
332
+ });
333
+ })();
334
+ `;
335
+ }
336
+
337
+ /**
338
+ * Generate complete polyfill script based on detected features.
339
+ *
340
+ * @param {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} features - Detected features
341
+ * @returns {string|null} Complete polyfill script or null if none needed
342
+ */
343
+ export function generatePolyfillScript(features) {
344
+ const parts = [];
345
+
346
+ parts.push('/* SVG 2.0 Polyfills - Generated by svg-matrix */');
347
+
348
+ if (features.meshGradients.length > 0) {
349
+ parts.push(generateMeshPolyfillCode());
350
+ }
351
+
352
+ if (features.hatches.length > 0) {
353
+ parts.push(generateHatchPolyfillCode());
354
+ }
355
+
356
+ if (parts.length === 1) {
357
+ return null; // Only header, no actual polyfills
358
+ }
359
+
360
+ return parts.join('\n');
361
+ }
362
+
363
+ /**
364
+ * Inject polyfill script into SVG document.
365
+ *
366
+ * @param {Object} doc - Parsed SVG document
367
+ * @param {Object} [options] - Options
368
+ * @param {boolean} [options.force=false] - Force injection even if no features detected
369
+ * @param {Object} [options.features] - Pre-detected features (use instead of re-detecting)
370
+ * @returns {Object} The document (modified in place)
371
+ */
372
+ export function injectPolyfills(doc, options = {}) {
373
+ // Use pre-detected features if provided (for when pipeline has removed SVG2 elements)
374
+ const features = options.features || detectSVG2Features(doc);
375
+
376
+ // Check if polyfills are needed
377
+ if (!options.force &&
378
+ features.meshGradients.length === 0 &&
379
+ features.hatches.length === 0) {
380
+ return doc;
381
+ }
382
+
383
+ const script = generatePolyfillScript(features);
384
+ if (!script) return doc;
385
+
386
+ // Find or create the SVG root
387
+ const svg = doc.documentElement || doc;
388
+
389
+ // Create a proper SVGElement for the script
390
+ // The script content uses CDATA to avoid XML escaping issues
391
+ const scriptEl = new SVGElement('script', {
392
+ type: 'text/javascript',
393
+ id: 'svg-matrix-polyfill'
394
+ }, [], script);
395
+
396
+ // Insert script at beginning of SVG (after defs if present, else at start)
397
+ if (svg.children && svg.children.length > 0) {
398
+ // Find first non-defs element to insert before
399
+ let insertIdx = 0;
400
+ for (let i = 0; i < svg.children.length; i++) {
401
+ if (svg.children[i].tagName === 'defs') {
402
+ insertIdx = i + 1;
403
+ break;
404
+ }
405
+ }
406
+ svg.children.splice(insertIdx, 0, scriptEl);
407
+ } else if (svg.children) {
408
+ svg.children.push(scriptEl);
409
+ }
410
+
411
+ return doc;
412
+ }
413
+
414
+ /**
415
+ * Remove polyfill scripts from SVG document.
416
+ *
417
+ * @param {Object} doc - Parsed SVG document
418
+ * @returns {Object} The document (modified in place)
419
+ */
420
+ export function removePolyfills(doc) {
421
+ const walk = (el) => {
422
+ if (!el || !el.children) return;
423
+
424
+ // Remove script elements that are svg-matrix polyfills
425
+ el.children = el.children.filter(child => {
426
+ if (child.tagName === 'script') {
427
+ const content = child.textContent || '';
428
+ if (content.includes('SVG 2.0 Polyfill') ||
429
+ content.includes('Generated by svg-matrix')) {
430
+ return false;
431
+ }
432
+ }
433
+ return true;
434
+ });
435
+
436
+ // Recurse
437
+ for (const child of el.children) {
438
+ walk(child);
439
+ }
440
+ };
441
+
442
+ walk(doc);
443
+ return doc;
444
+ }