@emasoft/svg-matrix 1.0.19 → 1.0.21

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/bin/svg-matrix.js CHANGED
@@ -22,6 +22,8 @@ import * as SVGFlatten from '../src/svg-flatten.js';
22
22
  import * as GeometryToPath from '../src/geometry-to-path.js';
23
23
  import * as FlattenPipeline from '../src/flatten-pipeline.js';
24
24
  import { VERSION } from '../src/index.js';
25
+ import * as SVGToolbox from '../src/svg-toolbox.js';
26
+ import { parseSVG, serializeSVG } from '../src/svg-parser.js';
25
27
 
26
28
  // ============================================================================
27
29
  // CONSTANTS
@@ -117,6 +119,7 @@ const DEFAULT_CONFIG = {
117
119
  clipSegments: 64, // Polygon samples for clip operations (higher = more precise)
118
120
  bezierArcs: 8, // Bezier arcs for circles/ellipses (multiple of 4; 8=π/4 optimal)
119
121
  e2eTolerance: '1e-10', // E2E verification tolerance (tighter with more segments)
122
+ preserveVendor: false, // If true, preserve vendor-prefixed properties and editor namespaces
120
123
  };
121
124
 
122
125
  /** @type {CLIConfig} */
@@ -567,6 +570,9 @@ ${boxLine(` ${colors.dim}Ideal for animation and path morphing${col
567
570
  ${boxLine('', W)}
568
571
  ${boxLine(` ${colors.green}info${colors.reset} Show SVG file information and element counts`, W)}
569
572
  ${boxLine('', W)}
573
+ ${boxLine(` ${colors.green}test-toolbox${colors.reset} Test all svg-toolbox functions on an SVG file`, W)}
574
+ ${boxLine(` ${colors.dim}Creates timestamped folder with all processed versions${colors.reset}`, W)}
575
+ ${boxLine('', W)}
570
576
  ${boxLine(` ${colors.green}help${colors.reset} Show this help (or: svg-matrix <command> --help)`, W)}
571
577
  ${boxLine(` ${colors.green}version${colors.reset} Show version number`, W)}
572
578
  ${boxLine('', W)}
@@ -595,6 +601,8 @@ ${boxLine(` ${colors.dim}--no-use${colors.reset} Skip use/symbol
595
601
  ${boxLine(` ${colors.dim}--no-markers${colors.reset} Skip marker instantiation`, W)}
596
602
  ${boxLine(` ${colors.dim}--no-patterns${colors.reset} Skip pattern expansion`, W)}
597
603
  ${boxLine(` ${colors.dim}--no-gradients${colors.reset} Skip gradient transform baking`, W)}
604
+ ${boxLine(` ${colors.dim}--preserve-vendor${colors.reset} Keep vendor prefixes and editor namespaces`, W)}
605
+ ${boxLine(` ${colors.dim}(inkscape, sodipodi, -webkit-*, etc.)${colors.reset}`, W)}
598
606
  ${boxLine('', W)}
599
607
  ${boxDivider(W)}
600
608
  ${boxLine('', W)}
@@ -1174,6 +1182,161 @@ ${colors.bright}Elements:${colors.reset}
1174
1182
  }
1175
1183
  }
1176
1184
 
1185
+ // ============================================================================
1186
+ // TEST TOOLBOX
1187
+ // ============================================================================
1188
+
1189
+ // All testable functions from svg-toolbox.js organized by category
1190
+ const TOOLBOX_FUNCTIONS = {
1191
+ cleanup: ['cleanupIds', 'cleanupNumericValues', 'cleanupListOfValues', 'cleanupAttributes', 'cleanupEnableBackground'],
1192
+ remove: ['removeUnknownsAndDefaults', 'removeNonInheritableGroupAttrs', 'removeUselessDefs', 'removeHiddenElements', 'removeEmptyText', 'removeEmptyContainers', 'removeDoctype', 'removeXMLProcInst', 'removeComments', 'removeMetadata', 'removeTitle', 'removeDesc', 'removeEditorsNSData', 'removeEmptyAttrs', 'removeViewBox', 'removeXMLNS', 'removeRasterImages', 'removeScriptElement', 'removeStyleElement', 'removeXlink', 'removeDimensions', 'removeAttrs', 'removeElementsByAttr', 'removeAttributesBySelector', 'removeOffCanvasPath'],
1193
+ convert: ['convertShapesToPath', 'convertPathData', 'convertTransform', 'convertColors', 'convertStyleToAttrs', 'convertEllipseToCircle'],
1194
+ collapse: ['collapseGroups', 'mergePaths'],
1195
+ move: ['moveGroupAttrsToElems', 'moveElemsAttrsToGroup'],
1196
+ style: ['minifyStyles', 'inlineStyles'],
1197
+ sort: ['sortAttrs', 'sortDefsChildren'],
1198
+ pathOptimization: ['optimizePaths', 'simplifyPaths', 'simplifyPath', 'reusePaths'],
1199
+ flatten: ['flattenClipPaths', 'flattenMasks', 'flattenGradients', 'flattenPatterns', 'flattenFilters', 'flattenUseElements', 'flattenAll'],
1200
+ transform: ['decomposeTransform'],
1201
+ other: ['addAttributesToSVGElement', 'addClassesToSVGElement', 'prefixIds', 'optimizeAnimationTiming'],
1202
+ validation: ['validateXML', 'validateSVG', 'fixInvalidSvg'],
1203
+ detection: ['detectCollisions', 'measureDistance'],
1204
+ };
1205
+
1206
+ const SKIP_TOOLBOX_FUNCTIONS = ['textToPath', 'imageToPath', 'detectCollisions', 'measureDistance'];
1207
+
1208
+ function getTimestamp() {
1209
+ const now = new Date();
1210
+ const pad = (n) => String(n).padStart(2, '0');
1211
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
1212
+ }
1213
+
1214
+ async function testToolboxFunction(fnName, originalContent, originalSize, outputDir) {
1215
+ const result = { name: fnName, status: 'unknown', outputSize: 0, sizeDiff: 0, sizeDiffPercent: 0, error: null, outputFile: null, timeMs: 0 };
1216
+
1217
+ try {
1218
+ const fn = SVGToolbox[fnName];
1219
+ if (!fn) { result.status = 'not_found'; result.error = `Function not found`; return result; }
1220
+ if (SKIP_TOOLBOX_FUNCTIONS.includes(fnName)) { result.status = 'skipped'; result.error = 'Requires special handling'; return result; }
1221
+
1222
+ const startTime = Date.now();
1223
+ const doc = parseSVG(originalContent);
1224
+ // Pass preserveVendor option from config to toolbox functions
1225
+ await fn(doc, { preserveVendor: config.preserveVendor });
1226
+ const output = serializeSVG(doc);
1227
+ const outputSize = Buffer.byteLength(output);
1228
+ result.timeMs = Date.now() - startTime;
1229
+
1230
+ const outputFile = join(outputDir, `${fnName}.svg`);
1231
+ writeFileSync(outputFile, output);
1232
+
1233
+ result.status = 'success';
1234
+ result.outputSize = outputSize;
1235
+ result.sizeDiff = outputSize - originalSize;
1236
+ result.sizeDiffPercent = ((outputSize - originalSize) / originalSize * 100).toFixed(1);
1237
+ result.outputFile = outputFile;
1238
+
1239
+ if (output.length < 100) { result.status = 'warning'; result.error = 'Output suspiciously small'; }
1240
+ } catch (err) {
1241
+ result.status = 'error';
1242
+ result.error = err.message;
1243
+ }
1244
+ return result;
1245
+ }
1246
+
1247
+ async function processTestToolbox() {
1248
+ const files = gatherInputFiles();
1249
+ if (files.length === 0) { logError('No input file specified'); process.exit(CONSTANTS.EXIT_ERROR); }
1250
+ if (files.length > 1) { logError('test-toolbox only accepts one input file'); process.exit(CONSTANTS.EXIT_ERROR); }
1251
+
1252
+ const inputFile = files[0];
1253
+ validateSvgFile(inputFile);
1254
+
1255
+ const baseName = basename(inputFile, extname(inputFile));
1256
+ const timestamp = getTimestamp();
1257
+ const outputDirName = `${baseName}_${timestamp}`;
1258
+ const outputBase = config.output || dirname(inputFile);
1259
+ const outputDir = join(outputBase, outputDirName);
1260
+ mkdirSync(outputDir, { recursive: true });
1261
+
1262
+ console.log('\n' + '='.repeat(70));
1263
+ console.log(`${colors.bright}SVG-TOOLBOX FUNCTION TEST${colors.reset}`);
1264
+ console.log('='.repeat(70) + '\n');
1265
+
1266
+ const originalContent = readFileSync(inputFile, 'utf8');
1267
+ const originalSize = Buffer.byteLength(originalContent);
1268
+
1269
+ logInfo(`Input: ${colors.cyan}${inputFile}${colors.reset}`);
1270
+ logInfo(`Size: ${(originalSize / 1024 / 1024).toFixed(2)} MB`);
1271
+ logInfo(`Output: ${colors.cyan}${outputDir}/${colors.reset}\n`);
1272
+
1273
+ const allResults = [];
1274
+ let total = 0, success = 0, errors = 0, skipped = 0;
1275
+
1276
+ for (const [category, functions] of Object.entries(TOOLBOX_FUNCTIONS)) {
1277
+ console.log(`\n${colors.bright}--- ${category.toUpperCase()} ---${colors.reset}`);
1278
+ for (const fnName of functions) {
1279
+ total++;
1280
+ process.stdout.write(` Testing ${fnName.padEnd(30)}... `);
1281
+ const result = await testToolboxFunction(fnName, originalContent, originalSize, outputDir);
1282
+ allResults.push({ category, ...result });
1283
+
1284
+ if (result.status === 'success') {
1285
+ success++;
1286
+ const sizeStr = result.sizeDiff >= 0 ? `+${result.sizeDiffPercent}%` : `${result.sizeDiffPercent}%`;
1287
+ console.log(`${colors.green}OK${colors.reset} (${sizeStr}, ${result.timeMs}ms)`);
1288
+ } else if (result.status === 'skipped' || result.status === 'not_found') {
1289
+ skipped++;
1290
+ console.log(`${colors.yellow}SKIP${colors.reset}: ${result.error}`);
1291
+ } else if (result.status === 'warning') {
1292
+ success++;
1293
+ console.log(`${colors.yellow}WARN${colors.reset}: ${result.error}`);
1294
+ } else {
1295
+ errors++;
1296
+ console.log(`${colors.red}ERROR${colors.reset}: ${result.error}`);
1297
+ }
1298
+ }
1299
+ }
1300
+
1301
+ console.log('\n' + '='.repeat(70));
1302
+ console.log(`${colors.bright}SUMMARY${colors.reset}`);
1303
+ console.log('='.repeat(70));
1304
+ console.log(`Total: ${total} ${colors.green}Success: ${success}${colors.reset} ${colors.red}Errors: ${errors}${colors.reset} ${colors.yellow}Skipped: ${skipped}${colors.reset}`);
1305
+
1306
+ console.log('\n' + '-'.repeat(70));
1307
+ console.log(`${colors.bright}TOP SIZE REDUCERS${colors.reset}`);
1308
+ console.log('-'.repeat(70));
1309
+
1310
+ const successResults = allResults.filter(r => r.status === 'success' || r.status === 'warning').sort((a, b) => a.sizeDiff - b.sizeDiff).slice(0, 10);
1311
+ console.log('\nFunction Output Size Diff');
1312
+ console.log('-'.repeat(60));
1313
+ for (const r of successResults) {
1314
+ const name = r.name.padEnd(38);
1315
+ const size = ((r.outputSize / 1024 / 1024).toFixed(2) + ' MB').padStart(10);
1316
+ const diff = (r.sizeDiff >= 0 ? '+' : '') + r.sizeDiffPercent + '%';
1317
+ console.log(`${name} ${size} ${diff}`);
1318
+ }
1319
+
1320
+ if (errors > 0) {
1321
+ console.log('\n' + '-'.repeat(70));
1322
+ console.log(`${colors.red}${colors.bright}ERROR DETAILS${colors.reset}`);
1323
+ console.log('-'.repeat(70));
1324
+ for (const r of allResults.filter(r => r.status === 'error')) {
1325
+ console.log(`\n${colors.red}${r.name}:${colors.reset} ${r.error}`);
1326
+ }
1327
+ }
1328
+
1329
+ const resultsFile = join(outputDir, '_test-results.json');
1330
+ writeFileSync(resultsFile, JSON.stringify({ timestamp: new Date().toISOString(), inputFile, originalSize, outputDir, summary: { total, success, errors, skipped }, results: allResults }, null, 2));
1331
+
1332
+ console.log('\n' + '='.repeat(70));
1333
+ logInfo(`Results: ${colors.cyan}${resultsFile}${colors.reset}`);
1334
+ logInfo(`Output: ${colors.cyan}${outputDir}${colors.reset}`);
1335
+ console.log('='.repeat(70) + '\n');
1336
+
1337
+ if (errors > 0) process.exit(CONSTANTS.EXIT_ERROR);
1338
+ }
1339
+
1177
1340
  // ============================================================================
1178
1341
  // ARGUMENT PARSING
1179
1342
  // ============================================================================
@@ -1205,7 +1368,7 @@ function parseArgs(args) {
1205
1368
  case '--log-file': cfg.logFile = args[++i]; break;
1206
1369
  case '-h': case '--help':
1207
1370
  // If a command is already set (not 'help'), show command-specific help
1208
- if (cfg.command !== 'help' && ['flatten', 'convert', 'normalize', 'info'].includes(cfg.command)) {
1371
+ if (cfg.command !== 'help' && ['flatten', 'convert', 'normalize', 'info', 'test-toolbox'].includes(cfg.command)) {
1209
1372
  cfg.showCommandHelp = true;
1210
1373
  } else {
1211
1374
  cfg.command = 'help';
@@ -1220,6 +1383,8 @@ function parseArgs(args) {
1220
1383
  case '--no-markers': cfg.resolveMarkers = false; break;
1221
1384
  case '--no-patterns': cfg.resolvePatterns = false; break;
1222
1385
  case '--no-gradients': cfg.bakeGradients = false; break;
1386
+ // Vendor preservation option
1387
+ case '--preserve-vendor': cfg.preserveVendor = true; break;
1223
1388
  // E2E verification precision options
1224
1389
  case '--clip-segments': {
1225
1390
  const segs = parseInt(args[++i], 10);
@@ -1251,7 +1416,7 @@ function parseArgs(args) {
1251
1416
  // NOTE: --verify removed - verification is ALWAYS enabled
1252
1417
  default:
1253
1418
  if (arg.startsWith('-')) { logError(`Unknown option: ${arg}`); process.exit(CONSTANTS.EXIT_ERROR); }
1254
- if (['flatten', 'convert', 'normalize', 'info', 'help', 'version'].includes(arg) && cfg.command === 'help') {
1419
+ if (['flatten', 'convert', 'normalize', 'info', 'test-toolbox', 'help', 'version'].includes(arg) && cfg.command === 'help') {
1255
1420
  cfg.command = arg;
1256
1421
  } else {
1257
1422
  inputs.push(arg);
@@ -1384,6 +1549,10 @@ async function main() {
1384
1549
  if (fail > 0) process.exit(CONSTANTS.EXIT_ERROR);
1385
1550
  break;
1386
1551
  }
1552
+ case 'test-toolbox': {
1553
+ await processTestToolbox();
1554
+ break;
1555
+ }
1387
1556
  default:
1388
1557
  logError(`Unknown command: ${config.command}`);
1389
1558
  showHelp();