@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/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +9 -3
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
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();
|