@emasoft/svg-matrix 1.0.27 → 1.0.29

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 +994 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +744 -184
  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 +404 -0
  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 +48 -19
  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 +16411 -3298
  34. package/src/svg2-polyfills.js +114 -245
  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
@@ -4,12 +4,68 @@
4
4
  * Detects SVG 2.0 features (mesh gradients, hatches) and generates inline
5
5
  * JavaScript polyfills for browser compatibility.
6
6
  *
7
- * Uses the existing mesh-gradient.js math for Coons patch evaluation.
7
+ * Uses the Inkscape mesh.js polyfill by Tavmjong Bah for mesh gradient support.
8
+ * The mesh polyfill is licensed under GPLv3 - see src/vendor/inkscape-mesh-polyfill.js
9
+ * Hatch polyfills use a simplified MIT-licensed implementation.
10
+ *
8
11
  * All polyfills are embedded inline in the SVG for self-contained output.
9
12
  *
10
13
  * @module svg2-polyfills
11
14
  */
12
15
 
16
+ import { SVGElement } from './svg-parser.js';
17
+ import { readFileSync } from 'fs';
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, join } from 'path';
20
+
21
+ // Load Inkscape polyfills at module initialization (minified + full versions)
22
+ let INKSCAPE_MESH_POLYFILL_MIN = '';
23
+ let INKSCAPE_MESH_POLYFILL_FULL = '';
24
+ let INKSCAPE_HATCH_POLYFILL_MIN = '';
25
+ let INKSCAPE_HATCH_POLYFILL_FULL = '';
26
+
27
+ try {
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+
31
+ // Load mesh gradient polyfills (GPLv3 by Tavmjong Bah)
32
+ // Minified version (default, ~16KB)
33
+ INKSCAPE_MESH_POLYFILL_MIN = readFileSync(
34
+ join(__dirname, 'vendor', 'inkscape-mesh-polyfill.min.js'),
35
+ 'utf-8'
36
+ );
37
+ // Full version (~35KB, for debugging or when --no-minify-polyfills is used)
38
+ INKSCAPE_MESH_POLYFILL_FULL = readFileSync(
39
+ join(__dirname, 'vendor', 'inkscape-mesh-polyfill.js'),
40
+ 'utf-8'
41
+ );
42
+
43
+ // Load hatch polyfills (CC0/Public Domain by Valentin Ionita)
44
+ // Minified version (default, ~5KB)
45
+ INKSCAPE_HATCH_POLYFILL_MIN = readFileSync(
46
+ join(__dirname, 'vendor', 'inkscape-hatch-polyfill.min.js'),
47
+ 'utf-8'
48
+ );
49
+ // Full version (~10KB, for debugging)
50
+ INKSCAPE_HATCH_POLYFILL_FULL = readFileSync(
51
+ join(__dirname, 'vendor', 'inkscape-hatch-polyfill.js'),
52
+ 'utf-8'
53
+ );
54
+ } catch (e) {
55
+ throw new Error(`Failed to load SVG2 polyfill files from vendor/ directory: ${e.message}. Ensure all vendor files are present.`);
56
+ }
57
+
58
+ // Module-level option for minification (default: true)
59
+ let useMinifiedPolyfills = true;
60
+
61
+ /**
62
+ * Set whether to use minified polyfills.
63
+ * @param {boolean} minify - True to use minified (default), false for full version
64
+ */
65
+ export function setPolyfillMinification(minify) {
66
+ useMinifiedPolyfills = minify;
67
+ }
68
+
13
69
  /**
14
70
  * SVG 2.0 features that can be polyfilled
15
71
  */
@@ -27,6 +83,8 @@ export const SVG2_FEATURES = {
27
83
  * @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
28
84
  */
29
85
  export function detectSVG2Features(doc) {
86
+ if (!doc) return { meshGradients: [], hatches: [], contextPaint: false, autoStartReverse: false };
87
+
30
88
  const features = {
31
89
  meshGradients: [],
32
90
  hatches: [],
@@ -83,6 +141,8 @@ export function detectSVG2Features(doc) {
83
141
  * @returns {boolean} True if polyfills are needed
84
142
  */
85
143
  export function needsPolyfills(doc) {
144
+ if (!doc) return false;
145
+
86
146
  const features = detectSVG2Features(doc);
87
147
  return features.meshGradients.length > 0 ||
88
148
  features.hatches.length > 0 ||
@@ -92,244 +152,51 @@ export function needsPolyfills(doc) {
92
152
 
93
153
  /**
94
154
  * Generate the mesh gradient polyfill code.
155
+ * Uses the Inkscape mesh.js polyfill by Tavmjong Bah (GPLv3).
95
156
  * This polyfill renders mesh gradients to canvas and uses them as image fills.
96
157
  *
158
+ * Features:
159
+ * - Multi-patch grid support
160
+ * - Bezier curve edge parsing (l, L, c, C commands)
161
+ * - Adaptive tessellation via de Casteljau subdivision
162
+ * - gradientTransform support
163
+ * - Proper shape clipping
164
+ *
97
165
  * @returns {string} JavaScript polyfill code
98
166
  */
99
167
  function generateMeshPolyfillCode() {
100
- return `
101
- // Mesh Gradient Polyfill - SVG 2.0 to Canvas fallback
102
- // Generated by svg-matrix
103
- (function() {
104
- 'use strict';
105
-
106
- // Skip if browser supports mesh gradients natively
107
- if (typeof document.createElementNS('http://www.w3.org/2000/svg', 'meshGradient').x !== 'undefined') {
108
- return;
109
- }
110
-
111
- // Find all mesh gradients
112
- var meshes = document.querySelectorAll('meshGradient, meshgradient');
113
- if (!meshes.length) return;
114
-
115
- // Parse color string to RGBA
116
- function parseColor(str) {
117
- if (!str) return {r: 0, g: 0, b: 0, a: 255};
118
- var m = str.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)/);
119
- if (m) return {r: +m[1], g: +m[2], b: +m[3], a: m[4] ? Math.round(+m[4] * 255) : 255};
120
- if (str[0] === '#') {
121
- var hex = str.slice(1);
122
- if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
123
- return {r: parseInt(hex.slice(0,2), 16), g: parseInt(hex.slice(2,4), 16), b: parseInt(hex.slice(4,6), 16), a: 255};
124
- }
125
- return {r: 0, g: 0, b: 0, a: 255};
126
- }
127
-
128
- // Bilinear color interpolation
129
- function bilinearColor(c00, c10, c01, c11, u, v) {
130
- var mu = 1 - u, mv = 1 - v;
131
- return {
132
- r: Math.round(mu*mv*c00.r + u*mv*c10.r + mu*v*c01.r + u*v*c11.r),
133
- g: Math.round(mu*mv*c00.g + u*mv*c10.g + mu*v*c01.g + u*v*c11.g),
134
- b: Math.round(mu*mv*c00.b + u*mv*c10.b + mu*v*c01.b + u*v*c11.b),
135
- a: Math.round(mu*mv*c00.a + u*mv*c10.a + mu*v*c01.a + u*v*c11.a)
136
- };
137
- }
138
-
139
- // Evaluate cubic Bezier at t
140
- function evalBezier(p0, p1, p2, p3, t) {
141
- var mt = 1 - t, mt2 = mt * mt, mt3 = mt2 * mt;
142
- var t2 = t * t, t3 = t2 * t;
143
- return {
144
- x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
145
- y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
146
- };
168
+ // Return minified or full Inkscape mesh.js polyfill based on setting
169
+ const polyfill = useMinifiedPolyfills ? INKSCAPE_MESH_POLYFILL_MIN : INKSCAPE_MESH_POLYFILL_FULL;
170
+ if (polyfill) {
171
+ return polyfill;
147
172
  }
148
-
149
- // Process each mesh gradient
150
- meshes.forEach(function(mesh) {
151
- var id = mesh.getAttribute('id');
152
- if (!id) return;
153
-
154
- // Create canvas for rasterization
155
- var canvas = document.createElement('canvas');
156
- var ctx = canvas.getContext('2d');
157
-
158
- // Get bounding box from referencing elements
159
- var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
160
- if (!refs.length) return;
161
-
162
- var bbox = refs[0].getBBox();
163
- var size = Math.max(bbox.width, bbox.height, 256);
164
- canvas.width = canvas.height = size;
165
-
166
- // Parse mesh patches
167
- var patches = [];
168
- var rows = mesh.querySelectorAll('meshrow');
169
- rows.forEach(function(row) {
170
- var rowPatches = row.querySelectorAll('meshpatch');
171
- rowPatches.forEach(function(patch) {
172
- var stops = patch.querySelectorAll('stop');
173
- if (stops.length >= 4) {
174
- patches.push({
175
- colors: [
176
- parseColor(stops[0].getAttribute('stop-color') || stops[0].style.stopColor),
177
- parseColor(stops[1].getAttribute('stop-color') || stops[1].style.stopColor),
178
- parseColor(stops[2].getAttribute('stop-color') || stops[2].style.stopColor),
179
- parseColor(stops[3].getAttribute('stop-color') || stops[3].style.stopColor)
180
- ]
181
- });
182
- }
183
- });
184
- });
185
-
186
- // Render patches with bilinear interpolation
187
- if (patches.length > 0) {
188
- var imgData = ctx.createImageData(size, size);
189
- var data = imgData.data;
190
- var patch = patches[0];
191
- for (var y = 0; y < size; y++) {
192
- for (var x = 0; x < size; x++) {
193
- var u = x / (size - 1);
194
- var v = y / (size - 1);
195
- var c = bilinearColor(patch.colors[0], patch.colors[1], patch.colors[2], patch.colors[3], u, v);
196
- var i = (y * size + x) * 4;
197
- data[i] = c.r;
198
- data[i+1] = c.g;
199
- data[i+2] = c.b;
200
- data[i+3] = c.a;
201
- }
202
- }
203
- ctx.putImageData(imgData, 0, 0);
204
- }
205
-
206
- // Create pattern from canvas
207
- var dataUrl = canvas.toDataURL('image/png');
208
- var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
209
- pattern.setAttribute('id', id + '_polyfill');
210
- pattern.setAttribute('patternUnits', 'objectBoundingBox');
211
- pattern.setAttribute('width', '1');
212
- pattern.setAttribute('height', '1');
213
-
214
- var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
215
- img.setAttribute('href', dataUrl);
216
- img.setAttribute('width', '1');
217
- img.setAttribute('height', '1');
218
- img.setAttribute('preserveAspectRatio', 'none');
219
- pattern.appendChild(img);
220
-
221
- // Add pattern to defs
222
- var defs = mesh.closest('svg').querySelector('defs') || (function() {
223
- var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
224
- mesh.closest('svg').insertBefore(d, mesh.closest('svg').firstChild);
225
- return d;
226
- })();
227
- defs.appendChild(pattern);
228
-
229
- // Update references to use polyfill pattern
230
- refs.forEach(function(ref) {
231
- if (ref.getAttribute('fill') === 'url(#' + id + ')') {
232
- ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
233
- }
234
- if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
235
- ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
236
- }
237
- });
238
- });
239
- })();
240
- `;
173
+ // Fallback: return empty string if polyfill couldn't be loaded
174
+ console.warn('svg2-polyfills: Inkscape mesh polyfill not available');
175
+ return '';
241
176
  }
242
177
 
243
178
  /**
244
179
  * Generate the hatch pattern polyfill code.
180
+ * Uses the Inkscape hatch polyfill by Valentin Ionita (CC0/Public Domain).
245
181
  * Converts SVG 2 hatch elements to SVG 1.1 pattern elements.
246
182
  *
183
+ * Features:
184
+ * - Full SVG path command support (M, L, C, S, Q, A, etc.)
185
+ * - Proper coordinate system handling (objectBoundingBox, userSpaceOnUse)
186
+ * - Hatch rotation and transform support
187
+ * - Multiple hatchpath elements with offset values
188
+ *
247
189
  * @returns {string} JavaScript polyfill code
248
190
  */
249
191
  function generateHatchPolyfillCode() {
250
- return `
251
- // Hatch Pattern Polyfill - SVG 2.0 to SVG 1.1 pattern conversion
252
- // Generated by svg-matrix
253
- (function() {
254
- 'use strict';
255
-
256
- // Find all hatch elements
257
- var hatches = document.querySelectorAll('hatch');
258
- if (!hatches.length) return;
259
-
260
- hatches.forEach(function(hatch) {
261
- var id = hatch.getAttribute('id');
262
- if (!id) return;
263
-
264
- // Get hatch properties
265
- var href = hatch.getAttribute('href') || hatch.getAttribute('xlink:href');
266
- var hatchUnits = hatch.getAttribute('hatchUnits') || 'objectBoundingBox';
267
- var hatchContentUnits = hatch.getAttribute('hatchContentUnits') || 'userSpaceOnUse';
268
- var pitch = parseFloat(hatch.getAttribute('pitch')) || 8;
269
- var rotate = parseFloat(hatch.getAttribute('rotate')) || 0;
270
- var transform = hatch.getAttribute('transform') || '';
271
-
272
- // Create equivalent pattern
273
- var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
274
- pattern.setAttribute('id', id + '_polyfill');
275
- pattern.setAttribute('patternUnits', hatchUnits);
276
- pattern.setAttribute('width', pitch);
277
- pattern.setAttribute('height', pitch);
278
-
279
- if (transform || rotate) {
280
- var fullTransform = transform;
281
- if (rotate) fullTransform += ' rotate(' + rotate + ')';
282
- pattern.setAttribute('patternTransform', fullTransform.trim());
283
- }
284
-
285
- // Copy hatchpath children as lines
286
- var hatchpaths = hatch.querySelectorAll('hatchpath, hatchPath');
287
- hatchpaths.forEach(function(hp) {
288
- var d = hp.getAttribute('d');
289
- var strokeColor = hp.getAttribute('stroke') || 'black';
290
- var strokeWidth = hp.getAttribute('stroke-width') || '1';
291
-
292
- var line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
293
- line.setAttribute('d', d || 'M0,0 L' + pitch + ',0');
294
- line.setAttribute('stroke', strokeColor);
295
- line.setAttribute('stroke-width', strokeWidth);
296
- line.setAttribute('fill', 'none');
297
- pattern.appendChild(line);
298
- });
299
-
300
- // If no hatchpaths, create default diagonal line
301
- if (!hatchpaths.length) {
302
- var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
303
- line.setAttribute('x1', '0');
304
- line.setAttribute('y1', '0');
305
- line.setAttribute('x2', pitch);
306
- line.setAttribute('y2', pitch);
307
- line.setAttribute('stroke', 'black');
308
- line.setAttribute('stroke-width', '1');
309
- pattern.appendChild(line);
310
- }
311
-
312
- // Add pattern to defs
313
- var defs = hatch.closest('svg').querySelector('defs') || (function() {
314
- var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
315
- hatch.closest('svg').insertBefore(d, hatch.closest('svg').firstChild);
316
- return d;
317
- })();
318
- defs.appendChild(pattern);
319
-
320
- // Update references
321
- var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
322
- refs.forEach(function(ref) {
323
- if (ref.getAttribute('fill') === 'url(#' + id + ')') {
324
- ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
325
- }
326
- if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
327
- ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
328
- }
329
- });
330
- });
331
- })();
332
- `;
192
+ // Return minified or full Inkscape hatch polyfill based on setting
193
+ const polyfill = useMinifiedPolyfills ? INKSCAPE_HATCH_POLYFILL_MIN : INKSCAPE_HATCH_POLYFILL_FULL;
194
+ if (polyfill) {
195
+ return polyfill;
196
+ }
197
+ // Fallback: return empty string if polyfill couldn't be loaded
198
+ console.warn('svg2-polyfills: Inkscape hatch polyfill not available');
199
+ return '';
333
200
  }
334
201
 
335
202
  /**
@@ -364,10 +231,14 @@ export function generatePolyfillScript(features) {
364
231
  * @param {Object} doc - Parsed SVG document
365
232
  * @param {Object} [options] - Options
366
233
  * @param {boolean} [options.force=false] - Force injection even if no features detected
234
+ * @param {Object} [options.features] - Pre-detected features (use instead of re-detecting)
367
235
  * @returns {Object} The document (modified in place)
368
236
  */
369
237
  export function injectPolyfills(doc, options = {}) {
370
- const features = detectSVG2Features(doc);
238
+ if (!doc) return doc;
239
+
240
+ // Use pre-detected features if provided (for when pipeline has removed SVG2 elements)
241
+ const features = options.features || detectSVG2Features(doc);
371
242
 
372
243
  // Check if polyfills are needed
373
244
  if (!options.force &&
@@ -382,28 +253,24 @@ export function injectPolyfills(doc, options = {}) {
382
253
  // Find or create the SVG root
383
254
  const svg = doc.documentElement || doc;
384
255
 
385
- // Create script element
386
- // Note: We need to handle this based on the parser being used
387
- const scriptEl = {
388
- tagName: 'script',
389
- attributes: new Map([['type', 'text/javascript']]),
390
- children: [],
391
- textContent: script,
392
- getAttribute(name) { return this.attributes.get(name); },
393
- setAttribute(name, value) { this.attributes.set(name, value); },
394
- hasAttribute(name) { return this.attributes.has(name); },
395
- removeAttribute(name) { this.attributes.delete(name); },
396
- getAttributeNames() { return [...this.attributes.keys()]; },
397
- appendChild(child) { this.children.push(child); },
398
- removeChild(child) {
399
- const idx = this.children.indexOf(child);
400
- if (idx >= 0) this.children.splice(idx, 1);
401
- }
402
- };
256
+ // Create a proper SVGElement for the script
257
+ // The script content uses CDATA to avoid XML escaping issues
258
+ const scriptEl = new SVGElement('script', {
259
+ type: 'text/javascript',
260
+ id: 'svg-matrix-polyfill'
261
+ }, [], script);
403
262
 
404
- // Insert script at beginning of SVG (after any xml declaration)
263
+ // Insert script at beginning of SVG (after defs if present, else at start)
405
264
  if (svg.children && svg.children.length > 0) {
406
- svg.children.unshift(scriptEl);
265
+ // Find first non-defs element to insert before
266
+ let insertIdx = 0;
267
+ for (let i = 0; i < svg.children.length; i++) {
268
+ if (svg.children[i].tagName === 'defs') {
269
+ insertIdx = i + 1;
270
+ break;
271
+ }
272
+ }
273
+ svg.children.splice(insertIdx, 0, scriptEl);
407
274
  } else if (svg.children) {
408
275
  svg.children.push(scriptEl);
409
276
  }
@@ -418,6 +285,8 @@ export function injectPolyfills(doc, options = {}) {
418
285
  * @returns {Object} The document (modified in place)
419
286
  */
420
287
  export function removePolyfills(doc) {
288
+ if (!doc) return doc;
289
+
421
290
  const walk = (el) => {
422
291
  if (!el || !el.children) return;
423
292