@emasoft/svg-matrix 1.0.25 → 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 -1
- package/bin/svg-matrix.js +19 -3
- package/bin/svgm.js +28 -2
- package/package.json +1 -1
- package/src/index.js +8 -6
- package/src/inkscape-support.js +248 -0
- package/src/svg-toolbox.js +15 -19
- package/src/svg2-polyfills.js +444 -0
- package/src/text-to-path.js +0 -820
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 |
|
|
@@ -369,7 +426,6 @@ console.log(fixed.svg); // Fixed SVG string
|
|
|
369
426
|
| `flattenGradients()` | Bake gradients into fills |
|
|
370
427
|
| `flattenPatterns()` | Expand pattern tiles |
|
|
371
428
|
| `flattenUseElements()` | Inline use/symbol references |
|
|
372
|
-
| `textToPath()` | Convert text to path outlines |
|
|
373
429
|
| `detectCollisions()` | GJK collision detection |
|
|
374
430
|
| `validateSVG()` | W3C schema validation |
|
|
375
431
|
| `decomposeTransform()` | Matrix decomposition |
|
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
|
|
|
@@ -1203,7 +1209,7 @@ const TOOLBOX_FUNCTIONS = {
|
|
|
1203
1209
|
detection: ['detectCollisions', 'measureDistance'],
|
|
1204
1210
|
};
|
|
1205
1211
|
|
|
1206
|
-
const SKIP_TOOLBOX_FUNCTIONS = ['
|
|
1212
|
+
const SKIP_TOOLBOX_FUNCTIONS = ['imageToPath', 'detectCollisions', 'measureDistance'];
|
|
1207
1213
|
|
|
1208
1214
|
function getTimestamp() {
|
|
1209
1215
|
const now = new Date();
|
|
@@ -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
|
|
@@ -43,10 +43,11 @@ import * as PatternResolver from './pattern-resolver.js';
|
|
|
43
43
|
import * as UseSymbolResolver from './use-symbol-resolver.js';
|
|
44
44
|
import * as MarkerResolver from './marker-resolver.js';
|
|
45
45
|
import * as MeshGradient from './mesh-gradient.js';
|
|
46
|
-
import * as TextToPath from './text-to-path.js';
|
|
47
46
|
import * as SVGParser from './svg-parser.js';
|
|
48
47
|
import * as FlattenPipeline from './flatten-pipeline.js';
|
|
49
48
|
import * as Verification from './verification.js';
|
|
49
|
+
import * as InkscapeSupport from './inkscape-support.js';
|
|
50
|
+
import * as SVG2Polyfills from './svg2-polyfills.js';
|
|
50
51
|
import { Logger, LogLevel, setLogLevel, getLogLevel as getLoggerLevel, enableFileLogging, disableFileLogging } from './logger.js';
|
|
51
52
|
|
|
52
53
|
// SVGO-inspired precision modules
|
|
@@ -86,7 +87,7 @@ Decimal.set({ precision: 80 });
|
|
|
86
87
|
* Library version
|
|
87
88
|
* @constant {string}
|
|
88
89
|
*/
|
|
89
|
-
export const VERSION = '1.0.
|
|
90
|
+
export const VERSION = '1.0.27';
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* Default precision for path output (decimal places)
|
|
@@ -119,8 +120,9 @@ export { GeometryToPath, PolygonClip };
|
|
|
119
120
|
export { SVGFlatten, BrowserVerify };
|
|
120
121
|
export { ClipPathResolver, MaskResolver, PatternResolver };
|
|
121
122
|
export { UseSymbolResolver, MarkerResolver };
|
|
122
|
-
export { MeshGradient
|
|
123
|
+
export { MeshGradient };
|
|
123
124
|
export { SVGParser, FlattenPipeline, Verification };
|
|
125
|
+
export { InkscapeSupport, SVG2Polyfills };
|
|
124
126
|
|
|
125
127
|
// ============================================================================
|
|
126
128
|
// SVGO-INSPIRED PRECISION MODULES
|
|
@@ -233,7 +235,6 @@ export {
|
|
|
233
235
|
flattenPatterns,
|
|
234
236
|
flattenFilters,
|
|
235
237
|
flattenUseElements,
|
|
236
|
-
textToPath,
|
|
237
238
|
imageToPath,
|
|
238
239
|
detectCollisions,
|
|
239
240
|
measureDistance,
|
|
@@ -576,10 +577,11 @@ export default {
|
|
|
576
577
|
UseSymbolResolver,
|
|
577
578
|
MarkerResolver,
|
|
578
579
|
MeshGradient,
|
|
579
|
-
TextToPath,
|
|
580
580
|
SVGParser,
|
|
581
581
|
FlattenPipeline,
|
|
582
582
|
Verification,
|
|
583
|
+
InkscapeSupport,
|
|
584
|
+
SVG2Polyfills,
|
|
583
585
|
|
|
584
586
|
// Logging
|
|
585
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
|
|
@@ -3702,21 +3714,6 @@ export const flattenUseElements = createOperation(async (doc, options = {}) => {
|
|
|
3702
3714
|
return parseSVG(result.svg);
|
|
3703
3715
|
});
|
|
3704
3716
|
|
|
3705
|
-
/**
|
|
3706
|
-
* Convert text to path geometry (requires font info - stub)
|
|
3707
|
-
*/
|
|
3708
|
-
export const textToPath = createOperation((doc, options = {}) => {
|
|
3709
|
-
// Note: True text-to-path conversion requires font rendering engine
|
|
3710
|
-
// This is a placeholder that removes text elements
|
|
3711
|
-
const texts = doc.getElementsByTagName("text");
|
|
3712
|
-
for (const text of [...texts]) {
|
|
3713
|
-
// In real implementation, would convert using font metrics
|
|
3714
|
-
// For now, just mark with comment
|
|
3715
|
-
text.setAttribute("data-text-to-path-needed", "true");
|
|
3716
|
-
}
|
|
3717
|
-
return doc;
|
|
3718
|
-
});
|
|
3719
|
-
|
|
3720
3717
|
/**
|
|
3721
3718
|
* Trace raster images to paths (basic - stub)
|
|
3722
3719
|
*/
|
|
@@ -7421,7 +7418,6 @@ export default {
|
|
|
7421
7418
|
flattenPatterns,
|
|
7422
7419
|
flattenFilters,
|
|
7423
7420
|
flattenUseElements,
|
|
7424
|
-
textToPath,
|
|
7425
7421
|
imageToPath,
|
|
7426
7422
|
detectCollisions,
|
|
7427
7423
|
measureDistance,
|