@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 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 = ['textToPath', 'imageToPath', 'detectCollisions', 'measureDistance'];
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 option from config to toolbox functions
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emasoft/svg-matrix",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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.25
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.25';
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, TextToPath };
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
+ }
@@ -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
- // Skip if preserveVendor is enabled - editor namespaces are vendor-specific
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,