@emasoft/svg-matrix 1.3.4 → 1.3.6

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
@@ -333,6 +333,46 @@ svgm --export --export-prefix myapp_ input.svg -o output.svg --export-dir ./asse
333
333
  | `--export-dry-run` | Preview extraction without writing files |
334
334
  | `--export-ids <ids>` | Only export from specific element IDs |
335
335
 
336
+ ### Inkscape Conversion
337
+
338
+ Convert Inkscape SVG files to plain/standard SVG by removing editor-specific metadata while preserving SVG 2 features:
339
+
340
+ ```bash
341
+ # Using svgm
342
+ svgm --to-plain-svg inkscape-drawing.svg -o plain.svg
343
+
344
+ # Keep SVG 1.2 flowText elements (normally removed)
345
+ svgm --to-plain-svg --keep-flow-text inkscape-drawing.svg -o plain.svg
346
+
347
+ # Using svg-matrix CLI
348
+ svg-matrix to-plain inkscape-drawing.svg -o plain.svg
349
+ ```
350
+
351
+ **What gets removed:**
352
+ - `sodipodi:*` elements and attributes (guide lines, named views)
353
+ - `inkscape:*` elements and attributes (layers, path effects, version)
354
+ - SVG 1.2 flowText elements (not browser-supported)
355
+ - Namespace declarations (xmlns:inkscape, xmlns:sodipodi)
356
+
357
+ **What gets preserved:**
358
+ - Mesh gradients and hatches (SVG 2 features)
359
+ - All standard SVG elements and attributes
360
+ - Document structure and styling
361
+
362
+ ### Run Without Installing
363
+
364
+ Use `bunx` or `npx` to run the CLI tools without installing the package:
365
+
366
+ ```bash
367
+ # Using bunx (faster)
368
+ bunx @emasoft/svg-matrix svgm input.svg -o output.svg
369
+ bunx @emasoft/svg-matrix svg-matrix flatten input.svg -o output.svg
370
+
371
+ # Using npx
372
+ npx @emasoft/svg-matrix svgm input.svg -o output.svg
373
+ npx @emasoft/svg-matrix svg-matrix to-plain inkscape.svg -o plain.svg
374
+ ```
375
+
336
376
  ### YAML Configuration
337
377
 
338
378
  Instead of CLI flags, you can use a YAML configuration file:
@@ -600,6 +640,22 @@ const layers = InkscapeSupport.findLayers(doc);
600
640
  const settings = InkscapeSupport.getNamedViewSettings(doc);
601
641
  ```
602
642
 
643
+ ### Plain SVG Conversion
644
+
645
+ ```javascript
646
+ import { convertToPlainSVG } from '@emasoft/svg-matrix';
647
+
648
+ // Convert Inkscape SVG to plain/standard SVG
649
+ const plainSVG = await convertToPlainSVG(inkscapeSVG);
650
+
651
+ // With options
652
+ const plainSVG = await convertToPlainSVG(inkscapeSVG, {
653
+ removeFlowText: true, // Remove SVG 1.2 flowText (default: true)
654
+ removeEmptyDefs: true, // Clean up empty defs (default: true)
655
+ removeEmptyGroups: false, // Keep empty groups with IDs (default: false)
656
+ });
657
+ ```
658
+
603
659
  ### SVG 2.0 Polyfills Module
604
660
 
605
661
  ```javascript
package/bin/svg-matrix.js CHANGED
@@ -881,6 +881,9 @@ ${boxLine(` ${colors.dim}Ideal for animation and path morphing${col
881
881
  ${boxLine("", W)}
882
882
  ${boxLine(` ${colors.green}info${colors.reset} Show SVG file information and element counts`, W)}
883
883
  ${boxLine("", W)}
884
+ ${boxLine(` ${colors.green}to-plain${colors.reset} Convert Inkscape SVG to plain/standard SVG`, W)}
885
+ ${boxLine(` ${colors.dim}Removes all sodipodi:* and inkscape:* data${colors.reset}`, W)}
886
+ ${boxLine("", W)}
884
887
  ${boxLine(` ${colors.green}test-toolbox${colors.reset} Test all svg-toolbox functions on an SVG file`, W)}
885
888
  ${boxLine(` ${colors.dim}Creates timestamped folder with all processed versions${colors.reset}`, W)}
886
889
  ${boxLine("", W)}
@@ -921,6 +924,14 @@ ${boxLine(` ${colors.dim}--no-minify-polyfills${colors.reset} Use full (non-m
921
924
  ${boxLine("", W)}
922
925
  ${boxDivider(W)}
923
926
  ${boxLine("", W)}
927
+ ${boxHeader("TO-PLAIN OPTIONS", W)}
928
+ ${boxLine("", W)}
929
+ ${boxLine(` ${colors.yellow}${B.dot} Converts Inkscape SVG to plain/standard SVG${colors.reset}`, W)}
930
+ ${boxLine(` ${colors.yellow}${B.dot} Removes: sodipodi:*, inkscape:*, SVG 1.2 flowText${colors.reset}`, W)}
931
+ ${boxLine(` ${colors.yellow}${B.dot} Preserves: mesh gradients, hatches, standard SVG 2${colors.reset}`, W)}
932
+ ${boxLine("", W)}
933
+ ${boxDivider(W)}
934
+ ${boxLine("", W)}
924
935
  ${boxHeader("PRECISION OPTIONS", W)}
925
936
  ${boxLine("", W)}
926
937
  ${boxLine(` ${colors.dim}--clip-segments <n>${colors.reset} Polygon samples for clipping (default: 64)`, W)}
@@ -945,6 +956,7 @@ ${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} ./svgs/ -o ./out/
945
956
  ${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} --list files.txt -o ./out/ --no-patterns`, W)}
946
957
  ${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} input.svg -o out.svg --preserve-ns inkscape,sodipodi`, W)}
947
958
  ${boxLine(` ${colors.green}svg-matrix convert${colors.reset} input.svg -o output.svg -p 10`, W)}
959
+ ${boxLine(` ${colors.green}svg-matrix to-plain${colors.reset} inkscape.svg -o plain.svg`, W)}
948
960
  ${boxLine(` ${colors.green}svg-matrix info${colors.reset} input.svg`, W)}
949
961
  ${boxLine("", W)}
950
962
  ${boxDivider(W)}
@@ -1644,6 +1656,52 @@ function processNormalize(inputPath, outputPath) {
1644
1656
  }
1645
1657
  }
1646
1658
 
1659
+ /**
1660
+ * Convert Inkscape SVG to plain/standard SVG.
1661
+ * Removes all Inkscape-specific and Sodipodi-specific content while preserving
1662
+ * standard SVG elements like mesh gradients, hatches, and other SVG 2 features.
1663
+ *
1664
+ * @param {string} inputPath - Input file path
1665
+ * @param {string} outputPath - Output file path
1666
+ * @returns {Promise<boolean>} True if successful
1667
+ */
1668
+ async function processToPlain(inputPath, outputPath) {
1669
+ // Why: Validate parameters to prevent crashes
1670
+ if (!inputPath || typeof inputPath !== "string") {
1671
+ logError("Invalid input path: must be a non-empty string");
1672
+ return false;
1673
+ }
1674
+ if (!outputPath || typeof outputPath !== "string") {
1675
+ logError("Invalid output path: must be a non-empty string");
1676
+ return false;
1677
+ }
1678
+
1679
+ try {
1680
+ logDebug(`Converting to plain SVG: ${inputPath}`);
1681
+ const content = readFileSync(inputPath, "utf8");
1682
+
1683
+ // Apply convertToPlainSVG function
1684
+ // Why: This removes inkscape:* and sodipodi:* elements/attributes
1685
+ // but preserves mesh gradients, hatches, and other standard SVG features
1686
+ // Note: createOperation wrapper makes this async and handles parsing internally
1687
+ const result = await SVGToolbox.convertToPlainSVG(content, {
1688
+ removeFlowText: true, // SVG 1.2 flowText is Inkscape-only
1689
+ removeEmptyDefs: true, // Clean up defs after removing Inkscape content
1690
+ removeEmptyGroups: false, // Keep groups even if empty (may have id refs)
1691
+ });
1692
+
1693
+ if (!config.dryRun) {
1694
+ ensureDir(dirname(outputPath));
1695
+ writeFileSync(outputPath, result, "utf8");
1696
+ }
1697
+ logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
1698
+ return true;
1699
+ } catch (error) {
1700
+ logError(`Failed: ${inputPath}: ${error.message}`);
1701
+ return false;
1702
+ }
1703
+ }
1704
+
1647
1705
  /**
1648
1706
  * Display information about SVG file.
1649
1707
  * @param {string} inputPath - Input file path
@@ -2250,6 +2308,7 @@ function parseArgs(args) {
2250
2308
  "convert",
2251
2309
  "normalize",
2252
2310
  "info",
2311
+ "to-plain",
2253
2312
  "test-toolbox",
2254
2313
  "help",
2255
2314
  "version",
@@ -2373,7 +2432,8 @@ async function main() {
2373
2432
  }
2374
2433
  case "flatten":
2375
2434
  case "convert":
2376
- case "normalize": {
2435
+ case "normalize":
2436
+ case "to-plain": {
2377
2437
  const files = gatherInputFiles();
2378
2438
  if (files.length === 0) {
2379
2439
  logError("No input files");
@@ -2407,9 +2467,13 @@ async function main() {
2407
2467
  ? processFlatten
2408
2468
  : config.command === "convert"
2409
2469
  ? processConvert
2410
- : processNormalize;
2470
+ : config.command === "to-plain"
2471
+ ? processToPlain
2472
+ : processNormalize;
2411
2473
 
2412
- if (fn(f, out)) {
2474
+ // Why: processToPlain is async, so we need to await it
2475
+ const fnResult = await fn(f, out);
2476
+ if (fnResult) {
2413
2477
  // Verify write if not dry run
2414
2478
  if (!config.dryRun) {
2415
2479
  // Why: Simple empty check instead of full verifyWriteSuccess because:
package/bin/svglinter.cjs CHANGED
@@ -583,6 +583,73 @@ const RULES = {
583
583
  description: "Missing lang/xml:lang attribute on SVG with text content",
584
584
  fixable: false,
585
585
  },
586
+
587
+ // === INKSCAPE RULES (I###) ===
588
+ // Namespace errors (I001-I099)
589
+ I001: {
590
+ type: "inkscape-invalid_namespace_uri",
591
+ severity: "error",
592
+ description: "Invalid Inkscape or Sodipodi namespace URI",
593
+ fixable: false,
594
+ },
595
+ I002: {
596
+ type: "inkscape-unknown_inkscape_element",
597
+ severity: "error",
598
+ description: "Unknown inkscape: element (not in namespace schema)",
599
+ fixable: false,
600
+ },
601
+ I003: {
602
+ type: "inkscape-unknown_sodipodi_element",
603
+ severity: "error",
604
+ description: "Unknown sodipodi: element (not in namespace schema)",
605
+ fixable: false,
606
+ },
607
+
608
+ // Attribute errors (I100-I199)
609
+ I101: {
610
+ type: "inkscape-invalid_inkscape_attribute",
611
+ severity: "error",
612
+ description: "Invalid value for inkscape: attribute",
613
+ fixable: false,
614
+ },
615
+ I102: {
616
+ type: "inkscape-invalid_sodipodi_attribute",
617
+ severity: "error",
618
+ description: "Invalid value for sodipodi: attribute",
619
+ fixable: false,
620
+ },
621
+ I103: {
622
+ type: "inkscape-unknown_inkscape_attribute",
623
+ severity: "error",
624
+ description: "Unknown inkscape: attribute (not in namespace schema)",
625
+ fixable: false,
626
+ },
627
+ I104: {
628
+ type: "inkscape-unknown_sodipodi_attribute",
629
+ severity: "error",
630
+ description: "Unknown sodipodi: attribute (not in namespace schema)",
631
+ fixable: false,
632
+ },
633
+
634
+ // Compatibility warnings (I200-I299)
635
+ I201: {
636
+ type: "inkscape-flowtext_compatibility",
637
+ severity: "warning",
638
+ description: "flowRoot/flowPara elements require polyfill for browser rendering",
639
+ fixable: false,
640
+ },
641
+ I202: {
642
+ type: "inkscape-mesh_gradient",
643
+ severity: "warning",
644
+ description: "Mesh gradient requires polyfill for browser rendering",
645
+ fixable: false,
646
+ },
647
+ I203: {
648
+ type: "inkscape-hatch_paint",
649
+ severity: "warning",
650
+ description: "Hatch paint server requires polyfill for browser rendering",
651
+ fixable: false,
652
+ },
586
653
  };
587
654
 
588
655
  // Create reverse lookup: type -> code
@@ -1330,6 +1397,8 @@ function parseArgs(argv) {
1330
1397
  verbose: false, // Enable verbose debug output
1331
1398
  stdin: false, // Read from stdin instead of files
1332
1399
  severityOverrides: {}, // Map of rule code -> 'error'|'warning' for severity customization
1400
+ validateInkscape: false, // Enable Inkscape namespace validation
1401
+ inkscapeStrict: false, // Strict Inkscape validation (unknown attributes are errors)
1333
1402
  };
1334
1403
 
1335
1404
  let i = 2;
@@ -1752,6 +1821,9 @@ function parseArgs(argv) {
1752
1821
  args.verbose = true;
1753
1822
  } else if (arg === "--stdin" || arg === "-") {
1754
1823
  args.stdin = true;
1824
+ } else if (arg === "--validate-inkscape" || arg === "--inkscape" || arg === "--inkscape-strict") {
1825
+ // All Inkscape validation is now strict by default (spec compliance)
1826
+ args.validateInkscape = true;
1755
1827
  } else if (!arg.startsWith("-")) {
1756
1828
  args.files.push(arg);
1757
1829
  } else {
@@ -1835,6 +1907,12 @@ ${c("bold", "FORMATS")}
1835
1907
  sarif SARIF 2.1.0 for security tools (CodeQL, Snyk, etc.)
1836
1908
  github GitHub Actions annotations (::error, ::warning)
1837
1909
 
1910
+ ${c("bold", "INKSCAPE SUPPORT")}
1911
+ ${c("cyan", "--validate-inkscape")} Enable Inkscape namespace validation (strict mode)
1912
+ ${c("cyan", "--inkscape")} Alias for --validate-inkscape
1913
+ ${c("dim", "All unknown elements/attributes are errors (spec compliance)")}
1914
+ ${c("dim", "Polyfill requirements (flowText, mesh, hatch) are warnings")}
1915
+
1838
1916
  ${c("bold", "CONFIGURATION")}
1839
1917
  ${c("cyan", "-c, --config")} <file> Path to config file
1840
1918
  ${c("cyan", "--init")}[=format] Create config file (json, yaml, toml; default: json)
@@ -4269,6 +4347,8 @@ async function main() {
4269
4347
  const result = await validateSVGAsync(content, {
4270
4348
  errorsOnly: args.errorsOnly,
4271
4349
  includeSource: true,
4350
+ validateInkscape: args.validateInkscape,
4351
+ inkscapeStrict: args.inkscapeStrict,
4272
4352
  });
4273
4353
 
4274
4354
  // Add invalid character issues
@@ -4426,6 +4506,8 @@ async function main() {
4426
4506
  const result = await validateSVGAsync(file, {
4427
4507
  errorsOnly: args.errorsOnly,
4428
4508
  includeSource: true,
4509
+ validateInkscape: args.validateInkscape,
4510
+ inkscapeStrict: args.inkscapeStrict,
4429
4511
  });
4430
4512
 
4431
4513
  // Add invalid character issues
@@ -4836,6 +4918,8 @@ async function lintFile(filePath, options = {}) {
4836
4918
  const result = await validateSVGAsync(filePath, {
4837
4919
  errorsOnly: options.errorsOnly || false,
4838
4920
  includeSource: options.includeSource !== false,
4921
+ validateInkscape: options.validateInkscape || false,
4922
+ inkscapeStrict: options.inkscapeStrict || false,
4839
4923
  });
4840
4924
 
4841
4925
  // Add invalid character issues
package/bin/svgm.js CHANGED
@@ -109,6 +109,9 @@ const DEFAULT_CONFIG = {
109
109
  embedMaxRecursionDepth: 10,
110
110
  embedTimeout: 30000,
111
111
  embedOnMissingResource: "warn",
112
+ // Plain SVG conversion
113
+ toPlainSVG: false,
114
+ removeFlowText: true,
112
115
  };
113
116
 
114
117
  let config = { ...DEFAULT_CONFIG };
@@ -529,6 +532,20 @@ async function optimizeSvg(content, options = {}) {
529
532
  }
530
533
  }
531
534
 
535
+ // Convert to plain SVG if requested (remove all Inkscape/Sodipodi content)
536
+ // Why: This must run AFTER the optimization pipeline but BEFORE polyfills
537
+ if (options.toPlainSVG) {
538
+ try {
539
+ SVGToolbox.convertToPlainSVG(doc, {
540
+ removeFlowText: options.removeFlowText !== false,
541
+ removeEmptyDefs: true,
542
+ removeEmptyGroups: false,
543
+ });
544
+ } catch (err) {
545
+ logWarn(`convertToPlainSVG failed: ${err.message}`);
546
+ }
547
+ }
548
+
532
549
  // Inject SVG 2 polyfills if requested (using pre-detected features)
533
550
  if (options.svg2Polyfills && svg2Features) {
534
551
  if (
@@ -843,6 +860,12 @@ Embed Options:
843
860
  --embed-timeout <ms> Timeout for external resources (default: 30000)
844
861
  --embed-on-missing <mode> Handle missing resources: 'warn', 'fail', 'skip'
845
862
 
863
+ Inkscape Conversion:
864
+ --to-plain-svg Convert Inkscape SVG to plain/standard SVG
865
+ (removes sodipodi:*, inkscape:* elements and attributes)
866
+ --keep-flow-text Keep SVG 1.2 flowText elements when using --to-plain-svg
867
+ (by default they are removed as browsers don't support them)
868
+
846
869
  Examples:
847
870
  svgm input.svg -o output.svg
848
871
  svgm -f ./icons/ -o ./optimized/
@@ -850,6 +873,7 @@ Examples:
850
873
  svgm -p 2 --multipass input.svg
851
874
  svgm input.svg --embed-all -o output.svg
852
875
  svgm input.svg --config svgm.yml -o output.svg
876
+ svgm inkscape.svg --to-plain-svg -o plain.svg
853
877
 
854
878
  Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
855
879
  }
@@ -1320,6 +1344,16 @@ function parseArgs(args) {
1320
1344
  i++;
1321
1345
  break;
1322
1346
 
1347
+ case "--to-plain-svg":
1348
+ // Why: Enable conversion from Inkscape SVG to plain/standard SVG
1349
+ cfg.toPlainSVG = true;
1350
+ break;
1351
+
1352
+ case "--keep-flow-text":
1353
+ // Why: When using --to-plain-svg, keep SVG 1.2 flowText elements
1354
+ cfg.removeFlowText = false;
1355
+ break;
1356
+
1323
1357
  default:
1324
1358
  if (arg.startsWith("-")) {
1325
1359
  logError(`Unknown option: ${arg}`);
@@ -1471,6 +1505,9 @@ async function main() {
1471
1505
  embedMaxRecursionDepth: config.embedMaxRecursionDepth,
1472
1506
  embedTimeout: config.embedTimeout,
1473
1507
  embedOnMissingResource: config.embedOnMissingResource,
1508
+ // Plain SVG conversion options
1509
+ toPlainSVG: config.toPlainSVG,
1510
+ removeFlowText: config.removeFlowText,
1474
1511
  };
1475
1512
 
1476
1513
  // Handle string input