@emasoft/svg-matrix 1.0.26 → 1.0.27
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 +57 -0
- package/bin/svg-matrix.js +18 -2
- package/bin/svgm.js +28 -2
- package/package.json +1 -1
- package/src/index.js +7 -2
- package/src/inkscape-support.js +248 -0
- package/src/svg-toolbox.js +15 -3
- package/src/svg2-polyfills.js +444 -0
package/README.md
CHANGED
|
@@ -255,6 +255,36 @@ svgm --show-plugins
|
|
|
255
255
|
|
|
256
256
|
Run `svgm --help` for all options.
|
|
257
257
|
|
|
258
|
+
### Namespace Preservation
|
|
259
|
+
|
|
260
|
+
Preserve vendor-specific namespaces during optimization:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Preserve Inkscape/Sodipodi namespaces (layers, guides, document settings)
|
|
264
|
+
svgm --preserve-ns inkscape input.svg -o output.svg
|
|
265
|
+
|
|
266
|
+
# Preserve multiple vendor namespaces
|
|
267
|
+
svgm --preserve-ns inkscape,illustrator input.svg -o output.svg
|
|
268
|
+
|
|
269
|
+
# Available namespaces: inkscape, sodipodi, illustrator, sketch, ai, serif, vectornator, figma
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### SVG 2.0 Polyfills
|
|
273
|
+
|
|
274
|
+
Enable browser compatibility for SVG 2.0 features:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Add polyfills for mesh gradients and hatches
|
|
278
|
+
svgm --svg2-polyfills input.svg -o output.svg
|
|
279
|
+
|
|
280
|
+
# Combine with namespace preservation
|
|
281
|
+
svgm --preserve-ns inkscape --svg2-polyfills input.svg -o output.svg
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Supported SVG 2.0 features:
|
|
285
|
+
- **Mesh gradients** (`<meshGradient>`) - Rendered via canvas fallback
|
|
286
|
+
- **Hatches** (`<hatch>`) - Converted to SVG 1.1 patterns
|
|
287
|
+
|
|
258
288
|
---
|
|
259
289
|
|
|
260
290
|
### `svg-matrix` - Advanced SVG Processing
|
|
@@ -360,6 +390,33 @@ const fixed = await fixInvalidSvg('broken.svg');
|
|
|
360
390
|
console.log(fixed.svg); // Fixed SVG string
|
|
361
391
|
```
|
|
362
392
|
|
|
393
|
+
### Inkscape Support Module
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
import { InkscapeSupport } from '@emasoft/svg-matrix';
|
|
397
|
+
|
|
398
|
+
// Check if element is an Inkscape layer
|
|
399
|
+
InkscapeSupport.isInkscapeLayer(element);
|
|
400
|
+
|
|
401
|
+
// Find all layers in document
|
|
402
|
+
const layers = InkscapeSupport.findLayers(doc);
|
|
403
|
+
|
|
404
|
+
// Get document settings from sodipodi:namedview
|
|
405
|
+
const settings = InkscapeSupport.getNamedViewSettings(doc);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### SVG 2.0 Polyfills Module
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
import { SVG2Polyfills } from '@emasoft/svg-matrix';
|
|
412
|
+
|
|
413
|
+
// Detect SVG 2.0 features in document
|
|
414
|
+
const features = SVG2Polyfills.detectSVG2Features(doc);
|
|
415
|
+
|
|
416
|
+
// Inject polyfills into document
|
|
417
|
+
SVG2Polyfills.injectPolyfills(doc);
|
|
418
|
+
```
|
|
419
|
+
|
|
363
420
|
### Exclusive Features (Not in SVGO)
|
|
364
421
|
|
|
365
422
|
| Function | Description |
|
package/bin/svg-matrix.js
CHANGED
|
@@ -91,6 +91,7 @@ let currentOutputFile = null; // Track for cleanup on interrupt
|
|
|
91
91
|
* @property {boolean} resolveMarkers - Expand marker references
|
|
92
92
|
* @property {boolean} resolvePatterns - Expand pattern fills
|
|
93
93
|
* @property {boolean} bakeGradients - Bake gradientTransform into coordinates
|
|
94
|
+
* @property {string[]} preserveNamespaces - Array of namespace prefixes to preserve
|
|
94
95
|
*/
|
|
95
96
|
|
|
96
97
|
const DEFAULT_CONFIG = {
|
|
@@ -120,6 +121,7 @@ const DEFAULT_CONFIG = {
|
|
|
120
121
|
bezierArcs: 8, // Bezier arcs for circles/ellipses (multiple of 4; 8=π/4 optimal)
|
|
121
122
|
e2eTolerance: '1e-10', // E2E verification tolerance (tighter with more segments)
|
|
122
123
|
preserveVendor: false, // If true, preserve vendor-prefixed properties and editor namespaces
|
|
124
|
+
preserveNamespaces: [], // Array of namespace prefixes to preserve (e.g., ['inkscape', 'sodipodi'])
|
|
123
125
|
};
|
|
124
126
|
|
|
125
127
|
/** @type {CLIConfig} */
|
|
@@ -603,6 +605,8 @@ ${boxLine(` ${colors.dim}--no-patterns${colors.reset} Skip pattern ex
|
|
|
603
605
|
${boxLine(` ${colors.dim}--no-gradients${colors.reset} Skip gradient transform baking`, W)}
|
|
604
606
|
${boxLine(` ${colors.dim}--preserve-vendor${colors.reset} Keep vendor prefixes and editor namespaces`, W)}
|
|
605
607
|
${boxLine(` ${colors.dim}(inkscape, sodipodi, -webkit-*, etc.)${colors.reset}`, W)}
|
|
608
|
+
${boxLine(` ${colors.dim}--preserve-ns <list>${colors.reset} Preserve specific namespaces (comma-separated)`, W)}
|
|
609
|
+
${boxLine(` ${colors.dim}Example: --preserve-ns inkscape,sodipodi${colors.reset}`, W)}
|
|
606
610
|
${boxLine('', W)}
|
|
607
611
|
${boxDivider(W)}
|
|
608
612
|
${boxLine('', W)}
|
|
@@ -628,6 +632,7 @@ ${boxLine('', W)}
|
|
|
628
632
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} input.svg -o output.svg`, W)}
|
|
629
633
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} ./svgs/ -o ./out/ --transform-only`, W)}
|
|
630
634
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} --list files.txt -o ./out/ --no-patterns`, W)}
|
|
635
|
+
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} input.svg -o out.svg --preserve-ns inkscape,sodipodi`, W)}
|
|
631
636
|
${boxLine(` ${colors.green}svg-matrix convert${colors.reset} input.svg -o output.svg -p 10`, W)}
|
|
632
637
|
${boxLine(` ${colors.green}svg-matrix info${colors.reset} input.svg`, W)}
|
|
633
638
|
${boxLine('', W)}
|
|
@@ -867,6 +872,7 @@ function processFlatten(inputPath, outputPath) {
|
|
|
867
872
|
flattenTransforms: true, // Always flatten transforms
|
|
868
873
|
bakeGradients: config.bakeGradients,
|
|
869
874
|
removeUnusedDefs: true,
|
|
875
|
+
preserveNamespaces: config.preserveNamespaces, // Pass namespace preservation to pipeline
|
|
870
876
|
// NOTE: Verification is ALWAYS enabled - precision is non-negotiable
|
|
871
877
|
};
|
|
872
878
|
|
|
@@ -1221,8 +1227,8 @@ async function testToolboxFunction(fnName, originalContent, originalSize, output
|
|
|
1221
1227
|
|
|
1222
1228
|
const startTime = Date.now();
|
|
1223
1229
|
const doc = parseSVG(originalContent);
|
|
1224
|
-
// Pass preserveVendor
|
|
1225
|
-
await fn(doc, { preserveVendor: config.preserveVendor });
|
|
1230
|
+
// Pass preserveVendor and preserveNamespaces options from config to toolbox functions
|
|
1231
|
+
await fn(doc, { preserveVendor: config.preserveVendor, preserveNamespaces: config.preserveNamespaces });
|
|
1226
1232
|
const output = serializeSVG(doc);
|
|
1227
1233
|
const outputSize = Buffer.byteLength(output);
|
|
1228
1234
|
result.timeMs = Date.now() - startTime;
|
|
@@ -1385,6 +1391,16 @@ function parseArgs(args) {
|
|
|
1385
1391
|
case '--no-gradients': cfg.bakeGradients = false; break;
|
|
1386
1392
|
// Vendor preservation option
|
|
1387
1393
|
case '--preserve-vendor': cfg.preserveVendor = true; break;
|
|
1394
|
+
// Namespace preservation option (comma-separated list)
|
|
1395
|
+
case '--preserve-ns': {
|
|
1396
|
+
const namespaces = args[++i];
|
|
1397
|
+
if (!namespaces) {
|
|
1398
|
+
logError('--preserve-ns requires a comma-separated list of namespaces');
|
|
1399
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1400
|
+
}
|
|
1401
|
+
cfg.preserveNamespaces = namespaces.split(',').map(ns => ns.trim()).filter(ns => ns.length > 0);
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1388
1404
|
// E2E verification precision options
|
|
1389
1405
|
case '--clip-segments': {
|
|
1390
1406
|
const segs = parseInt(args[++i], 10);
|
package/bin/svgm.js
CHANGED
|
@@ -20,6 +20,7 @@ import { join, dirname, basename, extname, resolve, isAbsolute } from 'path';
|
|
|
20
20
|
import { VERSION } from '../src/index.js';
|
|
21
21
|
import * as SVGToolbox from '../src/svg-toolbox.js';
|
|
22
22
|
import { parseSVG, serializeSVG } from '../src/svg-parser.js';
|
|
23
|
+
import { injectPolyfills, detectSVG2Features } from '../src/svg2-polyfills.js';
|
|
23
24
|
|
|
24
25
|
// ============================================================================
|
|
25
26
|
// CONSTANTS
|
|
@@ -62,6 +63,8 @@ const DEFAULT_CONFIG = {
|
|
|
62
63
|
exclude: [],
|
|
63
64
|
quiet: false,
|
|
64
65
|
datauri: null,
|
|
66
|
+
preserveNamespaces: [],
|
|
67
|
+
svg2Polyfills: false,
|
|
65
68
|
showPlugins: false,
|
|
66
69
|
};
|
|
67
70
|
|
|
@@ -222,7 +225,7 @@ async function optimizeSvg(content, options = {}) {
|
|
|
222
225
|
const fn = SVGToolbox[pluginName];
|
|
223
226
|
if (fn && typeof fn === 'function') {
|
|
224
227
|
try {
|
|
225
|
-
await fn(doc, { precision: options.precision });
|
|
228
|
+
await fn(doc, { precision: options.precision, preserveNamespaces: options.preserveNamespaces });
|
|
226
229
|
} catch (e) {
|
|
227
230
|
// Skip failed optimizations silently
|
|
228
231
|
}
|
|
@@ -235,7 +238,7 @@ async function optimizeSvg(content, options = {}) {
|
|
|
235
238
|
const fn = SVGToolbox[pluginName];
|
|
236
239
|
if (fn && typeof fn === 'function') {
|
|
237
240
|
try {
|
|
238
|
-
await fn(doc, { precision: options.precision });
|
|
241
|
+
await fn(doc, { precision: options.precision, preserveNamespaces: options.preserveNamespaces });
|
|
239
242
|
} catch (e) {
|
|
240
243
|
// Skip failed optimizations silently
|
|
241
244
|
}
|
|
@@ -243,6 +246,14 @@ async function optimizeSvg(content, options = {}) {
|
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
// Inject SVG 2 polyfills if requested
|
|
250
|
+
if (options.svg2Polyfills) {
|
|
251
|
+
const features = detectSVG2Features(doc);
|
|
252
|
+
if (features.meshGradients.length > 0 || features.hatches.length > 0) {
|
|
253
|
+
injectPolyfills(doc);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
246
257
|
let result = serializeSVG(doc);
|
|
247
258
|
|
|
248
259
|
// Pretty print if requested, otherwise minify (SVGO default behavior)
|
|
@@ -384,6 +395,10 @@ Options:
|
|
|
384
395
|
regular expression pattern.
|
|
385
396
|
-q, --quiet Only output error messages
|
|
386
397
|
--show-plugins Show available plugins and exit
|
|
398
|
+
--preserve-ns <NS,...> Preserve vendor namespaces (inkscape, sodipodi,
|
|
399
|
+
illustrator, figma, etc.). Comma-separated.
|
|
400
|
+
--svg2-polyfills Inject JavaScript polyfills for SVG 2 features
|
|
401
|
+
(mesh gradients, hatches) for browser support
|
|
387
402
|
--no-color Output plain text without color
|
|
388
403
|
-h, --help Display help for command
|
|
389
404
|
|
|
@@ -517,6 +532,15 @@ function parseArgs(args) {
|
|
|
517
532
|
cfg.showPlugins = true;
|
|
518
533
|
break;
|
|
519
534
|
|
|
535
|
+
case '--preserve-ns':
|
|
536
|
+
i++;
|
|
537
|
+
cfg.preserveNamespaces = args[i].split(',').map(s => s.trim().toLowerCase());
|
|
538
|
+
break;
|
|
539
|
+
|
|
540
|
+
case '--svg2-polyfills':
|
|
541
|
+
cfg.svg2Polyfills = true;
|
|
542
|
+
break;
|
|
543
|
+
|
|
520
544
|
case '--no-color':
|
|
521
545
|
// Already handled in colors initialization
|
|
522
546
|
break;
|
|
@@ -561,6 +585,8 @@ async function main() {
|
|
|
561
585
|
eol: config.eol,
|
|
562
586
|
finalNewline: config.finalNewline,
|
|
563
587
|
datauri: config.datauri,
|
|
588
|
+
preserveNamespaces: config.preserveNamespaces,
|
|
589
|
+
svg2Polyfills: config.svg2Polyfills,
|
|
564
590
|
};
|
|
565
591
|
|
|
566
592
|
// Handle string input
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* SVG path conversion, and 2D/3D affine transformations using Decimal.js.
|
|
6
6
|
*
|
|
7
7
|
* @module @emasoft/svg-matrix
|
|
8
|
-
* @version 1.0.
|
|
8
|
+
* @version 1.0.27
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -46,6 +46,8 @@ import * as MeshGradient from './mesh-gradient.js';
|
|
|
46
46
|
import * as SVGParser from './svg-parser.js';
|
|
47
47
|
import * as FlattenPipeline from './flatten-pipeline.js';
|
|
48
48
|
import * as Verification from './verification.js';
|
|
49
|
+
import * as InkscapeSupport from './inkscape-support.js';
|
|
50
|
+
import * as SVG2Polyfills from './svg2-polyfills.js';
|
|
49
51
|
import { Logger, LogLevel, setLogLevel, getLogLevel as getLoggerLevel, enableFileLogging, disableFileLogging } from './logger.js';
|
|
50
52
|
|
|
51
53
|
// SVGO-inspired precision modules
|
|
@@ -85,7 +87,7 @@ Decimal.set({ precision: 80 });
|
|
|
85
87
|
* Library version
|
|
86
88
|
* @constant {string}
|
|
87
89
|
*/
|
|
88
|
-
export const VERSION = '1.0.
|
|
90
|
+
export const VERSION = '1.0.27';
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
93
|
* Default precision for path output (decimal places)
|
|
@@ -120,6 +122,7 @@ export { ClipPathResolver, MaskResolver, PatternResolver };
|
|
|
120
122
|
export { UseSymbolResolver, MarkerResolver };
|
|
121
123
|
export { MeshGradient };
|
|
122
124
|
export { SVGParser, FlattenPipeline, Verification };
|
|
125
|
+
export { InkscapeSupport, SVG2Polyfills };
|
|
123
126
|
|
|
124
127
|
// ============================================================================
|
|
125
128
|
// SVGO-INSPIRED PRECISION MODULES
|
|
@@ -577,6 +580,8 @@ export default {
|
|
|
577
580
|
SVGParser,
|
|
578
581
|
FlattenPipeline,
|
|
579
582
|
Verification,
|
|
583
|
+
InkscapeSupport,
|
|
584
|
+
SVG2Polyfills,
|
|
580
585
|
|
|
581
586
|
// Logging
|
|
582
587
|
Logger,
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inkscape/Sodipodi Support Module
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for preserving and manipulating Inkscape-specific SVG features
|
|
5
|
+
* including layers, guides, document settings, and arc parameters.
|
|
6
|
+
*
|
|
7
|
+
* @module inkscape-support
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Inkscape namespace URIs
|
|
11
|
+
export const INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape';
|
|
12
|
+
export const SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd';
|
|
13
|
+
|
|
14
|
+
// Inkscape-specific element and attribute prefixes
|
|
15
|
+
export const INKSCAPE_PREFIXES = ['inkscape', 'sodipodi'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if an element is an Inkscape layer.
|
|
19
|
+
* Inkscape uses `<g inkscape:groupmode="layer">` for layers.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} element - SVG element to check
|
|
22
|
+
* @returns {boolean} True if the element is an Inkscape layer
|
|
23
|
+
*/
|
|
24
|
+
export function isInkscapeLayer(element) {
|
|
25
|
+
if (!element || element.tagName !== 'g') return false;
|
|
26
|
+
return element.getAttribute('inkscape:groupmode') === 'layer';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the label of an Inkscape layer.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} element - Inkscape layer element
|
|
33
|
+
* @returns {string|null} Layer label or null if not set
|
|
34
|
+
*/
|
|
35
|
+
export function getLayerLabel(element) {
|
|
36
|
+
return element?.getAttribute('inkscape:label') || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find all Inkscape layers in a document.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} doc - Parsed SVG document
|
|
43
|
+
* @returns {Array<{element: Object, label: string|null, id: string|null}>} Array of layer info objects
|
|
44
|
+
*/
|
|
45
|
+
export function findLayers(doc) {
|
|
46
|
+
const layers = [];
|
|
47
|
+
|
|
48
|
+
const walk = (el) => {
|
|
49
|
+
if (!el || !el.children) return;
|
|
50
|
+
if (isInkscapeLayer(el)) {
|
|
51
|
+
layers.push({
|
|
52
|
+
element: el,
|
|
53
|
+
label: getLayerLabel(el),
|
|
54
|
+
id: el.getAttribute('id')
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const child of el.children) {
|
|
58
|
+
walk(child);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
walk(doc);
|
|
63
|
+
return layers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get sodipodi:namedview document settings.
|
|
68
|
+
* Contains Inkscape document settings like page color, grid, guides.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} doc - Parsed SVG document
|
|
71
|
+
* @returns {Object|null} Named view settings or null if not found
|
|
72
|
+
*/
|
|
73
|
+
export function getNamedViewSettings(doc) {
|
|
74
|
+
// Find namedview element - may be direct child or nested
|
|
75
|
+
let namedview = null;
|
|
76
|
+
|
|
77
|
+
const findNamedview = (el) => {
|
|
78
|
+
if (!el) return;
|
|
79
|
+
if (el.tagName === 'sodipodi:namedview') {
|
|
80
|
+
namedview = el;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (el.children) {
|
|
84
|
+
for (const child of el.children) {
|
|
85
|
+
findNamedview(child);
|
|
86
|
+
if (namedview) return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
findNamedview(doc);
|
|
92
|
+
if (!namedview) return null;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
pagecolor: namedview.getAttribute('pagecolor'),
|
|
96
|
+
bordercolor: namedview.getAttribute('bordercolor'),
|
|
97
|
+
borderopacity: namedview.getAttribute('borderopacity'),
|
|
98
|
+
showgrid: namedview.getAttribute('showgrid'),
|
|
99
|
+
showguides: namedview.getAttribute('showguides'),
|
|
100
|
+
guidetolerance: namedview.getAttribute('guidetolerance'),
|
|
101
|
+
inkscapeZoom: namedview.getAttribute('inkscape:zoom'),
|
|
102
|
+
inkscapeCx: namedview.getAttribute('inkscape:cx'),
|
|
103
|
+
inkscapeCy: namedview.getAttribute('inkscape:cy'),
|
|
104
|
+
inkscapeWindowWidth: namedview.getAttribute('inkscape:window-width'),
|
|
105
|
+
inkscapeWindowHeight: namedview.getAttribute('inkscape:window-height'),
|
|
106
|
+
inkscapeCurrentLayer: namedview.getAttribute('inkscape:current-layer')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find all sodipodi:guide elements (guidelines).
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} doc - Parsed SVG document
|
|
114
|
+
* @returns {Array<{position: string, orientation: string, id: string|null}>} Array of guide info
|
|
115
|
+
*/
|
|
116
|
+
export function findGuides(doc) {
|
|
117
|
+
const guides = [];
|
|
118
|
+
|
|
119
|
+
const walk = (el) => {
|
|
120
|
+
if (!el || !el.children) return;
|
|
121
|
+
if (el.tagName === 'sodipodi:guide') {
|
|
122
|
+
guides.push({
|
|
123
|
+
position: el.getAttribute('position'),
|
|
124
|
+
orientation: el.getAttribute('orientation'),
|
|
125
|
+
id: el.getAttribute('id'),
|
|
126
|
+
inkscapeColor: el.getAttribute('inkscape:color'),
|
|
127
|
+
inkscapeLabel: el.getAttribute('inkscape:label')
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const child of el.children) {
|
|
131
|
+
walk(child);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
walk(doc);
|
|
136
|
+
return guides;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get sodipodi arc parameters from a path element.
|
|
141
|
+
* Inkscape stores original arc parameters for shapes converted from ellipses.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} element - SVG element (typically a path)
|
|
144
|
+
* @returns {Object|null} Arc parameters or null if not an arc
|
|
145
|
+
*/
|
|
146
|
+
export function getArcParameters(element) {
|
|
147
|
+
if (!element) return null;
|
|
148
|
+
|
|
149
|
+
const type = element.getAttribute('sodipodi:type');
|
|
150
|
+
if (type !== 'arc') return null;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
type: 'arc',
|
|
154
|
+
cx: element.getAttribute('sodipodi:cx'),
|
|
155
|
+
cy: element.getAttribute('sodipodi:cy'),
|
|
156
|
+
rx: element.getAttribute('sodipodi:rx'),
|
|
157
|
+
ry: element.getAttribute('sodipodi:ry'),
|
|
158
|
+
start: element.getAttribute('sodipodi:start'),
|
|
159
|
+
end: element.getAttribute('sodipodi:end'),
|
|
160
|
+
open: element.getAttribute('sodipodi:open')
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get node types from a path element.
|
|
166
|
+
* Inkscape stores node types (corner, smooth, symmetric, auto) for path editing.
|
|
167
|
+
*
|
|
168
|
+
* @param {Object} element - SVG path element
|
|
169
|
+
* @returns {string|null} Node types string (c=corner, s=smooth, z=symmetric, a=auto)
|
|
170
|
+
*/
|
|
171
|
+
export function getNodeTypes(element) {
|
|
172
|
+
return element?.getAttribute('sodipodi:nodetypes') || null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get export settings from an element.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} element - SVG element
|
|
179
|
+
* @returns {Object|null} Export settings or null if not set
|
|
180
|
+
*/
|
|
181
|
+
export function getExportSettings(element) {
|
|
182
|
+
if (!element) return null;
|
|
183
|
+
|
|
184
|
+
const filename = element.getAttribute('inkscape:export-filename');
|
|
185
|
+
const xdpi = element.getAttribute('inkscape:export-xdpi');
|
|
186
|
+
const ydpi = element.getAttribute('inkscape:export-ydpi');
|
|
187
|
+
|
|
188
|
+
if (!filename && !xdpi && !ydpi) return null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
filename,
|
|
192
|
+
xdpi: xdpi ? parseFloat(xdpi) : null,
|
|
193
|
+
ydpi: ydpi ? parseFloat(ydpi) : null
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if element is part of a tiled clone.
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} element - SVG element
|
|
201
|
+
* @returns {boolean} True if element is a tiled clone
|
|
202
|
+
*/
|
|
203
|
+
export function isTiledClone(element) {
|
|
204
|
+
return element?.hasAttribute('inkscape:tiled-clone-of') || false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get tiled clone source ID.
|
|
209
|
+
*
|
|
210
|
+
* @param {Object} element - SVG element
|
|
211
|
+
* @returns {string|null} Source element ID or null
|
|
212
|
+
*/
|
|
213
|
+
export function getTiledCloneSource(element) {
|
|
214
|
+
return element?.getAttribute('inkscape:tiled-clone-of') || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if document has Inkscape namespaces declared.
|
|
219
|
+
*
|
|
220
|
+
* @param {Object} doc - Parsed SVG document
|
|
221
|
+
* @returns {boolean} True if Inkscape namespaces are present
|
|
222
|
+
*/
|
|
223
|
+
export function hasInkscapeNamespaces(doc) {
|
|
224
|
+
const svg = doc.documentElement || doc;
|
|
225
|
+
const hasInkscape = svg.getAttribute('xmlns:inkscape') === INKSCAPE_NS;
|
|
226
|
+
const hasSodipodi = svg.getAttribute('xmlns:sodipodi') === SODIPODI_NS;
|
|
227
|
+
return hasInkscape || hasSodipodi;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Ensure Inkscape namespace declarations are present.
|
|
232
|
+
* Adds xmlns:inkscape and xmlns:sodipodi if missing.
|
|
233
|
+
*
|
|
234
|
+
* @param {Object} doc - Parsed SVG document
|
|
235
|
+
* @returns {Object} The document (modified in place)
|
|
236
|
+
*/
|
|
237
|
+
export function ensureInkscapeNamespaces(doc) {
|
|
238
|
+
const svg = doc.documentElement || doc;
|
|
239
|
+
|
|
240
|
+
if (!svg.getAttribute('xmlns:inkscape')) {
|
|
241
|
+
svg.setAttribute('xmlns:inkscape', INKSCAPE_NS);
|
|
242
|
+
}
|
|
243
|
+
if (!svg.getAttribute('xmlns:sodipodi')) {
|
|
244
|
+
svg.setAttribute('xmlns:sodipodi', SODIPODI_NS);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return doc;
|
|
248
|
+
}
|
package/src/svg-toolbox.js
CHANGED
|
@@ -1456,9 +1456,18 @@ export const removeDesc = createOperation((doc, options = {}) => {
|
|
|
1456
1456
|
* @param {boolean} options.preserveVendor - If true, preserves all editor namespace elements and attributes
|
|
1457
1457
|
*/
|
|
1458
1458
|
export const removeEditorsNSData = createOperation((doc, options = {}) => {
|
|
1459
|
-
//
|
|
1460
|
-
if (options.preserveVendor) return doc;
|
|
1459
|
+
// preserveVendor = true preserves ALL editor namespaces (backward compat)
|
|
1460
|
+
if (options.preserveVendor === true) return doc;
|
|
1461
|
+
|
|
1462
|
+
// preserveNamespaces = array of prefixes to preserve selectively
|
|
1463
|
+
const preserveNamespaces = options.preserveNamespaces || [];
|
|
1464
|
+
|
|
1465
|
+
// Handle namespace aliases: sodipodi and inkscape always go together
|
|
1466
|
+
const normalizedPreserve = new Set(preserveNamespaces);
|
|
1467
|
+
if (normalizedPreserve.has('sodipodi')) normalizedPreserve.add('inkscape');
|
|
1468
|
+
if (normalizedPreserve.has('inkscape')) normalizedPreserve.add('sodipodi');
|
|
1461
1469
|
|
|
1470
|
+
// Filter out preserved namespaces from the removal list
|
|
1462
1471
|
const editorPrefixes = [
|
|
1463
1472
|
"inkscape",
|
|
1464
1473
|
"sodipodi",
|
|
@@ -1468,7 +1477,10 @@ export const removeEditorsNSData = createOperation((doc, options = {}) => {
|
|
|
1468
1477
|
"serif",
|
|
1469
1478
|
"vectornator",
|
|
1470
1479
|
"figma",
|
|
1471
|
-
];
|
|
1480
|
+
].filter(prefix => !normalizedPreserve.has(prefix));
|
|
1481
|
+
|
|
1482
|
+
// If all namespaces are preserved, exit early
|
|
1483
|
+
if (editorPrefixes.length === 0) return doc;
|
|
1472
1484
|
|
|
1473
1485
|
// FIRST: Remove editor-specific elements (sodipodi:namedview, etc.)
|
|
1474
1486
|
// This must happen BEFORE checking remaining prefixes to avoid SVGO bug #1530
|
|
@@ -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
|
+
/**
|
|
14
|
+
* SVG 2.0 features that can be polyfilled
|
|
15
|
+
*/
|
|
16
|
+
export const SVG2_FEATURES = {
|
|
17
|
+
MESH_GRADIENT: 'meshGradient',
|
|
18
|
+
HATCH: 'hatch',
|
|
19
|
+
CONTEXT_PAINT: 'context-paint',
|
|
20
|
+
AUTO_START_REVERSE: 'auto-start-reverse'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect SVG 2.0 features that need polyfills in a document.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} doc - Parsed SVG document
|
|
27
|
+
* @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
|
|
28
|
+
*/
|
|
29
|
+
export function detectSVG2Features(doc) {
|
|
30
|
+
const features = {
|
|
31
|
+
meshGradients: [],
|
|
32
|
+
hatches: [],
|
|
33
|
+
contextPaint: false,
|
|
34
|
+
autoStartReverse: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const walk = (el) => {
|
|
38
|
+
if (!el) return;
|
|
39
|
+
|
|
40
|
+
// Check tag name for mesh gradient (case-insensitive)
|
|
41
|
+
const tagName = el.tagName?.toLowerCase();
|
|
42
|
+
if (tagName === 'meshgradient') {
|
|
43
|
+
const id = el.getAttribute('id');
|
|
44
|
+
if (id) features.meshGradients.push(id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for hatch element
|
|
48
|
+
if (tagName === 'hatch') {
|
|
49
|
+
const id = el.getAttribute('id');
|
|
50
|
+
if (id) features.hatches.push(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for context-paint in fill/stroke
|
|
54
|
+
const fill = el.getAttribute('fill');
|
|
55
|
+
const stroke = el.getAttribute('stroke');
|
|
56
|
+
if (fill === 'context-fill' || fill === 'context-stroke' ||
|
|
57
|
+
stroke === 'context-fill' || stroke === 'context-stroke') {
|
|
58
|
+
features.contextPaint = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for auto-start-reverse in markers
|
|
62
|
+
const orient = el.getAttribute('orient');
|
|
63
|
+
if (orient === 'auto-start-reverse') {
|
|
64
|
+
features.autoStartReverse = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Recurse into children
|
|
68
|
+
if (el.children) {
|
|
69
|
+
for (const child of el.children) {
|
|
70
|
+
walk(child);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
walk(doc);
|
|
76
|
+
return features;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if document needs any SVG 2 polyfills.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} doc - Parsed SVG document
|
|
83
|
+
* @returns {boolean} True if polyfills are needed
|
|
84
|
+
*/
|
|
85
|
+
export function needsPolyfills(doc) {
|
|
86
|
+
const features = detectSVG2Features(doc);
|
|
87
|
+
return features.meshGradients.length > 0 ||
|
|
88
|
+
features.hatches.length > 0 ||
|
|
89
|
+
features.contextPaint ||
|
|
90
|
+
features.autoStartReverse;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate the mesh gradient polyfill code.
|
|
95
|
+
* This polyfill renders mesh gradients to canvas and uses them as image fills.
|
|
96
|
+
*
|
|
97
|
+
* @returns {string} JavaScript polyfill code
|
|
98
|
+
*/
|
|
99
|
+
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
|
+
};
|
|
147
|
+
}
|
|
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
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate the hatch pattern polyfill code.
|
|
245
|
+
* Converts SVG 2 hatch elements to SVG 1.1 pattern elements.
|
|
246
|
+
*
|
|
247
|
+
* @returns {string} JavaScript polyfill code
|
|
248
|
+
*/
|
|
249
|
+
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
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate complete polyfill script based on detected features.
|
|
337
|
+
*
|
|
338
|
+
* @param {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} features - Detected features
|
|
339
|
+
* @returns {string|null} Complete polyfill script or null if none needed
|
|
340
|
+
*/
|
|
341
|
+
export function generatePolyfillScript(features) {
|
|
342
|
+
const parts = [];
|
|
343
|
+
|
|
344
|
+
parts.push('/* SVG 2.0 Polyfills - Generated by svg-matrix */');
|
|
345
|
+
|
|
346
|
+
if (features.meshGradients.length > 0) {
|
|
347
|
+
parts.push(generateMeshPolyfillCode());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (features.hatches.length > 0) {
|
|
351
|
+
parts.push(generateHatchPolyfillCode());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (parts.length === 1) {
|
|
355
|
+
return null; // Only header, no actual polyfills
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return parts.join('\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Inject polyfill script into SVG document.
|
|
363
|
+
*
|
|
364
|
+
* @param {Object} doc - Parsed SVG document
|
|
365
|
+
* @param {Object} [options] - Options
|
|
366
|
+
* @param {boolean} [options.force=false] - Force injection even if no features detected
|
|
367
|
+
* @returns {Object} The document (modified in place)
|
|
368
|
+
*/
|
|
369
|
+
export function injectPolyfills(doc, options = {}) {
|
|
370
|
+
const features = detectSVG2Features(doc);
|
|
371
|
+
|
|
372
|
+
// Check if polyfills are needed
|
|
373
|
+
if (!options.force &&
|
|
374
|
+
features.meshGradients.length === 0 &&
|
|
375
|
+
features.hatches.length === 0) {
|
|
376
|
+
return doc;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const script = generatePolyfillScript(features);
|
|
380
|
+
if (!script) return doc;
|
|
381
|
+
|
|
382
|
+
// Find or create the SVG root
|
|
383
|
+
const svg = doc.documentElement || doc;
|
|
384
|
+
|
|
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
|
+
};
|
|
403
|
+
|
|
404
|
+
// Insert script at beginning of SVG (after any xml declaration)
|
|
405
|
+
if (svg.children && svg.children.length > 0) {
|
|
406
|
+
svg.children.unshift(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
|
+
}
|