@emasoft/svg-matrix 1.0.27 → 1.0.29
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 +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
package/bin/svg-matrix.js
CHANGED
|
@@ -14,16 +14,29 @@
|
|
|
14
14
|
* @license MIT
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
statSync,
|
|
24
|
+
appendFileSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
openSync,
|
|
27
|
+
readSync,
|
|
28
|
+
closeSync,
|
|
29
|
+
} from "fs";
|
|
30
|
+
import { join, dirname, basename, extname, resolve, isAbsolute } from "path";
|
|
19
31
|
|
|
20
32
|
// Import library modules
|
|
21
|
-
import * as SVGFlatten from
|
|
22
|
-
import * as GeometryToPath from
|
|
23
|
-
import * as FlattenPipeline from
|
|
24
|
-
import { VERSION } from
|
|
25
|
-
import * as SVGToolbox from
|
|
26
|
-
import { parseSVG, serializeSVG } from
|
|
33
|
+
import * as SVGFlatten from "../src/svg-flatten.js";
|
|
34
|
+
import * as GeometryToPath from "../src/geometry-to-path.js";
|
|
35
|
+
import * as FlattenPipeline from "../src/flatten-pipeline.js";
|
|
36
|
+
import { VERSION } from "../src/index.js";
|
|
37
|
+
import * as SVGToolbox from "../src/svg-toolbox.js";
|
|
38
|
+
import { parseSVG, serializeSVG } from "../src/svg-parser.js";
|
|
39
|
+
import { setPolyfillMinification } from "../src/svg2-polyfills.js";
|
|
27
40
|
|
|
28
41
|
// ============================================================================
|
|
29
42
|
// CONSTANTS
|
|
@@ -52,7 +65,7 @@ const CONSTANTS = {
|
|
|
52
65
|
EXIT_INTERRUPTED: 130, // 128 + SIGINT(2)
|
|
53
66
|
|
|
54
67
|
// SVG file extensions recognized
|
|
55
|
-
SVG_EXTENSIONS: [
|
|
68
|
+
SVG_EXTENSIONS: [".svg", ".svgz"],
|
|
56
69
|
|
|
57
70
|
// SVG header pattern for validation
|
|
58
71
|
SVG_HEADER_PATTERN: /<svg[\s>]/i,
|
|
@@ -95,7 +108,7 @@ let currentOutputFile = null; // Track for cleanup on interrupt
|
|
|
95
108
|
*/
|
|
96
109
|
|
|
97
110
|
const DEFAULT_CONFIG = {
|
|
98
|
-
command:
|
|
111
|
+
command: "help",
|
|
99
112
|
inputs: [],
|
|
100
113
|
output: null,
|
|
101
114
|
listFile: null,
|
|
@@ -106,22 +119,23 @@ const DEFAULT_CONFIG = {
|
|
|
106
119
|
recursive: false,
|
|
107
120
|
overwrite: false,
|
|
108
121
|
dryRun: false,
|
|
109
|
-
showCommandHelp: false,
|
|
122
|
+
showCommandHelp: false, // Track if --help was requested for a specific command
|
|
110
123
|
// Full flatten options - all enabled by default for TRUE flattening
|
|
111
|
-
transformOnly: false,
|
|
112
|
-
resolveClipPaths: true,
|
|
113
|
-
resolveMasks: true,
|
|
114
|
-
resolveUse: true,
|
|
115
|
-
resolveMarkers: true,
|
|
116
|
-
resolvePatterns: true,
|
|
117
|
-
bakeGradients: true,
|
|
124
|
+
transformOnly: false, // If true, skip all resolvers (legacy behavior)
|
|
125
|
+
resolveClipPaths: true, // Apply clipPath boolean intersection
|
|
126
|
+
resolveMasks: true, // Convert masks to clipped geometry
|
|
127
|
+
resolveUse: true, // Expand use/symbol elements inline
|
|
128
|
+
resolveMarkers: true, // Instantiate markers as path geometry
|
|
129
|
+
resolvePatterns: true, // Expand pattern fills to tiled geometry
|
|
130
|
+
bakeGradients: true, // Bake gradientTransform into gradient coords
|
|
118
131
|
// NOTE: Verification is ALWAYS enabled - precision is non-negotiable
|
|
119
132
|
// E2E verification precision controls
|
|
120
|
-
clipSegments: 64,
|
|
121
|
-
bezierArcs: 8,
|
|
122
|
-
e2eTolerance:
|
|
123
|
-
preserveVendor: false,
|
|
124
|
-
preserveNamespaces: [],
|
|
133
|
+
clipSegments: 64, // Polygon samples for clip operations (higher = more precise)
|
|
134
|
+
bezierArcs: 8, // Bezier arcs for circles/ellipses (multiple of 4; 8=π/4 optimal)
|
|
135
|
+
e2eTolerance: "1e-10", // E2E verification tolerance (tighter with more segments)
|
|
136
|
+
preserveVendor: false, // If true, preserve vendor-prefixed properties and editor namespaces
|
|
137
|
+
preserveNamespaces: [], // Array of namespace prefixes to preserve (e.g., ['inkscape', 'sodipodi'])
|
|
138
|
+
svg2Polyfills: false, // If true, inject JavaScript polyfills for SVG 2 features
|
|
125
139
|
};
|
|
126
140
|
|
|
127
141
|
/** @type {CLIConfig} */
|
|
@@ -133,18 +147,36 @@ let config = { ...DEFAULT_CONFIG };
|
|
|
133
147
|
|
|
134
148
|
const LogLevel = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 };
|
|
135
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Get current log level based on config flags.
|
|
152
|
+
* @returns {number} Log level (ERROR, WARN, INFO, or DEBUG)
|
|
153
|
+
*/
|
|
136
154
|
function getLogLevel() {
|
|
137
155
|
if (config.quiet) return LogLevel.ERROR;
|
|
138
156
|
if (config.verbose) return LogLevel.DEBUG;
|
|
139
157
|
return LogLevel.INFO;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
const colors =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
const colors =
|
|
161
|
+
process.env.NO_COLOR !== undefined
|
|
162
|
+
? {
|
|
163
|
+
reset: "",
|
|
164
|
+
red: "",
|
|
165
|
+
yellow: "",
|
|
166
|
+
green: "",
|
|
167
|
+
cyan: "",
|
|
168
|
+
dim: "",
|
|
169
|
+
bright: "",
|
|
170
|
+
}
|
|
171
|
+
: {
|
|
172
|
+
reset: "\x1b[0m",
|
|
173
|
+
red: "\x1b[31m",
|
|
174
|
+
yellow: "\x1b[33m",
|
|
175
|
+
green: "\x1b[32m",
|
|
176
|
+
cyan: "\x1b[36m",
|
|
177
|
+
dim: "\x1b[2m",
|
|
178
|
+
bright: "\x1b[1m",
|
|
179
|
+
};
|
|
148
180
|
|
|
149
181
|
// ============================================================================
|
|
150
182
|
// SIGNAL HANDLING (handlers)
|
|
@@ -153,24 +185,37 @@ const colors = process.env.NO_COLOR !== undefined ? {
|
|
|
153
185
|
// won't be properly released. The isShuttingDown flag prevents duplicate
|
|
154
186
|
// cleanup attempts during signal cascades.
|
|
155
187
|
// ============================================================================
|
|
188
|
+
/**
|
|
189
|
+
* Handle graceful shutdown on SIGINT/SIGTERM signals.
|
|
190
|
+
* @param {string} signal - Signal name (SIGINT or SIGTERM)
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
156
193
|
function handleGracefulExit(signal) {
|
|
157
194
|
// Why: Prevent duplicate cleanup if multiple signals arrive quickly
|
|
158
195
|
if (isShuttingDown) return;
|
|
159
196
|
isShuttingDown = true;
|
|
160
197
|
|
|
161
|
-
console.log(
|
|
198
|
+
console.log(
|
|
199
|
+
`\n${colors.yellow}Received ${signal}, cleaning up...${colors.reset}`,
|
|
200
|
+
);
|
|
162
201
|
|
|
163
202
|
// Why: Remove partial output file if interrupt occurred during processing
|
|
164
203
|
if (currentOutputFile && existsSync(currentOutputFile)) {
|
|
165
204
|
try {
|
|
166
205
|
unlinkSync(currentOutputFile);
|
|
167
|
-
console.log(
|
|
168
|
-
|
|
206
|
+
console.log(
|
|
207
|
+
`${colors.dim}Removed partial output: ${basename(currentOutputFile)}${colors.reset}`,
|
|
208
|
+
);
|
|
209
|
+
} catch {
|
|
210
|
+
/* ignore cleanup errors */
|
|
211
|
+
}
|
|
169
212
|
}
|
|
170
213
|
|
|
171
214
|
// Log the interruption for debugging
|
|
172
215
|
if (config.logFile) {
|
|
173
|
-
writeToLogFile(
|
|
216
|
+
writeToLogFile(
|
|
217
|
+
`INTERRUPTED: Received ${signal} while processing ${currentInputFile || "unknown"}`,
|
|
218
|
+
);
|
|
174
219
|
}
|
|
175
220
|
|
|
176
221
|
// Why: Give async operations time to complete, but don't hang indefinitely
|
|
@@ -180,44 +225,80 @@ function handleGracefulExit(signal) {
|
|
|
180
225
|
}
|
|
181
226
|
|
|
182
227
|
// Register signal handlers - must be done before any async operations
|
|
183
|
-
process.on(
|
|
184
|
-
process.on(
|
|
228
|
+
process.on("SIGINT", () => handleGracefulExit("SIGINT")); // Ctrl+C
|
|
229
|
+
process.on("SIGTERM", () => handleGracefulExit("SIGTERM")); // kill command
|
|
185
230
|
// Note: SIGTERM is not fully supported on Windows. On Windows, SIGINT (Ctrl+C) works,
|
|
186
231
|
// but SIGTERM requires special handling or third-party libraries. This is acceptable
|
|
187
232
|
// for a CLI tool as Ctrl+C is the primary interrupt mechanism on all platforms.
|
|
188
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Write message to log file with timestamp.
|
|
236
|
+
* @param {string} message - Message to log
|
|
237
|
+
* @returns {void}
|
|
238
|
+
*/
|
|
189
239
|
function writeToLogFile(message) {
|
|
190
240
|
if (config.logFile) {
|
|
191
241
|
try {
|
|
192
242
|
const timestamp = new Date().toISOString();
|
|
193
|
-
|
|
243
|
+
// eslint-disable-next-line no-control-regex -- ANSI escape codes are intentional for log stripping
|
|
244
|
+
const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, "");
|
|
194
245
|
appendFileSync(config.logFile, `[${timestamp}] ${cleanMessage}\n`);
|
|
195
|
-
} catch {
|
|
246
|
+
} catch {
|
|
247
|
+
/* ignore */
|
|
248
|
+
}
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Log error message to console and file.
|
|
254
|
+
* @param {string} msg - Error message
|
|
255
|
+
* @returns {void}
|
|
256
|
+
*/
|
|
199
257
|
function logError(msg) {
|
|
200
258
|
console.error(`${colors.red}ERROR:${colors.reset} ${msg}`);
|
|
201
259
|
writeToLogFile(`ERROR: ${msg}`);
|
|
202
260
|
}
|
|
203
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Log warning message to console and file.
|
|
264
|
+
* @param {string} msg - Warning message
|
|
265
|
+
* @returns {void}
|
|
266
|
+
*/
|
|
204
267
|
function logWarn(msg) {
|
|
205
|
-
if (getLogLevel() >= LogLevel.WARN)
|
|
268
|
+
if (getLogLevel() >= LogLevel.WARN)
|
|
269
|
+
console.warn(`${colors.yellow}WARN:${colors.reset} ${msg}`);
|
|
206
270
|
writeToLogFile(`WARN: ${msg}`);
|
|
207
271
|
}
|
|
208
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Log info message to console and file.
|
|
275
|
+
* @param {string} msg - Info message
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
209
278
|
function logInfo(msg) {
|
|
210
279
|
if (getLogLevel() >= LogLevel.INFO) console.log(msg);
|
|
211
280
|
writeToLogFile(`INFO: ${msg}`);
|
|
212
281
|
}
|
|
213
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Log debug message to console and file.
|
|
285
|
+
* @param {string} msg - Debug message
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
214
288
|
function logDebug(msg) {
|
|
215
|
-
if (getLogLevel() >= LogLevel.DEBUG)
|
|
289
|
+
if (getLogLevel() >= LogLevel.DEBUG)
|
|
290
|
+
console.log(`${colors.dim}DEBUG: ${msg}${colors.reset}`);
|
|
216
291
|
writeToLogFile(`DEBUG: ${msg}`);
|
|
217
292
|
}
|
|
218
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Log success message to console and file.
|
|
296
|
+
* @param {string} msg - Success message
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
219
299
|
function logSuccess(msg) {
|
|
220
|
-
if (getLogLevel() >= LogLevel.INFO)
|
|
300
|
+
if (getLogLevel() >= LogLevel.INFO)
|
|
301
|
+
console.log(`${colors.green}OK${colors.reset} ${msg}`);
|
|
221
302
|
writeToLogFile(`SUCCESS: ${msg}`);
|
|
222
303
|
}
|
|
223
304
|
|
|
@@ -227,27 +308,38 @@ function logSuccess(msg) {
|
|
|
227
308
|
// the operation is still running and how far along it is. Without this,
|
|
228
309
|
// users may think the program is frozen and kill it prematurely.
|
|
229
310
|
// ============================================================================
|
|
311
|
+
/**
|
|
312
|
+
* Display progress bar for batch operations.
|
|
313
|
+
* @param {number} current - Current file number
|
|
314
|
+
* @param {number} total - Total number of files
|
|
315
|
+
* @param {string} filename - Current filename being processed
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*/
|
|
230
318
|
function showProgress(current, total, filename) {
|
|
231
319
|
// Why: Don't show progress in quiet mode or when there's only one file
|
|
232
320
|
if (config.quiet || total <= 1) return;
|
|
233
321
|
|
|
234
322
|
// Why: In verbose mode, newline before progress to avoid overwriting debug output
|
|
235
323
|
if (config.verbose && current > 1) {
|
|
236
|
-
process.stdout.write(
|
|
324
|
+
process.stdout.write("\n");
|
|
237
325
|
}
|
|
238
326
|
|
|
239
327
|
const percent = Math.round((current / total) * 100);
|
|
240
|
-
const bar =
|
|
328
|
+
const bar =
|
|
329
|
+
"█".repeat(Math.floor(percent / 5)) +
|
|
330
|
+
"░".repeat(20 - Math.floor(percent / 5));
|
|
241
331
|
|
|
242
332
|
// Why: Use \r to overwrite the same line, keeping terminal clean
|
|
243
|
-
process.stdout.write(
|
|
333
|
+
process.stdout.write(
|
|
334
|
+
`\r${colors.cyan}[${bar}]${colors.reset} ${percent}% (${current}/${total}) ${basename(filename)}`,
|
|
335
|
+
);
|
|
244
336
|
|
|
245
337
|
// Why: Clear to end of line to remove any leftover characters from longer filenames
|
|
246
|
-
process.stdout.write(
|
|
338
|
+
process.stdout.write("\x1b[K");
|
|
247
339
|
|
|
248
340
|
// Why: Print newline when complete so next output starts on new line
|
|
249
341
|
if (current === total) {
|
|
250
|
-
process.stdout.write(
|
|
342
|
+
process.stdout.write("\n");
|
|
251
343
|
}
|
|
252
344
|
}
|
|
253
345
|
|
|
@@ -256,24 +348,32 @@ function showProgress(current, total, filename) {
|
|
|
256
348
|
// Why: Fail fast on invalid input. Processing non-SVG files wastes time and
|
|
257
349
|
// may produce confusing errors. Size limits prevent memory exhaustion.
|
|
258
350
|
// ============================================================================
|
|
351
|
+
/**
|
|
352
|
+
* Validate SVG file size and format.
|
|
353
|
+
* @param {string} filePath - Path to SVG file
|
|
354
|
+
* @returns {boolean} True if valid
|
|
355
|
+
* @throws {Error} If file is invalid or too large
|
|
356
|
+
*/
|
|
259
357
|
function validateSvgFile(filePath) {
|
|
260
358
|
const stats = statSync(filePath);
|
|
261
359
|
|
|
262
360
|
// Why: Prevent memory exhaustion from huge files
|
|
263
361
|
if (stats.size > CONSTANTS.MAX_FILE_SIZE_BYTES) {
|
|
264
|
-
throw new Error(
|
|
362
|
+
throw new Error(
|
|
363
|
+
`File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Max: ${CONSTANTS.MAX_FILE_SIZE_BYTES / 1024 / 1024}MB`,
|
|
364
|
+
);
|
|
265
365
|
}
|
|
266
366
|
|
|
267
367
|
// Why: Read only first 1KB to check header - don't load entire file
|
|
268
|
-
const fd = openSync(filePath,
|
|
368
|
+
const fd = openSync(filePath, "r");
|
|
269
369
|
const buffer = Buffer.alloc(1024);
|
|
270
370
|
readSync(fd, buffer, 0, 1024, 0);
|
|
271
371
|
closeSync(fd);
|
|
272
|
-
const header = buffer.toString(
|
|
372
|
+
const header = buffer.toString("utf8");
|
|
273
373
|
|
|
274
374
|
// Why: SVG files must have an <svg> element - if not, it's not a valid SVG
|
|
275
375
|
if (!CONSTANTS.SVG_HEADER_PATTERN.test(header)) {
|
|
276
|
-
throw new Error(
|
|
376
|
+
throw new Error("Not a valid SVG file (missing <svg> element)");
|
|
277
377
|
}
|
|
278
378
|
|
|
279
379
|
return true;
|
|
@@ -285,18 +385,28 @@ function validateSvgFile(filePath) {
|
|
|
285
385
|
// shares) may appear to write successfully but fail to persist data.
|
|
286
386
|
// Verification catches this immediately rather than discovering corruption later.
|
|
287
387
|
// ============================================================================
|
|
288
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Verify file was written correctly by comparing content.
|
|
390
|
+
* @param {string} filePath - Path to file
|
|
391
|
+
* @param {string} expectedContent - Expected file content
|
|
392
|
+
* @returns {boolean} True if verification passed
|
|
393
|
+
* @throws {Error} If verification failed
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
function _verifyWriteSuccess(filePath, expectedContent) {
|
|
289
397
|
// Why: Read back what was written and compare
|
|
290
|
-
const actualContent = readFileSync(filePath,
|
|
398
|
+
const actualContent = readFileSync(filePath, "utf8");
|
|
291
399
|
|
|
292
400
|
// Why: Compare lengths first (fast), then content if needed
|
|
293
401
|
if (actualContent.length !== expectedContent.length) {
|
|
294
|
-
throw new Error(
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Write verification failed: size mismatch (expected ${expectedContent.length}, got ${actualContent.length})`,
|
|
404
|
+
);
|
|
295
405
|
}
|
|
296
406
|
|
|
297
407
|
// Why: Full content comparison to catch bit flips or encoding issues
|
|
298
408
|
if (actualContent !== expectedContent) {
|
|
299
|
-
throw new Error(
|
|
409
|
+
throw new Error("Write verification failed: content mismatch");
|
|
300
410
|
}
|
|
301
411
|
|
|
302
412
|
return true;
|
|
@@ -308,9 +418,15 @@ function verifyWriteSuccess(filePath, expectedContent) {
|
|
|
308
418
|
// the bug or fix the issue themselves. This generates a timestamped log file
|
|
309
419
|
// with full context about what was being processed when the crash occurred.
|
|
310
420
|
// ============================================================================
|
|
421
|
+
/**
|
|
422
|
+
* Generate crash report with full context for debugging.
|
|
423
|
+
* @param {Error} error - Error that caused the crash
|
|
424
|
+
* @param {Object} context - Additional context information
|
|
425
|
+
* @returns {string|null} Path to crash log file, or null if write failed
|
|
426
|
+
*/
|
|
311
427
|
function generateCrashLog(error, context = {}) {
|
|
312
|
-
const crashDir = join(process.cwd(),
|
|
313
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g,
|
|
428
|
+
const crashDir = join(process.cwd(), ".svg-matrix-crashes");
|
|
429
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
314
430
|
const crashFile = join(crashDir, `crash-${timestamp}.log`);
|
|
315
431
|
|
|
316
432
|
try {
|
|
@@ -334,10 +450,10 @@ Stack:
|
|
|
334
450
|
${error.stack}
|
|
335
451
|
|
|
336
452
|
Config:
|
|
337
|
-
${JSON.stringify({ ...config, logFile: config.logFile ?
|
|
453
|
+
${JSON.stringify({ ...config, logFile: config.logFile ? "[redacted]" : null }, null, 2)}
|
|
338
454
|
`;
|
|
339
455
|
|
|
340
|
-
writeFileSync(crashFile, crashContent,
|
|
456
|
+
writeFileSync(crashFile, crashContent, "utf8");
|
|
341
457
|
logError(`Crash log written to: ${crashFile}`);
|
|
342
458
|
return crashFile;
|
|
343
459
|
} catch (e) {
|
|
@@ -351,15 +467,57 @@ ${JSON.stringify({ ...config, logFile: config.logFile ? '[redacted]' : null }, n
|
|
|
351
467
|
// PATH UTILITIES
|
|
352
468
|
// ============================================================================
|
|
353
469
|
|
|
354
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Normalize path separators to forward slashes.
|
|
472
|
+
* @param {string} p - Path to normalize
|
|
473
|
+
* @returns {string} Normalized path
|
|
474
|
+
*/
|
|
475
|
+
function normalizePath(p) {
|
|
476
|
+
return p.replace(/\\/g, "/");
|
|
477
|
+
}
|
|
355
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Resolve path to absolute path with normalized separators.
|
|
481
|
+
* @param {string} p - Path to resolve
|
|
482
|
+
* @returns {string} Absolute normalized path
|
|
483
|
+
*/
|
|
356
484
|
function resolvePath(p) {
|
|
357
|
-
return isAbsolute(p)
|
|
485
|
+
return isAbsolute(p)
|
|
486
|
+
? normalizePath(p)
|
|
487
|
+
: normalizePath(resolve(process.cwd(), p));
|
|
358
488
|
}
|
|
359
489
|
|
|
360
|
-
|
|
361
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Check if path is a directory.
|
|
492
|
+
* @param {string} p - Path to check
|
|
493
|
+
* @returns {boolean} True if directory exists
|
|
494
|
+
*/
|
|
495
|
+
function isDir(p) {
|
|
496
|
+
try {
|
|
497
|
+
return statSync(p).isDirectory();
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
362
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Check if path is a file.
|
|
505
|
+
* @param {string} p - Path to check
|
|
506
|
+
* @returns {boolean} True if file exists
|
|
507
|
+
*/
|
|
508
|
+
function isFile(p) {
|
|
509
|
+
try {
|
|
510
|
+
return statSync(p).isFile();
|
|
511
|
+
} catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Ensure directory exists, creating it if necessary.
|
|
518
|
+
* @param {string} dir - Directory path
|
|
519
|
+
* @returns {void}
|
|
520
|
+
*/
|
|
363
521
|
function ensureDir(dir) {
|
|
364
522
|
if (!existsSync(dir)) {
|
|
365
523
|
mkdirSync(dir, { recursive: true });
|
|
@@ -367,6 +525,12 @@ function ensureDir(dir) {
|
|
|
367
525
|
}
|
|
368
526
|
}
|
|
369
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Get all SVG files in directory.
|
|
530
|
+
* @param {string} dir - Directory path
|
|
531
|
+
* @param {boolean} recursive - Whether to search recursively
|
|
532
|
+
* @returns {string[]} Array of SVG file paths
|
|
533
|
+
*/
|
|
370
534
|
function getSvgFiles(dir, recursive = false) {
|
|
371
535
|
const files = [];
|
|
372
536
|
function scan(d) {
|
|
@@ -374,22 +538,32 @@ function getSvgFiles(dir, recursive = false) {
|
|
|
374
538
|
const fullPath = join(d, entry.name);
|
|
375
539
|
if (entry.isDirectory() && recursive) scan(fullPath);
|
|
376
540
|
// Why: Support both .svg and .svgz as defined in CONSTANTS.SVG_EXTENSIONS
|
|
377
|
-
else if (
|
|
541
|
+
else if (
|
|
542
|
+
entry.isFile() &&
|
|
543
|
+
CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())
|
|
544
|
+
)
|
|
545
|
+
files.push(normalizePath(fullPath));
|
|
378
546
|
}
|
|
379
547
|
}
|
|
380
548
|
scan(dir);
|
|
381
549
|
return files;
|
|
382
550
|
}
|
|
383
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Parse file list from text file.
|
|
554
|
+
* @param {string} listPath - Path to list file
|
|
555
|
+
* @returns {string[]} Array of resolved file paths
|
|
556
|
+
*/
|
|
384
557
|
function parseFileList(listPath) {
|
|
385
|
-
const content = readFileSync(listPath,
|
|
558
|
+
const content = readFileSync(listPath, "utf8");
|
|
386
559
|
const files = [];
|
|
387
560
|
for (const line of content.split(/\r?\n/)) {
|
|
388
561
|
const trimmed = line.trim();
|
|
389
|
-
if (!trimmed || trimmed.startsWith(
|
|
562
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
390
563
|
const resolved = resolvePath(trimmed);
|
|
391
564
|
if (isFile(resolved)) files.push(resolved);
|
|
392
|
-
else if (isDir(resolved))
|
|
565
|
+
else if (isDir(resolved))
|
|
566
|
+
files.push(...getSvgFiles(resolved, config.recursive));
|
|
393
567
|
else logWarn(`File not found: ${trimmed}`);
|
|
394
568
|
}
|
|
395
569
|
return files;
|
|
@@ -411,7 +585,7 @@ function parseFileList(listPath) {
|
|
|
411
585
|
*/
|
|
412
586
|
function extractNumericAttr(attrs, attrName, defaultValue = 0) {
|
|
413
587
|
// Why: Use word boundary \b to avoid matching 'rx' when looking for 'x'
|
|
414
|
-
const regex = new RegExp(`\\b${attrName}\\s*=\\s*["']([^"']+)["']`,
|
|
588
|
+
const regex = new RegExp(`\\b${attrName}\\s*=\\s*["']([^"']+)["']`, "i");
|
|
415
589
|
const match = attrs.match(regex);
|
|
416
590
|
return match ? parseFloat(match[1]) : defaultValue;
|
|
417
591
|
}
|
|
@@ -425,45 +599,58 @@ function extractNumericAttr(attrs, attrName, defaultValue = 0) {
|
|
|
425
599
|
*/
|
|
426
600
|
function extractShapeAsPath(shapeType, attrs, precision) {
|
|
427
601
|
switch (shapeType) {
|
|
428
|
-
case
|
|
429
|
-
const x = extractNumericAttr(attrs,
|
|
430
|
-
const y = extractNumericAttr(attrs,
|
|
431
|
-
const w = extractNumericAttr(attrs,
|
|
432
|
-
const h = extractNumericAttr(attrs,
|
|
433
|
-
const rx = extractNumericAttr(attrs,
|
|
434
|
-
const ry = extractNumericAttr(attrs,
|
|
602
|
+
case "rect": {
|
|
603
|
+
const x = extractNumericAttr(attrs, "x");
|
|
604
|
+
const y = extractNumericAttr(attrs, "y");
|
|
605
|
+
const w = extractNumericAttr(attrs, "width");
|
|
606
|
+
const h = extractNumericAttr(attrs, "height");
|
|
607
|
+
const rx = extractNumericAttr(attrs, "rx");
|
|
608
|
+
const ry = extractNumericAttr(attrs, "ry", rx); // ry defaults to rx per SVG spec
|
|
435
609
|
if (w <= 0 || h <= 0) return null;
|
|
436
|
-
return GeometryToPath.rectToPathData(
|
|
610
|
+
return GeometryToPath.rectToPathData(
|
|
611
|
+
x,
|
|
612
|
+
y,
|
|
613
|
+
w,
|
|
614
|
+
h,
|
|
615
|
+
rx,
|
|
616
|
+
ry,
|
|
617
|
+
false,
|
|
618
|
+
precision,
|
|
619
|
+
);
|
|
437
620
|
}
|
|
438
|
-
case
|
|
439
|
-
const cx = extractNumericAttr(attrs,
|
|
440
|
-
const cy = extractNumericAttr(attrs,
|
|
441
|
-
const r = extractNumericAttr(attrs,
|
|
621
|
+
case "circle": {
|
|
622
|
+
const cx = extractNumericAttr(attrs, "cx");
|
|
623
|
+
const cy = extractNumericAttr(attrs, "cy");
|
|
624
|
+
const r = extractNumericAttr(attrs, "r");
|
|
442
625
|
if (r <= 0) return null;
|
|
443
626
|
return GeometryToPath.circleToPathData(cx, cy, r, precision);
|
|
444
627
|
}
|
|
445
|
-
case
|
|
446
|
-
const cx = extractNumericAttr(attrs,
|
|
447
|
-
const cy = extractNumericAttr(attrs,
|
|
448
|
-
const rx = extractNumericAttr(attrs,
|
|
449
|
-
const ry = extractNumericAttr(attrs,
|
|
628
|
+
case "ellipse": {
|
|
629
|
+
const cx = extractNumericAttr(attrs, "cx");
|
|
630
|
+
const cy = extractNumericAttr(attrs, "cy");
|
|
631
|
+
const rx = extractNumericAttr(attrs, "rx");
|
|
632
|
+
const ry = extractNumericAttr(attrs, "ry");
|
|
450
633
|
if (rx <= 0 || ry <= 0) return null;
|
|
451
634
|
return GeometryToPath.ellipseToPathData(cx, cy, rx, ry, precision);
|
|
452
635
|
}
|
|
453
|
-
case
|
|
454
|
-
const x1 = extractNumericAttr(attrs,
|
|
455
|
-
const y1 = extractNumericAttr(attrs,
|
|
456
|
-
const x2 = extractNumericAttr(attrs,
|
|
457
|
-
const y2 = extractNumericAttr(attrs,
|
|
636
|
+
case "line": {
|
|
637
|
+
const x1 = extractNumericAttr(attrs, "x1");
|
|
638
|
+
const y1 = extractNumericAttr(attrs, "y1");
|
|
639
|
+
const x2 = extractNumericAttr(attrs, "x2");
|
|
640
|
+
const y2 = extractNumericAttr(attrs, "y2");
|
|
458
641
|
return GeometryToPath.lineToPathData(x1, y1, x2, y2, precision);
|
|
459
642
|
}
|
|
460
|
-
case
|
|
643
|
+
case "polygon": {
|
|
461
644
|
const points = attrs.match(/points\s*=\s*["']([^"']+)["']/)?.[1];
|
|
462
|
-
return points
|
|
645
|
+
return points
|
|
646
|
+
? GeometryToPath.polygonToPathData(points, precision)
|
|
647
|
+
: null;
|
|
463
648
|
}
|
|
464
|
-
case
|
|
649
|
+
case "polyline": {
|
|
465
650
|
const points = attrs.match(/points\s*=\s*["']([^"']+)["']/)?.[1];
|
|
466
|
-
return points
|
|
651
|
+
return points
|
|
652
|
+
? GeometryToPath.polylineToPathData(points, precision)
|
|
653
|
+
: null;
|
|
467
654
|
}
|
|
468
655
|
default:
|
|
469
656
|
return null;
|
|
@@ -477,12 +664,12 @@ function extractShapeAsPath(shapeType, attrs, precision) {
|
|
|
477
664
|
*/
|
|
478
665
|
function getShapeSpecificAttrs(shapeType) {
|
|
479
666
|
const attrMap = {
|
|
480
|
-
rect: [
|
|
481
|
-
circle: [
|
|
482
|
-
ellipse: [
|
|
483
|
-
line: [
|
|
484
|
-
polygon: [
|
|
485
|
-
polyline: [
|
|
667
|
+
rect: ["x", "y", "width", "height", "rx", "ry"],
|
|
668
|
+
circle: ["cx", "cy", "r"],
|
|
669
|
+
ellipse: ["cx", "cy", "rx", "ry"],
|
|
670
|
+
line: ["x1", "y1", "x2", "y2"],
|
|
671
|
+
polygon: ["points"],
|
|
672
|
+
polyline: ["points"],
|
|
486
673
|
};
|
|
487
674
|
return attrMap[shapeType] || [];
|
|
488
675
|
}
|
|
@@ -496,7 +683,10 @@ function getShapeSpecificAttrs(shapeType) {
|
|
|
496
683
|
function removeShapeAttrs(attrs, attrsToRemove) {
|
|
497
684
|
let result = attrs;
|
|
498
685
|
for (const attrName of attrsToRemove) {
|
|
499
|
-
result = result.replace(
|
|
686
|
+
result = result.replace(
|
|
687
|
+
new RegExp(`\\b${attrName}\\s*=\\s*["'][^"']*["']`, "gi"),
|
|
688
|
+
"",
|
|
689
|
+
);
|
|
500
690
|
}
|
|
501
691
|
return result.trim();
|
|
502
692
|
}
|
|
@@ -510,39 +700,74 @@ function removeShapeAttrs(attrs, attrsToRemove) {
|
|
|
510
700
|
// ============================================================================
|
|
511
701
|
|
|
512
702
|
// Box drawing helpers
|
|
703
|
+
/**
|
|
704
|
+
* Check if terminal supports Unicode box drawing characters.
|
|
705
|
+
* @returns {boolean} True if Unicode is supported
|
|
706
|
+
*/
|
|
513
707
|
const supportsUnicode = () => {
|
|
514
|
-
if (process.platform ===
|
|
515
|
-
return !!(
|
|
516
|
-
|
|
708
|
+
if (process.platform === "win32") {
|
|
709
|
+
return !!(
|
|
710
|
+
process.env.WT_SESSION ||
|
|
711
|
+
process.env.CHCP === "65001" ||
|
|
712
|
+
process.env.ConEmuANSI === "ON" ||
|
|
713
|
+
process.env.TERM_PROGRAM
|
|
714
|
+
);
|
|
517
715
|
}
|
|
518
716
|
return true;
|
|
519
717
|
};
|
|
520
718
|
|
|
521
719
|
const B = supportsUnicode()
|
|
522
|
-
? { tl:
|
|
523
|
-
: { tl:
|
|
720
|
+
? { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", dot: "•", arr: "→" }
|
|
721
|
+
: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", dot: "*", arr: "->" };
|
|
524
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Strip ANSI color codes from string.
|
|
725
|
+
* @param {string} s - String with ANSI codes
|
|
726
|
+
* @returns {string} String without ANSI codes
|
|
727
|
+
*/
|
|
525
728
|
function stripAnsi(s) {
|
|
526
|
-
|
|
729
|
+
// eslint-disable-next-line no-control-regex -- ANSI escape codes are intentional for terminal color stripping
|
|
730
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
527
731
|
}
|
|
528
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Create formatted box line with border.
|
|
735
|
+
* @param {string} content - Content to display
|
|
736
|
+
* @param {number} width - Box width
|
|
737
|
+
* @returns {string} Formatted line
|
|
738
|
+
*/
|
|
529
739
|
function boxLine(content, width = 70) {
|
|
530
740
|
const visible = stripAnsi(content).length;
|
|
531
741
|
const padding = Math.max(0, width - visible - 2);
|
|
532
|
-
return `${colors.cyan}${B.v}${colors.reset} ${content}${
|
|
742
|
+
return `${colors.cyan}${B.v}${colors.reset} ${content}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
|
|
533
743
|
}
|
|
534
744
|
|
|
745
|
+
/**
|
|
746
|
+
* Create formatted box header with title.
|
|
747
|
+
* @param {string} title - Header title
|
|
748
|
+
* @param {number} width - Box width
|
|
749
|
+
* @returns {string} Formatted header
|
|
750
|
+
*/
|
|
535
751
|
function boxHeader(title, width = 70) {
|
|
536
|
-
const
|
|
752
|
+
const _hr = B.h.repeat(width); // Reserved for future use (horizontal rule)
|
|
537
753
|
const visible = stripAnsi(title).length;
|
|
538
754
|
const padding = Math.max(0, width - visible - 2);
|
|
539
|
-
return `${colors.cyan}${B.v}${colors.reset} ${colors.bright}${title}${colors.reset}${
|
|
755
|
+
return `${colors.cyan}${B.v}${colors.reset} ${colors.bright}${title}${colors.reset}${" ".repeat(padding)}${colors.cyan}${B.v}${colors.reset}`;
|
|
540
756
|
}
|
|
541
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Create horizontal divider line.
|
|
760
|
+
* @param {number} width - Box width
|
|
761
|
+
* @returns {string} Divider line
|
|
762
|
+
*/
|
|
542
763
|
function boxDivider(width = 70) {
|
|
543
764
|
return `${colors.cyan}${B.v}${B.h.repeat(width)}${B.v}${colors.reset}`;
|
|
544
765
|
}
|
|
545
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Display main help message.
|
|
769
|
+
* @returns {void}
|
|
770
|
+
*/
|
|
546
771
|
function showHelp() {
|
|
547
772
|
const W = 72;
|
|
548
773
|
const hr = B.h.repeat(W);
|
|
@@ -552,36 +777,36 @@ ${colors.cyan}${B.tl}${hr}${B.tr}${colors.reset}
|
|
|
552
777
|
${boxLine(`${colors.bright}@emasoft/svg-matrix${colors.reset} v${VERSION}`, W)}
|
|
553
778
|
${boxLine(`${colors.dim}Arbitrary-precision SVG transforms with decimal.js${colors.reset}`, W)}
|
|
554
779
|
${boxDivider(W)}
|
|
555
|
-
${boxLine(
|
|
556
|
-
${boxHeader(
|
|
557
|
-
${boxLine(
|
|
780
|
+
${boxLine("", W)}
|
|
781
|
+
${boxHeader("USAGE", W)}
|
|
782
|
+
${boxLine("", W)}
|
|
558
783
|
${boxLine(` svg-matrix <command> [options] <input> [-o <output>]`, W)}
|
|
559
|
-
${boxLine(
|
|
784
|
+
${boxLine("", W)}
|
|
560
785
|
${boxDivider(W)}
|
|
561
|
-
${boxLine(
|
|
562
|
-
${boxHeader(
|
|
563
|
-
${boxLine(
|
|
786
|
+
${boxLine("", W)}
|
|
787
|
+
${boxHeader("COMMANDS", W)}
|
|
788
|
+
${boxLine("", W)}
|
|
564
789
|
${boxLine(` ${colors.green}flatten${colors.reset} TRUE flatten: resolve ALL transform dependencies`, W)}
|
|
565
790
|
${boxLine(` ${colors.dim}Bakes transforms, applies clipPaths, expands use/markers${colors.reset}`, W)}
|
|
566
|
-
${boxLine(
|
|
791
|
+
${boxLine("", W)}
|
|
567
792
|
${boxLine(` ${colors.green}convert${colors.reset} Convert shapes (rect, circle, ellipse, line) to paths`, W)}
|
|
568
793
|
${boxLine(` ${colors.dim}Preserves all style attributes${colors.reset}`, W)}
|
|
569
|
-
${boxLine(
|
|
794
|
+
${boxLine("", W)}
|
|
570
795
|
${boxLine(` ${colors.green}normalize${colors.reset} Convert all paths to absolute cubic Bezier curves`, W)}
|
|
571
796
|
${boxLine(` ${colors.dim}Ideal for animation and path morphing${colors.reset}`, W)}
|
|
572
|
-
${boxLine(
|
|
797
|
+
${boxLine("", W)}
|
|
573
798
|
${boxLine(` ${colors.green}info${colors.reset} Show SVG file information and element counts`, W)}
|
|
574
|
-
${boxLine(
|
|
799
|
+
${boxLine("", W)}
|
|
575
800
|
${boxLine(` ${colors.green}test-toolbox${colors.reset} Test all svg-toolbox functions on an SVG file`, W)}
|
|
576
801
|
${boxLine(` ${colors.dim}Creates timestamped folder with all processed versions${colors.reset}`, W)}
|
|
577
|
-
${boxLine(
|
|
802
|
+
${boxLine("", W)}
|
|
578
803
|
${boxLine(` ${colors.green}help${colors.reset} Show this help (or: svg-matrix <command> --help)`, W)}
|
|
579
804
|
${boxLine(` ${colors.green}version${colors.reset} Show version number`, W)}
|
|
580
|
-
${boxLine(
|
|
805
|
+
${boxLine("", W)}
|
|
581
806
|
${boxDivider(W)}
|
|
582
|
-
${boxLine(
|
|
583
|
-
${boxHeader(
|
|
584
|
-
${boxLine(
|
|
807
|
+
${boxLine("", W)}
|
|
808
|
+
${boxHeader("GLOBAL OPTIONS", W)}
|
|
809
|
+
${boxLine("", W)}
|
|
585
810
|
${boxLine(` ${colors.dim}-o, --output <path>${colors.reset} Output file or directory`, W)}
|
|
586
811
|
${boxLine(` ${colors.dim}-l, --list <file>${colors.reset} Read input files from text file`, W)}
|
|
587
812
|
${boxLine(` ${colors.dim}-r, --recursive${colors.reset} Process directories recursively`, W)}
|
|
@@ -591,11 +816,11 @@ ${boxLine(` ${colors.dim}-n, --dry-run${colors.reset} Show what would
|
|
|
591
816
|
${boxLine(` ${colors.dim}-q, --quiet${colors.reset} Suppress all output except errors`, W)}
|
|
592
817
|
${boxLine(` ${colors.dim}-v, --verbose${colors.reset} Enable verbose/debug output`, W)}
|
|
593
818
|
${boxLine(` ${colors.dim}--log-file <path>${colors.reset} Write log to file`, W)}
|
|
594
|
-
${boxLine(
|
|
819
|
+
${boxLine("", W)}
|
|
595
820
|
${boxDivider(W)}
|
|
596
|
-
${boxLine(
|
|
597
|
-
${boxHeader(
|
|
598
|
-
${boxLine(
|
|
821
|
+
${boxLine("", W)}
|
|
822
|
+
${boxHeader("FLATTEN OPTIONS", W)}
|
|
823
|
+
${boxLine("", W)}
|
|
599
824
|
${boxLine(` ${colors.dim}--transform-only${colors.reset} Only flatten transforms (skip resolvers)`, W)}
|
|
600
825
|
${boxLine(` ${colors.dim}--no-clip-paths${colors.reset} Skip clipPath boolean operations`, W)}
|
|
601
826
|
${boxLine(` ${colors.dim}--no-masks${colors.reset} Skip mask to clip conversion`, W)}
|
|
@@ -607,52 +832,59 @@ ${boxLine(` ${colors.dim}--preserve-vendor${colors.reset} Keep vendor pre
|
|
|
607
832
|
${boxLine(` ${colors.dim}(inkscape, sodipodi, -webkit-*, etc.)${colors.reset}`, W)}
|
|
608
833
|
${boxLine(` ${colors.dim}--preserve-ns <list>${colors.reset} Preserve specific namespaces (comma-separated)`, W)}
|
|
609
834
|
${boxLine(` ${colors.dim}Example: --preserve-ns inkscape,sodipodi${colors.reset}`, W)}
|
|
610
|
-
${boxLine(
|
|
835
|
+
${boxLine(` ${colors.dim}--svg2-polyfills${colors.reset} Inject JavaScript polyfills for SVG 2 features`, W)}
|
|
836
|
+
${boxLine(` ${colors.dim}--no-minify-polyfills${colors.reset} Use full (non-minified) polyfills for debugging`, W)}
|
|
837
|
+
${boxLine("", W)}
|
|
611
838
|
${boxDivider(W)}
|
|
612
|
-
${boxLine(
|
|
613
|
-
${boxHeader(
|
|
614
|
-
${boxLine(
|
|
839
|
+
${boxLine("", W)}
|
|
840
|
+
${boxHeader("PRECISION OPTIONS", W)}
|
|
841
|
+
${boxLine("", W)}
|
|
615
842
|
${boxLine(` ${colors.dim}--clip-segments <n>${colors.reset} Polygon samples for clipping (default: 64)`, W)}
|
|
616
843
|
${boxLine(` ${colors.dim}64=balanced, 128=high, 256=very high${colors.reset}`, W)}
|
|
617
|
-
${boxLine(
|
|
844
|
+
${boxLine("", W)}
|
|
618
845
|
${boxLine(` ${colors.dim}--bezier-arcs <n>${colors.reset} Bezier arcs for curves (default: 8)`, W)}
|
|
619
846
|
${boxLine(` ${colors.dim}Must be multiple of 4. Error rates:${colors.reset}`, W)}
|
|
620
847
|
${boxLine(` ${colors.dim}8: 0.0004%, 16: 0.000007%, 64: 0.00000001%${colors.reset}`, W)}
|
|
621
|
-
${boxLine(
|
|
848
|
+
${boxLine("", W)}
|
|
622
849
|
${boxLine(` ${colors.dim}--e2e-tolerance <exp>${colors.reset} Verification tolerance (default: 1e-10)`, W)}
|
|
623
850
|
${boxLine(` ${colors.dim}Examples: 1e-8, 1e-10, 1e-12, 1e-14${colors.reset}`, W)}
|
|
624
|
-
${boxLine(
|
|
851
|
+
${boxLine("", W)}
|
|
625
852
|
${boxLine(` ${colors.yellow}${B.dot} Mathematical verification is ALWAYS enabled${colors.reset}`, W)}
|
|
626
853
|
${boxLine(` ${colors.yellow}${B.dot} Precision is non-negotiable in this library${colors.reset}`, W)}
|
|
627
|
-
${boxLine(
|
|
854
|
+
${boxLine("", W)}
|
|
628
855
|
${boxDivider(W)}
|
|
629
|
-
${boxLine(
|
|
630
|
-
${boxHeader(
|
|
631
|
-
${boxLine(
|
|
856
|
+
${boxLine("", W)}
|
|
857
|
+
${boxHeader("EXAMPLES", W)}
|
|
858
|
+
${boxLine("", W)}
|
|
632
859
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} input.svg -o output.svg`, W)}
|
|
633
860
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} ./svgs/ -o ./out/ --transform-only`, W)}
|
|
634
861
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} --list files.txt -o ./out/ --no-patterns`, W)}
|
|
635
862
|
${boxLine(` ${colors.green}svg-matrix flatten${colors.reset} input.svg -o out.svg --preserve-ns inkscape,sodipodi`, W)}
|
|
636
863
|
${boxLine(` ${colors.green}svg-matrix convert${colors.reset} input.svg -o output.svg -p 10`, W)}
|
|
637
864
|
${boxLine(` ${colors.green}svg-matrix info${colors.reset} input.svg`, W)}
|
|
638
|
-
${boxLine(
|
|
865
|
+
${boxLine("", W)}
|
|
639
866
|
${boxDivider(W)}
|
|
640
|
-
${boxLine(
|
|
641
|
-
${boxHeader(
|
|
642
|
-
${boxLine(
|
|
867
|
+
${boxLine("", W)}
|
|
868
|
+
${boxHeader("JAVASCRIPT API", W)}
|
|
869
|
+
${boxLine("", W)}
|
|
643
870
|
${boxLine(` import { Matrix, Vector, Transforms2D } from '@emasoft/svg-matrix'`, W)}
|
|
644
|
-
${boxLine(
|
|
871
|
+
${boxLine("", W)}
|
|
645
872
|
${boxLine(` ${colors.green}${B.dot} Matrix${colors.reset} Arbitrary-precision matrix operations`, W)}
|
|
646
873
|
${boxLine(` ${colors.green}${B.dot} Vector${colors.reset} High-precision vector math`, W)}
|
|
647
874
|
${boxLine(` ${colors.green}${B.dot} Transforms2D${colors.reset} rotate, scale, translate, skew, reflect`, W)}
|
|
648
875
|
${boxLine(` ${colors.green}${B.dot} Transforms3D${colors.reset} 3D affine transformations`, W)}
|
|
649
|
-
${boxLine(
|
|
876
|
+
${boxLine("", W)}
|
|
650
877
|
${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
651
878
|
|
|
652
879
|
${colors.cyan}Docs:${colors.reset} https://github.com/Emasoft/SVG-MATRIX#readme
|
|
653
880
|
`);
|
|
654
881
|
}
|
|
655
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Display command-specific help message.
|
|
885
|
+
* @param {string} command - Command name
|
|
886
|
+
* @returns {void}
|
|
887
|
+
*/
|
|
656
888
|
function showCommandHelp(command) {
|
|
657
889
|
const W = 72;
|
|
658
890
|
const hr = B.h.repeat(W);
|
|
@@ -662,12 +894,12 @@ function showCommandHelp(command) {
|
|
|
662
894
|
${colors.cyan}${B.tl}${hr}${B.tr}${colors.reset}
|
|
663
895
|
${boxLine(`${colors.bright}svg-matrix flatten${colors.reset} - TRUE SVG Flattening`, W)}
|
|
664
896
|
${boxDivider(W)}
|
|
665
|
-
${boxLine(
|
|
897
|
+
${boxLine("", W)}
|
|
666
898
|
${boxLine(`Resolves ALL transform dependencies to produce a "flat" SVG where`, W)}
|
|
667
899
|
${boxLine(`every coordinate is in the root coordinate system.`, W)}
|
|
668
|
-
${boxLine(
|
|
669
|
-
${boxHeader(
|
|
670
|
-
${boxLine(
|
|
900
|
+
${boxLine("", W)}
|
|
901
|
+
${boxHeader("WHAT IT DOES", W)}
|
|
902
|
+
${boxLine("", W)}
|
|
671
903
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Bakes transform attributes into path coordinates`, W)}
|
|
672
904
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Applies clipPath as boolean intersection operations`, W)}
|
|
673
905
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Converts masks to equivalent clipped geometry`, W)}
|
|
@@ -675,110 +907,110 @@ ${boxLine(` ${colors.green}${B.dot}${colors.reset} Expands <use> and <symbol> r
|
|
|
675
907
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Instantiates markers as actual path geometry`, W)}
|
|
676
908
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Expands pattern fills to tiled geometry`, W)}
|
|
677
909
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Bakes gradientTransform into gradient coordinates`, W)}
|
|
678
|
-
${boxLine(
|
|
679
|
-
${boxHeader(
|
|
680
|
-
${boxLine(
|
|
910
|
+
${boxLine("", W)}
|
|
911
|
+
${boxHeader("USAGE", W)}
|
|
912
|
+
${boxLine("", W)}
|
|
681
913
|
${boxLine(` svg-matrix flatten <input> [options]`, W)}
|
|
682
|
-
${boxLine(
|
|
683
|
-
${boxHeader(
|
|
684
|
-
${boxLine(
|
|
914
|
+
${boxLine("", W)}
|
|
915
|
+
${boxHeader("EXAMPLES", W)}
|
|
916
|
+
${boxLine("", W)}
|
|
685
917
|
${boxLine(` ${colors.dim}# Full flatten (all resolvers enabled)${colors.reset}`, W)}
|
|
686
918
|
${boxLine(` svg-matrix flatten input.svg -o output.svg`, W)}
|
|
687
|
-
${boxLine(
|
|
919
|
+
${boxLine("", W)}
|
|
688
920
|
${boxLine(` ${colors.dim}# Transform-only mode (legacy, faster)${colors.reset}`, W)}
|
|
689
921
|
${boxLine(` svg-matrix flatten input.svg -o out.svg --transform-only`, W)}
|
|
690
|
-
${boxLine(
|
|
922
|
+
${boxLine("", W)}
|
|
691
923
|
${boxLine(` ${colors.dim}# High-precision mode for complex curves${colors.reset}`, W)}
|
|
692
924
|
${boxLine(` svg-matrix flatten in.svg -o out.svg --clip-segments 128`, W)}
|
|
693
|
-
${boxLine(
|
|
925
|
+
${boxLine("", W)}
|
|
694
926
|
${boxLine(` ${colors.dim}# Skip specific resolvers${colors.reset}`, W)}
|
|
695
927
|
${boxLine(` svg-matrix flatten in.svg -o out.svg --no-patterns --no-markers`, W)}
|
|
696
|
-
${boxLine(
|
|
928
|
+
${boxLine("", W)}
|
|
697
929
|
${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
698
930
|
`,
|
|
699
931
|
convert: `
|
|
700
932
|
${colors.cyan}${B.tl}${hr}${B.tr}${colors.reset}
|
|
701
933
|
${boxLine(`${colors.bright}svg-matrix convert${colors.reset} - Shape to Path Conversion`, W)}
|
|
702
934
|
${boxDivider(W)}
|
|
703
|
-
${boxLine(
|
|
935
|
+
${boxLine("", W)}
|
|
704
936
|
${boxLine(`Converts all basic shapes to <path> elements while preserving`, W)}
|
|
705
937
|
${boxLine(`all style and presentation attributes.`, W)}
|
|
706
|
-
${boxLine(
|
|
707
|
-
${boxHeader(
|
|
708
|
-
${boxLine(
|
|
938
|
+
${boxLine("", W)}
|
|
939
|
+
${boxHeader("SUPPORTED SHAPES", W)}
|
|
940
|
+
${boxLine("", W)}
|
|
709
941
|
${boxLine(` ${colors.green}${B.dot} rect${colors.reset} ${B.arr} path (with optional rounded corners)`, W)}
|
|
710
942
|
${boxLine(` ${colors.green}${B.dot} circle${colors.reset} ${B.arr} path (4 cubic Bezier arcs)`, W)}
|
|
711
943
|
${boxLine(` ${colors.green}${B.dot} ellipse${colors.reset} ${B.arr} path (4 cubic Bezier arcs)`, W)}
|
|
712
944
|
${boxLine(` ${colors.green}${B.dot} line${colors.reset} ${B.arr} path (M...L command)`, W)}
|
|
713
945
|
${boxLine(` ${colors.green}${B.dot} polygon${colors.reset} ${B.arr} path (closed polyline)`, W)}
|
|
714
946
|
${boxLine(` ${colors.green}${B.dot} polyline${colors.reset} ${B.arr} path (open polyline)`, W)}
|
|
715
|
-
${boxLine(
|
|
716
|
-
${boxHeader(
|
|
717
|
-
${boxLine(
|
|
947
|
+
${boxLine("", W)}
|
|
948
|
+
${boxHeader("USAGE", W)}
|
|
949
|
+
${boxLine("", W)}
|
|
718
950
|
${boxLine(` svg-matrix convert <input> -o <output> [-p <precision>]`, W)}
|
|
719
|
-
${boxLine(
|
|
720
|
-
${boxHeader(
|
|
721
|
-
${boxLine(
|
|
951
|
+
${boxLine("", W)}
|
|
952
|
+
${boxHeader("EXAMPLE", W)}
|
|
953
|
+
${boxLine("", W)}
|
|
722
954
|
${boxLine(` svg-matrix convert shapes.svg -o paths.svg --precision 8`, W)}
|
|
723
|
-
${boxLine(
|
|
955
|
+
${boxLine("", W)}
|
|
724
956
|
${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
725
957
|
`,
|
|
726
958
|
normalize: `
|
|
727
959
|
${colors.cyan}${B.tl}${hr}${B.tr}${colors.reset}
|
|
728
960
|
${boxLine(`${colors.bright}svg-matrix normalize${colors.reset} - Path Normalization`, W)}
|
|
729
961
|
${boxDivider(W)}
|
|
730
|
-
${boxLine(
|
|
962
|
+
${boxLine("", W)}
|
|
731
963
|
${boxLine(`Converts all path commands to absolute cubic Bezier curves.`, W)}
|
|
732
964
|
${boxLine(`Ideal for path morphing, animation, and consistent processing.`, W)}
|
|
733
|
-
${boxLine(
|
|
734
|
-
${boxHeader(
|
|
735
|
-
${boxLine(
|
|
965
|
+
${boxLine("", W)}
|
|
966
|
+
${boxHeader("WHAT IT DOES", W)}
|
|
967
|
+
${boxLine("", W)}
|
|
736
968
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Converts relative commands (m,l,c,s,q,t,a) to absolute`, W)}
|
|
737
969
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Converts all curves to cubic Beziers (C commands)`, W)}
|
|
738
970
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Expands shorthand (S,T) to full curves`, W)}
|
|
739
971
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Converts arcs (A) to cubic Bezier approximations`, W)}
|
|
740
972
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Converts lines (L,H,V) to degenerate cubics`, W)}
|
|
741
|
-
${boxLine(
|
|
742
|
-
${boxHeader(
|
|
743
|
-
${boxLine(
|
|
973
|
+
${boxLine("", W)}
|
|
974
|
+
${boxHeader("OUTPUT FORMAT", W)}
|
|
975
|
+
${boxLine("", W)}
|
|
744
976
|
${boxLine(` All paths become: M x y C x1 y1 x2 y2 x y C ... Z`, W)}
|
|
745
|
-
${boxLine(
|
|
746
|
-
${boxHeader(
|
|
747
|
-
${boxLine(
|
|
977
|
+
${boxLine("", W)}
|
|
978
|
+
${boxHeader("USAGE", W)}
|
|
979
|
+
${boxLine("", W)}
|
|
748
980
|
${boxLine(` svg-matrix normalize <input> -o <output>`, W)}
|
|
749
|
-
${boxLine(
|
|
750
|
-
${boxHeader(
|
|
751
|
-
${boxLine(
|
|
981
|
+
${boxLine("", W)}
|
|
982
|
+
${boxHeader("EXAMPLE", W)}
|
|
983
|
+
${boxLine("", W)}
|
|
752
984
|
${boxLine(` svg-matrix normalize complex.svg -o normalized.svg`, W)}
|
|
753
|
-
${boxLine(
|
|
985
|
+
${boxLine("", W)}
|
|
754
986
|
${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
755
987
|
`,
|
|
756
988
|
info: `
|
|
757
989
|
${colors.cyan}${B.tl}${hr}${B.tr}${colors.reset}
|
|
758
990
|
${boxLine(`${colors.bright}svg-matrix info${colors.reset} - SVG File Information`, W)}
|
|
759
991
|
${boxDivider(W)}
|
|
760
|
-
${boxLine(
|
|
992
|
+
${boxLine("", W)}
|
|
761
993
|
${boxLine(`Displays detailed information about an SVG file including`, W)}
|
|
762
994
|
${boxLine(`dimensions, element counts, and structure analysis.`, W)}
|
|
763
|
-
${boxLine(
|
|
764
|
-
${boxHeader(
|
|
765
|
-
${boxLine(
|
|
995
|
+
${boxLine("", W)}
|
|
996
|
+
${boxHeader("INFORMATION SHOWN", W)}
|
|
997
|
+
${boxLine("", W)}
|
|
766
998
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} File path and size`, W)}
|
|
767
999
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Dimensions (viewBox, width, height)`, W)}
|
|
768
1000
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Element counts (paths, shapes, groups)`, W)}
|
|
769
1001
|
${boxLine(` ${colors.green}${B.dot}${colors.reset} Transform attribute count`, W)}
|
|
770
|
-
${boxLine(
|
|
771
|
-
${boxHeader(
|
|
772
|
-
${boxLine(
|
|
1002
|
+
${boxLine("", W)}
|
|
1003
|
+
${boxHeader("USAGE", W)}
|
|
1004
|
+
${boxLine("", W)}
|
|
773
1005
|
${boxLine(` svg-matrix info <input>`, W)}
|
|
774
1006
|
${boxLine(` svg-matrix info <folder> -r ${colors.dim}# recursive${colors.reset}`, W)}
|
|
775
|
-
${boxLine(
|
|
776
|
-
${boxHeader(
|
|
777
|
-
${boxLine(
|
|
1007
|
+
${boxLine("", W)}
|
|
1008
|
+
${boxHeader("EXAMPLE", W)}
|
|
1009
|
+
${boxLine("", W)}
|
|
778
1010
|
${boxLine(` svg-matrix info logo.svg`, W)}
|
|
779
|
-
${boxLine(
|
|
1011
|
+
${boxLine("", W)}
|
|
780
1012
|
${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
781
|
-
|
|
1013
|
+
`,
|
|
782
1014
|
};
|
|
783
1015
|
|
|
784
1016
|
if (commandHelp[command]) {
|
|
@@ -788,7 +1020,13 @@ ${colors.cyan}${B.bl}${hr}${B.br}${colors.reset}
|
|
|
788
1020
|
}
|
|
789
1021
|
}
|
|
790
1022
|
|
|
791
|
-
|
|
1023
|
+
/**
|
|
1024
|
+
* Display version number.
|
|
1025
|
+
* @returns {void}
|
|
1026
|
+
*/
|
|
1027
|
+
function showVersion() {
|
|
1028
|
+
console.log(`@emasoft/svg-matrix v${VERSION}`);
|
|
1029
|
+
}
|
|
792
1030
|
|
|
793
1031
|
/**
|
|
794
1032
|
* Extract transform attribute value from element attributes string.
|
|
@@ -806,7 +1044,7 @@ function extractTransform(attrs) {
|
|
|
806
1044
|
* @returns {string} Attributes without transform
|
|
807
1045
|
*/
|
|
808
1046
|
function removeTransform(attrs) {
|
|
809
|
-
return attrs.replace(/\s*transform\s*=\s*["'][^"']*["']/gi,
|
|
1047
|
+
return attrs.replace(/\s*transform\s*=\s*["'][^"']*["']/gi, "");
|
|
810
1048
|
}
|
|
811
1049
|
|
|
812
1050
|
/**
|
|
@@ -850,7 +1088,7 @@ function replacePathD(attrs, newD) {
|
|
|
850
1088
|
function processFlatten(inputPath, outputPath) {
|
|
851
1089
|
try {
|
|
852
1090
|
logDebug(`Processing: ${inputPath}`);
|
|
853
|
-
const svgContent = readFileSync(inputPath,
|
|
1091
|
+
const svgContent = readFileSync(inputPath, "utf8");
|
|
854
1092
|
|
|
855
1093
|
// Use legacy transform-only mode if requested
|
|
856
1094
|
if (config.transformOnly) {
|
|
@@ -861,9 +1099,9 @@ function processFlatten(inputPath, outputPath) {
|
|
|
861
1099
|
const pipelineOptions = {
|
|
862
1100
|
precision: config.precision,
|
|
863
1101
|
curveSegments: 20,
|
|
864
|
-
clipSegments: config.clipSegments,
|
|
865
|
-
bezierArcs: config.bezierArcs,
|
|
866
|
-
e2eTolerance: config.e2eTolerance,
|
|
1102
|
+
clipSegments: config.clipSegments, // Higher segments for clip accuracy (default 64)
|
|
1103
|
+
bezierArcs: config.bezierArcs, // Bezier arcs for circles/ellipses (default 8)
|
|
1104
|
+
e2eTolerance: config.e2eTolerance, // Configurable E2E tolerance (default 1e-10)
|
|
867
1105
|
resolveUse: config.resolveUse,
|
|
868
1106
|
resolveMarkers: config.resolveMarkers,
|
|
869
1107
|
resolvePatterns: config.resolvePatterns,
|
|
@@ -873,26 +1111,35 @@ function processFlatten(inputPath, outputPath) {
|
|
|
873
1111
|
bakeGradients: config.bakeGradients,
|
|
874
1112
|
removeUnusedDefs: true,
|
|
875
1113
|
preserveNamespaces: config.preserveNamespaces, // Pass namespace preservation to pipeline
|
|
1114
|
+
svg2Polyfills: config.svg2Polyfills, // Pass SVG 2 polyfills flag to pipeline
|
|
876
1115
|
// NOTE: Verification is ALWAYS enabled - precision is non-negotiable
|
|
877
1116
|
};
|
|
878
1117
|
|
|
879
1118
|
// Run the full flatten pipeline
|
|
880
|
-
const { svg: flattenedSvg, stats } = FlattenPipeline.flattenSVG(
|
|
1119
|
+
const { svg: flattenedSvg, stats } = FlattenPipeline.flattenSVG(
|
|
1120
|
+
svgContent,
|
|
1121
|
+
pipelineOptions,
|
|
1122
|
+
);
|
|
881
1123
|
|
|
882
1124
|
// Report statistics
|
|
883
1125
|
const parts = [];
|
|
884
|
-
if (stats.transformsFlattened > 0)
|
|
1126
|
+
if (stats.transformsFlattened > 0)
|
|
1127
|
+
parts.push(`${stats.transformsFlattened} transforms`);
|
|
885
1128
|
if (stats.useResolved > 0) parts.push(`${stats.useResolved} use`);
|
|
886
|
-
if (stats.markersResolved > 0)
|
|
887
|
-
|
|
1129
|
+
if (stats.markersResolved > 0)
|
|
1130
|
+
parts.push(`${stats.markersResolved} markers`);
|
|
1131
|
+
if (stats.patternsResolved > 0)
|
|
1132
|
+
parts.push(`${stats.patternsResolved} patterns`);
|
|
888
1133
|
if (stats.masksResolved > 0) parts.push(`${stats.masksResolved} masks`);
|
|
889
|
-
if (stats.clipPathsApplied > 0)
|
|
890
|
-
|
|
1134
|
+
if (stats.clipPathsApplied > 0)
|
|
1135
|
+
parts.push(`${stats.clipPathsApplied} clipPaths`);
|
|
1136
|
+
if (stats.gradientsProcessed > 0)
|
|
1137
|
+
parts.push(`${stats.gradientsProcessed} gradients`);
|
|
891
1138
|
|
|
892
1139
|
if (parts.length > 0) {
|
|
893
|
-
logInfo(`Flattened: ${parts.join(
|
|
1140
|
+
logInfo(`Flattened: ${parts.join(", ")}`);
|
|
894
1141
|
} else {
|
|
895
|
-
logInfo(
|
|
1142
|
+
logInfo("No transform dependencies found");
|
|
896
1143
|
}
|
|
897
1144
|
|
|
898
1145
|
// Report verification results (ALWAYS - precision is non-negotiable)
|
|
@@ -908,28 +1155,46 @@ function processFlatten(inputPath, outputPath) {
|
|
|
908
1155
|
// Show detailed results in verbose mode
|
|
909
1156
|
if (config.verbose) {
|
|
910
1157
|
if (v.matrices.length > 0) {
|
|
911
|
-
logDebug(
|
|
1158
|
+
logDebug(
|
|
1159
|
+
` Matrix verifications: ${v.matrices.filter((m) => m.valid).length}/${v.matrices.length} passed`,
|
|
1160
|
+
);
|
|
912
1161
|
}
|
|
913
1162
|
if (v.transforms.length > 0) {
|
|
914
|
-
logDebug(
|
|
1163
|
+
logDebug(
|
|
1164
|
+
` Transform round-trips: ${v.transforms.filter((t) => t.valid).length}/${v.transforms.length} passed`,
|
|
1165
|
+
);
|
|
915
1166
|
}
|
|
916
1167
|
if (v.polygons.length > 0) {
|
|
917
|
-
logDebug(
|
|
1168
|
+
logDebug(
|
|
1169
|
+
` Polygon intersections: ${v.polygons.filter((p) => p.valid).length}/${v.polygons.length} passed`,
|
|
1170
|
+
);
|
|
918
1171
|
}
|
|
919
1172
|
if (v.gradients.length > 0) {
|
|
920
|
-
logDebug(
|
|
1173
|
+
logDebug(
|
|
1174
|
+
` Gradient transforms: ${v.gradients.filter((g) => g.valid).length}/${v.gradients.length} passed`,
|
|
1175
|
+
);
|
|
921
1176
|
}
|
|
922
1177
|
if (v.e2e && v.e2e.length > 0) {
|
|
923
|
-
logDebug(
|
|
1178
|
+
logDebug(
|
|
1179
|
+
` E2E area conservation: ${v.e2e.filter((e) => e.valid).length}/${v.e2e.length} passed`,
|
|
1180
|
+
);
|
|
924
1181
|
}
|
|
925
1182
|
}
|
|
926
1183
|
|
|
927
1184
|
// Always show failed verifications (not just in verbose mode)
|
|
928
|
-
const allVerifications = [
|
|
929
|
-
|
|
1185
|
+
const allVerifications = [
|
|
1186
|
+
...v.matrices,
|
|
1187
|
+
...v.transforms,
|
|
1188
|
+
...v.polygons,
|
|
1189
|
+
...v.gradients,
|
|
1190
|
+
...(v.e2e || []),
|
|
1191
|
+
];
|
|
1192
|
+
const failed = allVerifications.filter((vr) => !vr.valid);
|
|
930
1193
|
if (failed.length > 0) {
|
|
931
1194
|
for (const f of failed.slice(0, 3)) {
|
|
932
|
-
logError(
|
|
1195
|
+
logError(
|
|
1196
|
+
`${colors.red}VERIFICATION FAILED:${colors.reset} ${f.message}`,
|
|
1197
|
+
);
|
|
933
1198
|
}
|
|
934
1199
|
if (failed.length > 3) {
|
|
935
1200
|
logError(`...and ${failed.length - 3} more failed verifications`);
|
|
@@ -950,7 +1215,7 @@ function processFlatten(inputPath, outputPath) {
|
|
|
950
1215
|
|
|
951
1216
|
if (!config.dryRun) {
|
|
952
1217
|
ensureDir(dirname(outputPath));
|
|
953
|
-
writeFileSync(outputPath, flattenedSvg,
|
|
1218
|
+
writeFileSync(outputPath, flattenedSvg, "utf8");
|
|
954
1219
|
}
|
|
955
1220
|
logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
|
|
956
1221
|
return true;
|
|
@@ -983,12 +1248,14 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
983
1248
|
|
|
984
1249
|
try {
|
|
985
1250
|
const ctm = SVGFlatten.parseTransformAttribute(transform);
|
|
986
|
-
const transformedD = SVGFlatten.transformPathData(pathD, ctm, {
|
|
1251
|
+
const transformedD = SVGFlatten.transformPathData(pathD, ctm, {
|
|
1252
|
+
precision: config.precision,
|
|
1253
|
+
});
|
|
987
1254
|
const newAttrs = removeTransform(replacePathD(attrs, transformedD));
|
|
988
1255
|
transformCount++;
|
|
989
1256
|
pathCount++;
|
|
990
1257
|
logDebug(`Flattened path transform: ${transform}`);
|
|
991
|
-
return `<path ${newAttrs.trim()}${match.endsWith(
|
|
1258
|
+
return `<path ${newAttrs.trim()}${match.endsWith("/>") ? "/>" : ">"}`;
|
|
992
1259
|
} catch (e) {
|
|
993
1260
|
logWarn(`Failed to flatten path: ${e.message}`);
|
|
994
1261
|
return match;
|
|
@@ -996,10 +1263,17 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
996
1263
|
});
|
|
997
1264
|
|
|
998
1265
|
// Step 2: Convert shapes with transforms to flattened paths
|
|
999
|
-
const shapeTypes = [
|
|
1266
|
+
const shapeTypes = [
|
|
1267
|
+
"rect",
|
|
1268
|
+
"circle",
|
|
1269
|
+
"ellipse",
|
|
1270
|
+
"line",
|
|
1271
|
+
"polygon",
|
|
1272
|
+
"polyline",
|
|
1273
|
+
];
|
|
1000
1274
|
|
|
1001
1275
|
for (const shapeType of shapeTypes) {
|
|
1002
|
-
const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`,
|
|
1276
|
+
const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`, "gi");
|
|
1003
1277
|
|
|
1004
1278
|
result = result.replace(shapeRegex, (match, attrs) => {
|
|
1005
1279
|
const transform = extractTransform(attrs);
|
|
@@ -1014,13 +1288,18 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
1014
1288
|
}
|
|
1015
1289
|
|
|
1016
1290
|
const ctm = SVGFlatten.parseTransformAttribute(transform);
|
|
1017
|
-
const transformedD = SVGFlatten.transformPathData(pathD, ctm, {
|
|
1291
|
+
const transformedD = SVGFlatten.transformPathData(pathD, ctm, {
|
|
1292
|
+
precision: config.precision,
|
|
1293
|
+
});
|
|
1018
1294
|
const attrsToRemove = getShapeSpecificAttrs(shapeType);
|
|
1019
|
-
const styleAttrs = removeShapeAttrs(
|
|
1295
|
+
const styleAttrs = removeShapeAttrs(
|
|
1296
|
+
removeTransform(attrs),
|
|
1297
|
+
attrsToRemove,
|
|
1298
|
+
);
|
|
1020
1299
|
transformCount++;
|
|
1021
1300
|
shapeCount++;
|
|
1022
1301
|
logDebug(`Flattened ${shapeType} transform: ${transform}`);
|
|
1023
|
-
return `<path d="${transformedD}"${styleAttrs ?
|
|
1302
|
+
return `<path d="${transformedD}"${styleAttrs ? " " + styleAttrs : ""}/>`;
|
|
1024
1303
|
} catch (e) {
|
|
1025
1304
|
logWarn(`Failed to flatten ${shapeType}: ${e.message}`);
|
|
1026
1305
|
return match;
|
|
@@ -1041,33 +1320,47 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
1041
1320
|
let modifiedContent = content;
|
|
1042
1321
|
let childrenModified = false;
|
|
1043
1322
|
|
|
1044
|
-
modifiedContent = modifiedContent.replace(
|
|
1045
|
-
|
|
1046
|
-
|
|
1323
|
+
modifiedContent = modifiedContent.replace(
|
|
1324
|
+
/<path\s+([^>]*?)\s*\/?>/gi,
|
|
1325
|
+
(pathMatch, pathAttrs) => {
|
|
1326
|
+
const pathD = extractPathD(pathAttrs);
|
|
1327
|
+
if (!pathD) return pathMatch;
|
|
1047
1328
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1329
|
+
try {
|
|
1330
|
+
const childTransform = extractTransform(pathAttrs);
|
|
1331
|
+
let combinedCtm = groupCtm;
|
|
1051
1332
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1333
|
+
if (childTransform) {
|
|
1334
|
+
const childCtm =
|
|
1335
|
+
SVGFlatten.parseTransformAttribute(childTransform);
|
|
1336
|
+
combinedCtm = groupCtm.mul(childCtm);
|
|
1337
|
+
}
|
|
1056
1338
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1339
|
+
const transformedD = SVGFlatten.transformPathData(
|
|
1340
|
+
pathD,
|
|
1341
|
+
combinedCtm,
|
|
1342
|
+
{ precision: config.precision },
|
|
1343
|
+
);
|
|
1344
|
+
const newAttrs = removeTransform(
|
|
1345
|
+
replacePathD(pathAttrs, transformedD),
|
|
1346
|
+
);
|
|
1347
|
+
childrenModified = true;
|
|
1348
|
+
transformCount++;
|
|
1349
|
+
return `<path ${newAttrs.trim()}${pathMatch.endsWith("/>") ? "/>" : ">"}`;
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
logWarn(
|
|
1352
|
+
`Failed to apply group transform to path: ${e.message}`,
|
|
1353
|
+
);
|
|
1354
|
+
return pathMatch;
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
);
|
|
1067
1358
|
|
|
1068
1359
|
if (childrenModified) {
|
|
1069
1360
|
const newGAttrs = removeTransform(gAttrs);
|
|
1070
|
-
logDebug(
|
|
1361
|
+
logDebug(
|
|
1362
|
+
`Propagated group transform to children: ${groupTransform}`,
|
|
1363
|
+
);
|
|
1071
1364
|
return `<g${newGAttrs}>${modifiedContent}</g>`;
|
|
1072
1365
|
}
|
|
1073
1366
|
return match;
|
|
@@ -1075,7 +1368,7 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
1075
1368
|
logWarn(`Failed to process group: ${e.message}`);
|
|
1076
1369
|
return match;
|
|
1077
1370
|
}
|
|
1078
|
-
}
|
|
1371
|
+
},
|
|
1079
1372
|
);
|
|
1080
1373
|
|
|
1081
1374
|
if (result === beforeResult) {
|
|
@@ -1084,31 +1377,50 @@ function processFlattenLegacy(inputPath, outputPath, svgContent) {
|
|
|
1084
1377
|
groupIterations++;
|
|
1085
1378
|
}
|
|
1086
1379
|
|
|
1087
|
-
logInfo(
|
|
1380
|
+
logInfo(
|
|
1381
|
+
`Flattened ${transformCount} transforms (${pathCount} paths, ${shapeCount} shapes) [legacy mode]`,
|
|
1382
|
+
);
|
|
1088
1383
|
|
|
1089
1384
|
if (!config.dryRun) {
|
|
1090
1385
|
ensureDir(dirname(outputPath));
|
|
1091
|
-
writeFileSync(outputPath, result,
|
|
1386
|
+
writeFileSync(outputPath, result, "utf8");
|
|
1092
1387
|
}
|
|
1093
1388
|
logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
|
|
1094
1389
|
return true;
|
|
1095
1390
|
}
|
|
1096
1391
|
|
|
1392
|
+
/**
|
|
1393
|
+
* Convert all basic shapes to paths.
|
|
1394
|
+
* @param {string} inputPath - Input file path
|
|
1395
|
+
* @param {string} outputPath - Output file path
|
|
1396
|
+
* @returns {boolean} True if successful
|
|
1397
|
+
*/
|
|
1097
1398
|
function processConvert(inputPath, outputPath) {
|
|
1098
1399
|
try {
|
|
1099
1400
|
logDebug(`Converting: ${inputPath}`);
|
|
1100
|
-
let result = readFileSync(inputPath,
|
|
1401
|
+
let result = readFileSync(inputPath, "utf8");
|
|
1101
1402
|
|
|
1102
1403
|
// Convert all shape types to paths
|
|
1103
|
-
const shapeTypes = [
|
|
1404
|
+
const shapeTypes = [
|
|
1405
|
+
"rect",
|
|
1406
|
+
"circle",
|
|
1407
|
+
"ellipse",
|
|
1408
|
+
"line",
|
|
1409
|
+
"polygon",
|
|
1410
|
+
"polyline",
|
|
1411
|
+
];
|
|
1104
1412
|
|
|
1105
1413
|
for (const shapeType of shapeTypes) {
|
|
1106
|
-
const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`,
|
|
1414
|
+
const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`, "gi");
|
|
1107
1415
|
|
|
1108
1416
|
result = result.replace(shapeRegex, (match, attrs) => {
|
|
1109
1417
|
try {
|
|
1110
1418
|
// Extract shape as path using helper
|
|
1111
|
-
const pathData = extractShapeAsPath(
|
|
1419
|
+
const pathData = extractShapeAsPath(
|
|
1420
|
+
shapeType,
|
|
1421
|
+
attrs,
|
|
1422
|
+
config.precision,
|
|
1423
|
+
);
|
|
1112
1424
|
|
|
1113
1425
|
if (!pathData) {
|
|
1114
1426
|
return match; // Couldn't convert to path
|
|
@@ -1118,7 +1430,7 @@ function processConvert(inputPath, outputPath) {
|
|
|
1118
1430
|
const attrsToRemove = getShapeSpecificAttrs(shapeType);
|
|
1119
1431
|
const otherAttrs = removeShapeAttrs(attrs, attrsToRemove);
|
|
1120
1432
|
|
|
1121
|
-
return `<path d="${pathData}"${otherAttrs ?
|
|
1433
|
+
return `<path d="${pathData}"${otherAttrs ? " " + otherAttrs : ""}/>`;
|
|
1122
1434
|
} catch (e) {
|
|
1123
1435
|
logWarn(`Failed to convert ${shapeType}: ${e.message}`);
|
|
1124
1436
|
return match;
|
|
@@ -1128,7 +1440,7 @@ function processConvert(inputPath, outputPath) {
|
|
|
1128
1440
|
|
|
1129
1441
|
if (!config.dryRun) {
|
|
1130
1442
|
ensureDir(dirname(outputPath));
|
|
1131
|
-
writeFileSync(outputPath, result,
|
|
1443
|
+
writeFileSync(outputPath, result, "utf8");
|
|
1132
1444
|
}
|
|
1133
1445
|
logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
|
|
1134
1446
|
return true;
|
|
@@ -1138,21 +1450,29 @@ function processConvert(inputPath, outputPath) {
|
|
|
1138
1450
|
}
|
|
1139
1451
|
}
|
|
1140
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* Normalize all path commands to absolute cubic Beziers.
|
|
1455
|
+
* @param {string} inputPath - Input file path
|
|
1456
|
+
* @param {string} outputPath - Output file path
|
|
1457
|
+
* @returns {boolean} True if successful
|
|
1458
|
+
*/
|
|
1141
1459
|
function processNormalize(inputPath, outputPath) {
|
|
1142
1460
|
try {
|
|
1143
1461
|
logDebug(`Normalizing: ${inputPath}`);
|
|
1144
|
-
let result = readFileSync(inputPath,
|
|
1462
|
+
let result = readFileSync(inputPath, "utf8");
|
|
1145
1463
|
|
|
1146
1464
|
result = result.replace(/d\s*=\s*["']([^"']+)["']/gi, (match, pathData) => {
|
|
1147
1465
|
try {
|
|
1148
1466
|
const normalized = GeometryToPath.pathToCubics(pathData);
|
|
1149
1467
|
return `d="${normalized}"`;
|
|
1150
|
-
} catch {
|
|
1468
|
+
} catch {
|
|
1469
|
+
return match;
|
|
1470
|
+
}
|
|
1151
1471
|
});
|
|
1152
1472
|
|
|
1153
1473
|
if (!config.dryRun) {
|
|
1154
1474
|
ensureDir(dirname(outputPath));
|
|
1155
|
-
writeFileSync(outputPath, result,
|
|
1475
|
+
writeFileSync(outputPath, result, "utf8");
|
|
1156
1476
|
}
|
|
1157
1477
|
logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
|
|
1158
1478
|
return true;
|
|
@@ -1162,12 +1482,19 @@ function processNormalize(inputPath, outputPath) {
|
|
|
1162
1482
|
}
|
|
1163
1483
|
}
|
|
1164
1484
|
|
|
1485
|
+
/**
|
|
1486
|
+
* Display information about SVG file.
|
|
1487
|
+
* @param {string} inputPath - Input file path
|
|
1488
|
+
* @returns {boolean} True if successful
|
|
1489
|
+
*/
|
|
1165
1490
|
function processInfo(inputPath) {
|
|
1166
1491
|
try {
|
|
1167
|
-
const svg = readFileSync(inputPath,
|
|
1168
|
-
const vb = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i)?.[1] ||
|
|
1169
|
-
const w =
|
|
1170
|
-
|
|
1492
|
+
const svg = readFileSync(inputPath, "utf8");
|
|
1493
|
+
const vb = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i)?.[1] || "not set";
|
|
1494
|
+
const w =
|
|
1495
|
+
svg.match(/<svg[^>]*\swidth\s*=\s*["']([^"']+)["']/i)?.[1] || "not set";
|
|
1496
|
+
const h =
|
|
1497
|
+
svg.match(/<svg[^>]*\sheight\s*=\s*["']([^"']+)["']/i)?.[1] || "not set";
|
|
1171
1498
|
|
|
1172
1499
|
console.log(`
|
|
1173
1500
|
${colors.cyan}File:${colors.reset} ${inputPath}
|
|
@@ -1194,41 +1521,139 @@ ${colors.bright}Elements:${colors.reset}
|
|
|
1194
1521
|
|
|
1195
1522
|
// All testable functions from svg-toolbox.js organized by category
|
|
1196
1523
|
const TOOLBOX_FUNCTIONS = {
|
|
1197
|
-
cleanup: [
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1524
|
+
cleanup: [
|
|
1525
|
+
"cleanupIds",
|
|
1526
|
+
"cleanupNumericValues",
|
|
1527
|
+
"cleanupListOfValues",
|
|
1528
|
+
"cleanupAttributes",
|
|
1529
|
+
"cleanupEnableBackground",
|
|
1530
|
+
],
|
|
1531
|
+
remove: [
|
|
1532
|
+
"removeUnknownsAndDefaults",
|
|
1533
|
+
"removeNonInheritableGroupAttrs",
|
|
1534
|
+
"removeUselessDefs",
|
|
1535
|
+
"removeHiddenElements",
|
|
1536
|
+
"removeEmptyText",
|
|
1537
|
+
"removeEmptyContainers",
|
|
1538
|
+
"removeDoctype",
|
|
1539
|
+
"removeXMLProcInst",
|
|
1540
|
+
"removeComments",
|
|
1541
|
+
"removeMetadata",
|
|
1542
|
+
"removeTitle",
|
|
1543
|
+
"removeDesc",
|
|
1544
|
+
"removeEditorsNSData",
|
|
1545
|
+
"removeEmptyAttrs",
|
|
1546
|
+
"removeViewBox",
|
|
1547
|
+
"removeXMLNS",
|
|
1548
|
+
"removeRasterImages",
|
|
1549
|
+
"removeScriptElement",
|
|
1550
|
+
"removeStyleElement",
|
|
1551
|
+
"removeXlink",
|
|
1552
|
+
"removeDimensions",
|
|
1553
|
+
"removeAttrs",
|
|
1554
|
+
"removeElementsByAttr",
|
|
1555
|
+
"removeAttributesBySelector",
|
|
1556
|
+
"removeOffCanvasPath",
|
|
1557
|
+
],
|
|
1558
|
+
convert: [
|
|
1559
|
+
"convertShapesToPath",
|
|
1560
|
+
"convertPathData",
|
|
1561
|
+
"convertTransform",
|
|
1562
|
+
"convertColors",
|
|
1563
|
+
"convertStyleToAttrs",
|
|
1564
|
+
"convertEllipseToCircle",
|
|
1565
|
+
],
|
|
1566
|
+
collapse: ["collapseGroups", "mergePaths"],
|
|
1567
|
+
move: ["moveGroupAttrsToElems", "moveElemsAttrsToGroup"],
|
|
1568
|
+
style: ["minifyStyles", "inlineStyles"],
|
|
1569
|
+
sort: ["sortAttrs", "sortDefsChildren"],
|
|
1570
|
+
pathOptimization: [
|
|
1571
|
+
"optimizePaths",
|
|
1572
|
+
"simplifyPaths",
|
|
1573
|
+
"simplifyPath",
|
|
1574
|
+
"reusePaths",
|
|
1575
|
+
],
|
|
1576
|
+
flatten: [
|
|
1577
|
+
"flattenClipPaths",
|
|
1578
|
+
"flattenMasks",
|
|
1579
|
+
"flattenGradients",
|
|
1580
|
+
"flattenPatterns",
|
|
1581
|
+
"flattenFilters",
|
|
1582
|
+
"flattenUseElements",
|
|
1583
|
+
"flattenAll",
|
|
1584
|
+
],
|
|
1585
|
+
transform: ["decomposeTransform"],
|
|
1586
|
+
other: [
|
|
1587
|
+
"addAttributesToSVGElement",
|
|
1588
|
+
"addClassesToSVGElement",
|
|
1589
|
+
"prefixIds",
|
|
1590
|
+
"optimizeAnimationTiming",
|
|
1591
|
+
],
|
|
1592
|
+
validation: ["validateXML", "validateSVG", "fixInvalidSvg"],
|
|
1593
|
+
detection: ["detectCollisions", "measureDistance"],
|
|
1210
1594
|
};
|
|
1211
1595
|
|
|
1212
|
-
const SKIP_TOOLBOX_FUNCTIONS = [
|
|
1596
|
+
const SKIP_TOOLBOX_FUNCTIONS = [
|
|
1597
|
+
"imageToPath",
|
|
1598
|
+
"detectCollisions",
|
|
1599
|
+
"measureDistance",
|
|
1600
|
+
];
|
|
1213
1601
|
|
|
1602
|
+
/**
|
|
1603
|
+
* Generate timestamp string for file naming.
|
|
1604
|
+
* @returns {string} Timestamp in YYYYMMDD_HHMMSS format
|
|
1605
|
+
*/
|
|
1214
1606
|
function getTimestamp() {
|
|
1215
1607
|
const now = new Date();
|
|
1216
|
-
const pad = (n) => String(n).padStart(2,
|
|
1608
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1217
1609
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1218
1610
|
}
|
|
1219
1611
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1612
|
+
/**
|
|
1613
|
+
* Test single svg-toolbox function on SVG content.
|
|
1614
|
+
* @param {string} fnName - Function name to test
|
|
1615
|
+
* @param {string} originalContent - Original SVG content
|
|
1616
|
+
* @param {number} originalSize - Original content size in bytes
|
|
1617
|
+
* @param {string} outputDir - Output directory for results
|
|
1618
|
+
* @returns {Promise<Object>} Test result with status and metrics
|
|
1619
|
+
*/
|
|
1620
|
+
async function testToolboxFunction(
|
|
1621
|
+
fnName,
|
|
1622
|
+
originalContent,
|
|
1623
|
+
originalSize,
|
|
1624
|
+
outputDir,
|
|
1625
|
+
) {
|
|
1626
|
+
const result = {
|
|
1627
|
+
name: fnName,
|
|
1628
|
+
status: "unknown",
|
|
1629
|
+
outputSize: 0,
|
|
1630
|
+
sizeDiff: 0,
|
|
1631
|
+
sizeDiffPercent: 0,
|
|
1632
|
+
error: null,
|
|
1633
|
+
outputFile: null,
|
|
1634
|
+
timeMs: 0,
|
|
1635
|
+
};
|
|
1222
1636
|
|
|
1223
1637
|
try {
|
|
1224
1638
|
const fn = SVGToolbox[fnName];
|
|
1225
|
-
if (!fn) {
|
|
1226
|
-
|
|
1639
|
+
if (!fn) {
|
|
1640
|
+
result.status = "not_found";
|
|
1641
|
+
result.error = `Function not found`;
|
|
1642
|
+
return result;
|
|
1643
|
+
}
|
|
1644
|
+
if (SKIP_TOOLBOX_FUNCTIONS.includes(fnName)) {
|
|
1645
|
+
result.status = "skipped";
|
|
1646
|
+
result.error = "Requires special handling";
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1227
1649
|
|
|
1228
1650
|
const startTime = Date.now();
|
|
1229
1651
|
const doc = parseSVG(originalContent);
|
|
1230
1652
|
// Pass preserveVendor and preserveNamespaces options from config to toolbox functions
|
|
1231
|
-
await fn(doc, {
|
|
1653
|
+
await fn(doc, {
|
|
1654
|
+
preserveVendor: config.preserveVendor,
|
|
1655
|
+
preserveNamespaces: config.preserveNamespaces,
|
|
1656
|
+
});
|
|
1232
1657
|
const output = serializeSVG(doc);
|
|
1233
1658
|
const outputSize = Buffer.byteLength(output);
|
|
1234
1659
|
result.timeMs = Date.now() - startTime;
|
|
@@ -1236,24 +1661,40 @@ async function testToolboxFunction(fnName, originalContent, originalSize, output
|
|
|
1236
1661
|
const outputFile = join(outputDir, `${fnName}.svg`);
|
|
1237
1662
|
writeFileSync(outputFile, output);
|
|
1238
1663
|
|
|
1239
|
-
result.status =
|
|
1664
|
+
result.status = "success";
|
|
1240
1665
|
result.outputSize = outputSize;
|
|
1241
1666
|
result.sizeDiff = outputSize - originalSize;
|
|
1242
|
-
result.sizeDiffPercent = (
|
|
1667
|
+
result.sizeDiffPercent = (
|
|
1668
|
+
((outputSize - originalSize) / originalSize) *
|
|
1669
|
+
100
|
|
1670
|
+
).toFixed(1);
|
|
1243
1671
|
result.outputFile = outputFile;
|
|
1244
1672
|
|
|
1245
|
-
if (output.length < 100) {
|
|
1673
|
+
if (output.length < 100) {
|
|
1674
|
+
result.status = "warning";
|
|
1675
|
+
result.error = "Output suspiciously small";
|
|
1676
|
+
}
|
|
1246
1677
|
} catch (err) {
|
|
1247
|
-
result.status =
|
|
1678
|
+
result.status = "error";
|
|
1248
1679
|
result.error = err.message;
|
|
1249
1680
|
}
|
|
1250
1681
|
return result;
|
|
1251
1682
|
}
|
|
1252
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* Process test-toolbox command to test all svg-toolbox functions.
|
|
1686
|
+
* @returns {Promise<void>}
|
|
1687
|
+
*/
|
|
1253
1688
|
async function processTestToolbox() {
|
|
1254
1689
|
const files = gatherInputFiles();
|
|
1255
|
-
if (files.length === 0) {
|
|
1256
|
-
|
|
1690
|
+
if (files.length === 0) {
|
|
1691
|
+
logError("No input file specified");
|
|
1692
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1693
|
+
}
|
|
1694
|
+
if (files.length > 1) {
|
|
1695
|
+
logError("test-toolbox only accepts one input file");
|
|
1696
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1697
|
+
}
|
|
1257
1698
|
|
|
1258
1699
|
const inputFile = files[0];
|
|
1259
1700
|
validateSvgFile(inputFile);
|
|
@@ -1265,11 +1706,11 @@ async function processTestToolbox() {
|
|
|
1265
1706
|
const outputDir = join(outputBase, outputDirName);
|
|
1266
1707
|
mkdirSync(outputDir, { recursive: true });
|
|
1267
1708
|
|
|
1268
|
-
console.log(
|
|
1709
|
+
console.log("\n" + "=".repeat(70));
|
|
1269
1710
|
console.log(`${colors.bright}SVG-TOOLBOX FUNCTION TEST${colors.reset}`);
|
|
1270
|
-
console.log(
|
|
1711
|
+
console.log("=".repeat(70) + "\n");
|
|
1271
1712
|
|
|
1272
|
-
const originalContent = readFileSync(inputFile,
|
|
1713
|
+
const originalContent = readFileSync(inputFile, "utf8");
|
|
1273
1714
|
const originalSize = Buffer.byteLength(originalContent);
|
|
1274
1715
|
|
|
1275
1716
|
logInfo(`Input: ${colors.cyan}${inputFile}${colors.reset}`);
|
|
@@ -1277,24 +1718,39 @@ async function processTestToolbox() {
|
|
|
1277
1718
|
logInfo(`Output: ${colors.cyan}${outputDir}/${colors.reset}\n`);
|
|
1278
1719
|
|
|
1279
1720
|
const allResults = [];
|
|
1280
|
-
let total = 0,
|
|
1721
|
+
let total = 0,
|
|
1722
|
+
success = 0,
|
|
1723
|
+
errors = 0,
|
|
1724
|
+
skipped = 0;
|
|
1281
1725
|
|
|
1282
1726
|
for (const [category, functions] of Object.entries(TOOLBOX_FUNCTIONS)) {
|
|
1283
|
-
console.log(
|
|
1727
|
+
console.log(
|
|
1728
|
+
`\n${colors.bright}--- ${category.toUpperCase()} ---${colors.reset}`,
|
|
1729
|
+
);
|
|
1284
1730
|
for (const fnName of functions) {
|
|
1285
1731
|
total++;
|
|
1286
1732
|
process.stdout.write(` Testing ${fnName.padEnd(30)}... `);
|
|
1287
|
-
const result = await testToolboxFunction(
|
|
1733
|
+
const result = await testToolboxFunction(
|
|
1734
|
+
fnName,
|
|
1735
|
+
originalContent,
|
|
1736
|
+
originalSize,
|
|
1737
|
+
outputDir,
|
|
1738
|
+
);
|
|
1288
1739
|
allResults.push({ category, ...result });
|
|
1289
1740
|
|
|
1290
|
-
if (result.status ===
|
|
1741
|
+
if (result.status === "success") {
|
|
1291
1742
|
success++;
|
|
1292
|
-
const sizeStr =
|
|
1293
|
-
|
|
1294
|
-
|
|
1743
|
+
const sizeStr =
|
|
1744
|
+
result.sizeDiff >= 0
|
|
1745
|
+
? `+${result.sizeDiffPercent}%`
|
|
1746
|
+
: `${result.sizeDiffPercent}%`;
|
|
1747
|
+
console.log(
|
|
1748
|
+
`${colors.green}OK${colors.reset} (${sizeStr}, ${result.timeMs}ms)`,
|
|
1749
|
+
);
|
|
1750
|
+
} else if (result.status === "skipped" || result.status === "not_found") {
|
|
1295
1751
|
skipped++;
|
|
1296
1752
|
console.log(`${colors.yellow}SKIP${colors.reset}: ${result.error}`);
|
|
1297
|
-
} else if (result.status ===
|
|
1753
|
+
} else if (result.status === "warning") {
|
|
1298
1754
|
success++;
|
|
1299
1755
|
console.log(`${colors.yellow}WARN${colors.reset}: ${result.error}`);
|
|
1300
1756
|
} else {
|
|
@@ -1304,41 +1760,60 @@ async function processTestToolbox() {
|
|
|
1304
1760
|
}
|
|
1305
1761
|
}
|
|
1306
1762
|
|
|
1307
|
-
console.log(
|
|
1763
|
+
console.log("\n" + "=".repeat(70));
|
|
1308
1764
|
console.log(`${colors.bright}SUMMARY${colors.reset}`);
|
|
1309
|
-
console.log(
|
|
1310
|
-
console.log(
|
|
1765
|
+
console.log("=".repeat(70));
|
|
1766
|
+
console.log(
|
|
1767
|
+
`Total: ${total} ${colors.green}Success: ${success}${colors.reset} ${colors.red}Errors: ${errors}${colors.reset} ${colors.yellow}Skipped: ${skipped}${colors.reset}`,
|
|
1768
|
+
);
|
|
1311
1769
|
|
|
1312
|
-
console.log(
|
|
1770
|
+
console.log("\n" + "-".repeat(70));
|
|
1313
1771
|
console.log(`${colors.bright}TOP SIZE REDUCERS${colors.reset}`);
|
|
1314
|
-
console.log(
|
|
1315
|
-
|
|
1316
|
-
const successResults = allResults
|
|
1317
|
-
|
|
1318
|
-
|
|
1772
|
+
console.log("-".repeat(70));
|
|
1773
|
+
|
|
1774
|
+
const successResults = allResults
|
|
1775
|
+
.filter((r) => r.status === "success" || r.status === "warning")
|
|
1776
|
+
.sort((a, b) => a.sizeDiff - b.sizeDiff)
|
|
1777
|
+
.slice(0, 10);
|
|
1778
|
+
console.log("\nFunction Output Size Diff");
|
|
1779
|
+
console.log("-".repeat(60));
|
|
1319
1780
|
for (const r of successResults) {
|
|
1320
1781
|
const name = r.name.padEnd(38);
|
|
1321
|
-
const size = ((r.outputSize / 1024 / 1024).toFixed(2) +
|
|
1322
|
-
const diff = (r.sizeDiff >= 0 ?
|
|
1782
|
+
const size = ((r.outputSize / 1024 / 1024).toFixed(2) + " MB").padStart(10);
|
|
1783
|
+
const diff = (r.sizeDiff >= 0 ? "+" : "") + r.sizeDiffPercent + "%";
|
|
1323
1784
|
console.log(`${name} ${size} ${diff}`);
|
|
1324
1785
|
}
|
|
1325
1786
|
|
|
1326
1787
|
if (errors > 0) {
|
|
1327
|
-
console.log(
|
|
1788
|
+
console.log("\n" + "-".repeat(70));
|
|
1328
1789
|
console.log(`${colors.red}${colors.bright}ERROR DETAILS${colors.reset}`);
|
|
1329
|
-
console.log(
|
|
1330
|
-
for (const r of allResults.filter(
|
|
1790
|
+
console.log("-".repeat(70));
|
|
1791
|
+
for (const r of allResults.filter((item) => item.status === "error")) {
|
|
1331
1792
|
console.log(`\n${colors.red}${r.name}:${colors.reset} ${r.error}`);
|
|
1332
1793
|
}
|
|
1333
1794
|
}
|
|
1334
1795
|
|
|
1335
|
-
const resultsFile = join(outputDir,
|
|
1336
|
-
writeFileSync(
|
|
1337
|
-
|
|
1338
|
-
|
|
1796
|
+
const resultsFile = join(outputDir, "_test-results.json");
|
|
1797
|
+
writeFileSync(
|
|
1798
|
+
resultsFile,
|
|
1799
|
+
JSON.stringify(
|
|
1800
|
+
{
|
|
1801
|
+
timestamp: new Date().toISOString(),
|
|
1802
|
+
inputFile,
|
|
1803
|
+
originalSize,
|
|
1804
|
+
outputDir,
|
|
1805
|
+
summary: { total, success, errors, skipped },
|
|
1806
|
+
results: allResults,
|
|
1807
|
+
},
|
|
1808
|
+
null,
|
|
1809
|
+
2,
|
|
1810
|
+
),
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1813
|
+
console.log("\n" + "=".repeat(70));
|
|
1339
1814
|
logInfo(`Results: ${colors.cyan}${resultsFile}${colors.reset}`);
|
|
1340
1815
|
logInfo(`Output: ${colors.cyan}${outputDir}${colors.reset}`);
|
|
1341
|
-
console.log(
|
|
1816
|
+
console.log("=".repeat(70) + "\n");
|
|
1342
1817
|
|
|
1343
1818
|
if (errors > 0) process.exit(CONSTANTS.EXIT_ERROR);
|
|
1344
1819
|
}
|
|
@@ -1347,83 +1822,163 @@ async function processTestToolbox() {
|
|
|
1347
1822
|
// ARGUMENT PARSING
|
|
1348
1823
|
// ============================================================================
|
|
1349
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Parse command-line arguments.
|
|
1827
|
+
* @param {string[]} args - Command-line arguments
|
|
1828
|
+
* @returns {CLIConfig} Parsed configuration object
|
|
1829
|
+
*/
|
|
1350
1830
|
function parseArgs(args) {
|
|
1351
1831
|
const cfg = { ...DEFAULT_CONFIG };
|
|
1352
1832
|
const inputs = [];
|
|
1353
1833
|
let i = 0;
|
|
1354
1834
|
|
|
1355
1835
|
while (i < args.length) {
|
|
1356
|
-
|
|
1836
|
+
let arg = args[i];
|
|
1837
|
+
let argValue = null;
|
|
1838
|
+
|
|
1839
|
+
// Handle --arg=value format
|
|
1840
|
+
if (arg.includes("=") && arg.startsWith("--")) {
|
|
1841
|
+
const eqIdx = arg.indexOf("=");
|
|
1842
|
+
argValue = arg.substring(eqIdx + 1);
|
|
1843
|
+
arg = arg.substring(0, eqIdx);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1357
1846
|
switch (arg) {
|
|
1358
|
-
case
|
|
1359
|
-
case
|
|
1360
|
-
|
|
1361
|
-
|
|
1847
|
+
case "-o":
|
|
1848
|
+
case "--output":
|
|
1849
|
+
cfg.output = args[++i];
|
|
1850
|
+
break;
|
|
1851
|
+
case "-l":
|
|
1852
|
+
case "--list":
|
|
1853
|
+
cfg.listFile = args[++i];
|
|
1854
|
+
break;
|
|
1855
|
+
case "-r":
|
|
1856
|
+
case "--recursive":
|
|
1857
|
+
cfg.recursive = true;
|
|
1858
|
+
break;
|
|
1859
|
+
case "-p":
|
|
1860
|
+
case "--precision": {
|
|
1362
1861
|
const precision = parseInt(args[++i], 10);
|
|
1363
|
-
if (
|
|
1364
|
-
|
|
1862
|
+
if (
|
|
1863
|
+
isNaN(precision) ||
|
|
1864
|
+
precision < CONSTANTS.MIN_PRECISION ||
|
|
1865
|
+
precision > CONSTANTS.MAX_PRECISION
|
|
1866
|
+
) {
|
|
1867
|
+
logError(
|
|
1868
|
+
`Precision must be between ${CONSTANTS.MIN_PRECISION} and ${CONSTANTS.MAX_PRECISION}`,
|
|
1869
|
+
);
|
|
1365
1870
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1366
1871
|
}
|
|
1367
1872
|
cfg.precision = precision;
|
|
1368
1873
|
break;
|
|
1369
1874
|
}
|
|
1370
|
-
case
|
|
1371
|
-
case
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
case
|
|
1375
|
-
case
|
|
1875
|
+
case "-f":
|
|
1876
|
+
case "--force":
|
|
1877
|
+
cfg.overwrite = true;
|
|
1878
|
+
break;
|
|
1879
|
+
case "-n":
|
|
1880
|
+
case "--dry-run":
|
|
1881
|
+
cfg.dryRun = true;
|
|
1882
|
+
break;
|
|
1883
|
+
case "-q":
|
|
1884
|
+
case "--quiet":
|
|
1885
|
+
cfg.quiet = true;
|
|
1886
|
+
break;
|
|
1887
|
+
case "-v":
|
|
1888
|
+
case "--verbose":
|
|
1889
|
+
cfg.verbose = true;
|
|
1890
|
+
break;
|
|
1891
|
+
case "--log-file":
|
|
1892
|
+
cfg.logFile = args[++i];
|
|
1893
|
+
break;
|
|
1894
|
+
case "-h":
|
|
1895
|
+
case "--help":
|
|
1376
1896
|
// If a command is already set (not 'help'), show command-specific help
|
|
1377
|
-
if (
|
|
1897
|
+
if (
|
|
1898
|
+
cfg.command !== "help" &&
|
|
1899
|
+
["flatten", "convert", "normalize", "info", "test-toolbox"].includes(
|
|
1900
|
+
cfg.command,
|
|
1901
|
+
)
|
|
1902
|
+
) {
|
|
1378
1903
|
cfg.showCommandHelp = true;
|
|
1379
1904
|
} else {
|
|
1380
|
-
cfg.command =
|
|
1905
|
+
cfg.command = "help";
|
|
1381
1906
|
}
|
|
1382
1907
|
break;
|
|
1383
|
-
case
|
|
1908
|
+
case "--version":
|
|
1909
|
+
cfg.command = "version";
|
|
1910
|
+
break;
|
|
1384
1911
|
// Full flatten pipeline options
|
|
1385
|
-
case
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
case
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
case
|
|
1912
|
+
case "--transform-only":
|
|
1913
|
+
cfg.transformOnly = true;
|
|
1914
|
+
break;
|
|
1915
|
+
case "--no-clip-paths":
|
|
1916
|
+
cfg.resolveClipPaths = false;
|
|
1917
|
+
break;
|
|
1918
|
+
case "--no-masks":
|
|
1919
|
+
cfg.resolveMasks = false;
|
|
1920
|
+
break;
|
|
1921
|
+
case "--no-use":
|
|
1922
|
+
cfg.resolveUse = false;
|
|
1923
|
+
break;
|
|
1924
|
+
case "--no-markers":
|
|
1925
|
+
cfg.resolveMarkers = false;
|
|
1926
|
+
break;
|
|
1927
|
+
case "--no-patterns":
|
|
1928
|
+
cfg.resolvePatterns = false;
|
|
1929
|
+
break;
|
|
1930
|
+
case "--no-gradients":
|
|
1931
|
+
cfg.bakeGradients = false;
|
|
1932
|
+
break;
|
|
1392
1933
|
// Vendor preservation option
|
|
1393
|
-
case
|
|
1934
|
+
case "--preserve-vendor":
|
|
1935
|
+
cfg.preserveVendor = true;
|
|
1936
|
+
break;
|
|
1394
1937
|
// Namespace preservation option (comma-separated list)
|
|
1395
|
-
case
|
|
1396
|
-
const namespaces = args[++i];
|
|
1938
|
+
case "--preserve-ns": {
|
|
1939
|
+
const namespaces = argValue || args[++i];
|
|
1397
1940
|
if (!namespaces) {
|
|
1398
|
-
logError(
|
|
1941
|
+
logError(
|
|
1942
|
+
"--preserve-ns requires a comma-separated list of namespaces",
|
|
1943
|
+
);
|
|
1399
1944
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1400
1945
|
}
|
|
1401
|
-
cfg.preserveNamespaces = namespaces
|
|
1946
|
+
cfg.preserveNamespaces = namespaces
|
|
1947
|
+
.split(",")
|
|
1948
|
+
.map((ns) => ns.trim().toLowerCase())
|
|
1949
|
+
.filter((ns) => ns.length > 0);
|
|
1402
1950
|
break;
|
|
1403
1951
|
}
|
|
1952
|
+
case "--svg2-polyfills":
|
|
1953
|
+
cfg.svg2Polyfills = true;
|
|
1954
|
+
break;
|
|
1955
|
+
case "--no-minify-polyfills":
|
|
1956
|
+
cfg.noMinifyPolyfills = true;
|
|
1957
|
+
setPolyfillMinification(false);
|
|
1958
|
+
break;
|
|
1404
1959
|
// E2E verification precision options
|
|
1405
|
-
case
|
|
1960
|
+
case "--clip-segments": {
|
|
1406
1961
|
const segs = parseInt(args[++i], 10);
|
|
1407
1962
|
if (isNaN(segs) || segs < 8 || segs > 512) {
|
|
1408
|
-
logError(
|
|
1963
|
+
logError("clip-segments must be between 8 and 512");
|
|
1409
1964
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1410
1965
|
}
|
|
1411
1966
|
cfg.clipSegments = segs;
|
|
1412
1967
|
break;
|
|
1413
1968
|
}
|
|
1414
|
-
case
|
|
1969
|
+
case "--bezier-arcs": {
|
|
1415
1970
|
const arcs = parseInt(args[++i], 10);
|
|
1416
1971
|
if (isNaN(arcs) || arcs < 4 || arcs > 128) {
|
|
1417
|
-
logError(
|
|
1972
|
+
logError("bezier-arcs must be between 4 and 128");
|
|
1418
1973
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1419
1974
|
}
|
|
1420
1975
|
cfg.bezierArcs = arcs;
|
|
1421
1976
|
break;
|
|
1422
1977
|
}
|
|
1423
|
-
case
|
|
1978
|
+
case "--e2e-tolerance": {
|
|
1424
1979
|
const tol = args[++i];
|
|
1425
1980
|
if (!/^1e-\d+$/.test(tol)) {
|
|
1426
|
-
logError(
|
|
1981
|
+
logError("e2e-tolerance must be in format 1e-N (e.g., 1e-10, 1e-12)");
|
|
1427
1982
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1428
1983
|
}
|
|
1429
1984
|
cfg.e2eTolerance = tol;
|
|
@@ -1431,8 +1986,22 @@ function parseArgs(args) {
|
|
|
1431
1986
|
}
|
|
1432
1987
|
// NOTE: --verify removed - verification is ALWAYS enabled
|
|
1433
1988
|
default:
|
|
1434
|
-
if (arg.startsWith(
|
|
1435
|
-
|
|
1989
|
+
if (arg.startsWith("-")) {
|
|
1990
|
+
logError(`Unknown option: ${arg}`);
|
|
1991
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1992
|
+
}
|
|
1993
|
+
if (
|
|
1994
|
+
[
|
|
1995
|
+
"flatten",
|
|
1996
|
+
"convert",
|
|
1997
|
+
"normalize",
|
|
1998
|
+
"info",
|
|
1999
|
+
"test-toolbox",
|
|
2000
|
+
"help",
|
|
2001
|
+
"version",
|
|
2002
|
+
].includes(arg) &&
|
|
2003
|
+
cfg.command === "help"
|
|
2004
|
+
) {
|
|
1436
2005
|
cfg.command = arg;
|
|
1437
2006
|
} else {
|
|
1438
2007
|
inputs.push(arg);
|
|
@@ -1444,27 +2013,40 @@ function parseArgs(args) {
|
|
|
1444
2013
|
return cfg;
|
|
1445
2014
|
}
|
|
1446
2015
|
|
|
2016
|
+
/**
|
|
2017
|
+
* Gather input files from config inputs and list file.
|
|
2018
|
+
* @returns {string[]} Array of input file paths
|
|
2019
|
+
*/
|
|
1447
2020
|
function gatherInputFiles() {
|
|
1448
2021
|
const files = [];
|
|
1449
2022
|
if (config.listFile) {
|
|
1450
2023
|
const listPath = resolvePath(config.listFile);
|
|
1451
2024
|
// Why: Use CONSTANTS.EXIT_ERROR for consistency with all other error exits
|
|
1452
|
-
if (!isFile(listPath)) {
|
|
2025
|
+
if (!isFile(listPath)) {
|
|
2026
|
+
logError(`List file not found: ${config.listFile}`);
|
|
2027
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
2028
|
+
}
|
|
1453
2029
|
files.push(...parseFileList(listPath));
|
|
1454
2030
|
}
|
|
1455
2031
|
for (const input of config.inputs) {
|
|
1456
2032
|
const resolved = resolvePath(input);
|
|
1457
2033
|
if (isFile(resolved)) files.push(resolved);
|
|
1458
|
-
else if (isDir(resolved))
|
|
2034
|
+
else if (isDir(resolved))
|
|
2035
|
+
files.push(...getSvgFiles(resolved, config.recursive));
|
|
1459
2036
|
else logWarn(`Input not found: ${input}`);
|
|
1460
2037
|
}
|
|
1461
2038
|
return files;
|
|
1462
2039
|
}
|
|
1463
2040
|
|
|
2041
|
+
/**
|
|
2042
|
+
* Get output path for input file based on config.
|
|
2043
|
+
* @param {string} inputPath - Input file path
|
|
2044
|
+
* @returns {string} Output file path
|
|
2045
|
+
*/
|
|
1464
2046
|
function getOutputPath(inputPath) {
|
|
1465
2047
|
if (!config.output) {
|
|
1466
2048
|
const dir = dirname(inputPath);
|
|
1467
|
-
const base = basename(inputPath,
|
|
2049
|
+
const base = basename(inputPath, ".svg");
|
|
1468
2050
|
return join(dir, `${base}-processed.svg`);
|
|
1469
2051
|
}
|
|
1470
2052
|
const output = resolvePath(config.output);
|
|
@@ -1478,10 +2060,17 @@ function getOutputPath(inputPath) {
|
|
|
1478
2060
|
// MAIN
|
|
1479
2061
|
// ============================================================================
|
|
1480
2062
|
|
|
2063
|
+
/**
|
|
2064
|
+
* Main entry point for CLI.
|
|
2065
|
+
* @returns {Promise<void>}
|
|
2066
|
+
*/
|
|
1481
2067
|
async function main() {
|
|
1482
2068
|
try {
|
|
1483
2069
|
const args = process.argv.slice(2);
|
|
1484
|
-
if (args.length === 0) {
|
|
2070
|
+
if (args.length === 0) {
|
|
2071
|
+
showHelp();
|
|
2072
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
2073
|
+
}
|
|
1485
2074
|
|
|
1486
2075
|
config = parseArgs(args);
|
|
1487
2076
|
|
|
@@ -1495,25 +2084,40 @@ async function main() {
|
|
|
1495
2084
|
try {
|
|
1496
2085
|
if (existsSync(config.logFile)) unlinkSync(config.logFile);
|
|
1497
2086
|
writeToLogFile(`=== svg-matrix v${VERSION} ===`);
|
|
1498
|
-
} catch (e) {
|
|
2087
|
+
} catch (e) {
|
|
2088
|
+
logWarn(`Could not init log: ${e.message}`);
|
|
2089
|
+
}
|
|
1499
2090
|
}
|
|
1500
2091
|
|
|
1501
2092
|
switch (config.command) {
|
|
1502
|
-
case
|
|
1503
|
-
|
|
1504
|
-
|
|
2093
|
+
case "help":
|
|
2094
|
+
showHelp();
|
|
2095
|
+
break;
|
|
2096
|
+
case "version":
|
|
2097
|
+
showVersion();
|
|
2098
|
+
break;
|
|
2099
|
+
case "info": {
|
|
1505
2100
|
const files = gatherInputFiles();
|
|
1506
|
-
if (files.length === 0) {
|
|
2101
|
+
if (files.length === 0) {
|
|
2102
|
+
logError("No input files");
|
|
2103
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
2104
|
+
}
|
|
1507
2105
|
for (const f of files) processInfo(f);
|
|
1508
2106
|
break;
|
|
1509
2107
|
}
|
|
1510
|
-
case
|
|
2108
|
+
case "flatten":
|
|
2109
|
+
case "convert":
|
|
2110
|
+
case "normalize": {
|
|
1511
2111
|
const files = gatherInputFiles();
|
|
1512
|
-
if (files.length === 0) {
|
|
2112
|
+
if (files.length === 0) {
|
|
2113
|
+
logError("No input files");
|
|
2114
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
2115
|
+
}
|
|
1513
2116
|
logInfo(`Processing ${files.length} file(s)...`);
|
|
1514
|
-
if (config.dryRun) logInfo(
|
|
2117
|
+
if (config.dryRun) logInfo("(dry run)");
|
|
1515
2118
|
|
|
1516
|
-
let ok = 0,
|
|
2119
|
+
let ok = 0,
|
|
2120
|
+
fail = 0;
|
|
1517
2121
|
for (let i = 0; i < files.length; i++) {
|
|
1518
2122
|
const f = files[i];
|
|
1519
2123
|
currentInputFile = f; // Track for crash log
|
|
@@ -1532,8 +2136,12 @@ async function main() {
|
|
|
1532
2136
|
// Why: Track output file for cleanup on interrupt
|
|
1533
2137
|
currentOutputFile = out;
|
|
1534
2138
|
|
|
1535
|
-
const fn =
|
|
1536
|
-
|
|
2139
|
+
const fn =
|
|
2140
|
+
config.command === "flatten"
|
|
2141
|
+
? processFlatten
|
|
2142
|
+
: config.command === "convert"
|
|
2143
|
+
? processConvert
|
|
2144
|
+
: processNormalize;
|
|
1537
2145
|
|
|
1538
2146
|
if (fn(f, out)) {
|
|
1539
2147
|
// Verify write if not dry run
|
|
@@ -1542,9 +2150,9 @@ async function main() {
|
|
|
1542
2150
|
// 1. Full verification requires double memory (storing content before write)
|
|
1543
2151
|
// 2. Empty file is the most common silent failure mode
|
|
1544
2152
|
// 3. Full content already written, re-reading only to check existence/size
|
|
1545
|
-
const written = readFileSync(out,
|
|
2153
|
+
const written = readFileSync(out, "utf8");
|
|
1546
2154
|
if (written.length === 0) {
|
|
1547
|
-
throw new Error(
|
|
2155
|
+
throw new Error("Output file is empty after write");
|
|
1548
2156
|
}
|
|
1549
2157
|
}
|
|
1550
2158
|
// Why: Clear output file tracker after successful processing
|
|
@@ -1560,12 +2168,14 @@ async function main() {
|
|
|
1560
2168
|
}
|
|
1561
2169
|
currentInputFile = null;
|
|
1562
2170
|
currentOutputFile = null;
|
|
1563
|
-
logInfo(
|
|
2171
|
+
logInfo(
|
|
2172
|
+
`\n${colors.bright}Done:${colors.reset} ${ok} ok, ${fail} failed`,
|
|
2173
|
+
);
|
|
1564
2174
|
if (config.logFile) logInfo(`Log: ${config.logFile}`);
|
|
1565
2175
|
if (fail > 0) process.exit(CONSTANTS.EXIT_ERROR);
|
|
1566
2176
|
break;
|
|
1567
2177
|
}
|
|
1568
|
-
case
|
|
2178
|
+
case "test-toolbox": {
|
|
1569
2179
|
await processTestToolbox();
|
|
1570
2180
|
break;
|
|
1571
2181
|
}
|
|
@@ -1577,7 +2187,7 @@ async function main() {
|
|
|
1577
2187
|
} catch (error) {
|
|
1578
2188
|
generateCrashLog(error, {
|
|
1579
2189
|
currentFile: currentInputFile,
|
|
1580
|
-
args: process.argv.slice(2)
|
|
2190
|
+
args: process.argv.slice(2),
|
|
1581
2191
|
});
|
|
1582
2192
|
logError(`Fatal error: ${error.message}`);
|
|
1583
2193
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
@@ -1585,11 +2195,17 @@ async function main() {
|
|
|
1585
2195
|
}
|
|
1586
2196
|
|
|
1587
2197
|
// Why: Catch unhandled promise rejections which would otherwise cause silent failures
|
|
1588
|
-
process.on(
|
|
2198
|
+
process.on("unhandledRejection", (reason, _promise) => {
|
|
1589
2199
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
1590
|
-
generateCrashLog(error, {
|
|
2200
|
+
generateCrashLog(error, {
|
|
2201
|
+
type: "unhandledRejection",
|
|
2202
|
+
currentFile: currentInputFile,
|
|
2203
|
+
});
|
|
1591
2204
|
logError(`Unhandled rejection: ${error.message}`);
|
|
1592
2205
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1593
2206
|
});
|
|
1594
2207
|
|
|
1595
|
-
main().catch((e) => {
|
|
2208
|
+
main().catch((e) => {
|
|
2209
|
+
logError(`Error: ${e.message}`);
|
|
2210
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
2211
|
+
});
|