@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/svgm.js
CHANGED
|
@@ -13,14 +13,26 @@
|
|
|
13
13
|
* @license MIT
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
readFileSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
statSync,
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { join, dirname, basename, extname, resolve, isAbsolute } from "path";
|
|
25
|
+
import yaml from "js-yaml";
|
|
18
26
|
|
|
19
27
|
// Import library modules
|
|
20
|
-
import { VERSION } from
|
|
21
|
-
import * as SVGToolbox from
|
|
22
|
-
import { parseSVG, serializeSVG } from
|
|
23
|
-
import {
|
|
28
|
+
import { VERSION } from "../src/index.js";
|
|
29
|
+
import * as SVGToolbox from "../src/svg-toolbox.js";
|
|
30
|
+
import { parseSVG, serializeSVG } from "../src/svg-parser.js";
|
|
31
|
+
import {
|
|
32
|
+
injectPolyfills,
|
|
33
|
+
detectSVG2Features,
|
|
34
|
+
setPolyfillMinification,
|
|
35
|
+
} from "../src/svg2-polyfills.js";
|
|
24
36
|
|
|
25
37
|
// ============================================================================
|
|
26
38
|
// CONSTANTS
|
|
@@ -32,18 +44,32 @@ const CONSTANTS = {
|
|
|
32
44
|
MAX_FILE_SIZE_BYTES: 50 * 1024 * 1024,
|
|
33
45
|
EXIT_SUCCESS: 0,
|
|
34
46
|
EXIT_ERROR: 1,
|
|
35
|
-
SVG_EXTENSIONS: [
|
|
47
|
+
SVG_EXTENSIONS: [".svg", ".svgz"],
|
|
36
48
|
};
|
|
37
49
|
|
|
38
50
|
// ============================================================================
|
|
39
51
|
// COLORS (respects NO_COLOR env)
|
|
40
52
|
// ============================================================================
|
|
41
|
-
const colors =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const colors =
|
|
54
|
+
process.env.NO_COLOR !== undefined || process.argv.includes("--no-color")
|
|
55
|
+
? {
|
|
56
|
+
reset: "",
|
|
57
|
+
red: "",
|
|
58
|
+
yellow: "",
|
|
59
|
+
green: "",
|
|
60
|
+
cyan: "",
|
|
61
|
+
dim: "",
|
|
62
|
+
bright: "",
|
|
63
|
+
}
|
|
64
|
+
: {
|
|
65
|
+
reset: "\x1b[0m",
|
|
66
|
+
red: "\x1b[31m",
|
|
67
|
+
yellow: "\x1b[33m",
|
|
68
|
+
green: "\x1b[32m",
|
|
69
|
+
cyan: "\x1b[36m",
|
|
70
|
+
dim: "\x1b[2m",
|
|
71
|
+
bright: "\x1b[1m",
|
|
72
|
+
};
|
|
47
73
|
|
|
48
74
|
// ============================================================================
|
|
49
75
|
// CONFIGURATION
|
|
@@ -64,8 +90,24 @@ const DEFAULT_CONFIG = {
|
|
|
64
90
|
quiet: false,
|
|
65
91
|
datauri: null,
|
|
66
92
|
preserveNamespaces: [],
|
|
93
|
+
preserveVendor: false,
|
|
67
94
|
svg2Polyfills: false,
|
|
68
95
|
showPlugins: false,
|
|
96
|
+
configFile: null,
|
|
97
|
+
// Embed options
|
|
98
|
+
embed: false,
|
|
99
|
+
embedImages: false,
|
|
100
|
+
embedExternalSVGs: false,
|
|
101
|
+
embedExternalSVGMode: "extract",
|
|
102
|
+
embedCSS: false,
|
|
103
|
+
embedFonts: false,
|
|
104
|
+
embedScripts: false,
|
|
105
|
+
embedAudio: false,
|
|
106
|
+
embedSubsetFonts: false,
|
|
107
|
+
embedRecursive: false,
|
|
108
|
+
embedMaxRecursionDepth: 10,
|
|
109
|
+
embedTimeout: 30000,
|
|
110
|
+
embedOnMissingResource: "warn",
|
|
69
111
|
};
|
|
70
112
|
|
|
71
113
|
let config = { ...DEFAULT_CONFIG };
|
|
@@ -73,10 +115,20 @@ let config = { ...DEFAULT_CONFIG };
|
|
|
73
115
|
// ============================================================================
|
|
74
116
|
// LOGGING
|
|
75
117
|
// ============================================================================
|
|
118
|
+
/**
|
|
119
|
+
* Log message to console unless in quiet mode.
|
|
120
|
+
* @param {string} msg - Message to log
|
|
121
|
+
* @returns {void}
|
|
122
|
+
*/
|
|
76
123
|
function log(msg) {
|
|
77
124
|
if (!config.quiet) console.log(msg);
|
|
78
125
|
}
|
|
79
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Log error message to console.
|
|
129
|
+
* @param {string} msg - Error message
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
80
132
|
function logError(msg) {
|
|
81
133
|
console.error(`${colors.red}error:${colors.reset} ${msg}`);
|
|
82
134
|
}
|
|
@@ -86,44 +138,98 @@ function logError(msg) {
|
|
|
86
138
|
// ============================================================================
|
|
87
139
|
const OPTIMIZATIONS = [
|
|
88
140
|
// Cleanup plugins
|
|
89
|
-
{ name:
|
|
90
|
-
{ name:
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
{ name: "cleanupAttributes", description: "Remove useless attributes" },
|
|
142
|
+
{ name: "cleanupIds", description: "Remove unused and minify used IDs" },
|
|
143
|
+
{
|
|
144
|
+
name: "cleanupNumericValues",
|
|
145
|
+
description: "Round numeric values, remove default px units",
|
|
146
|
+
},
|
|
147
|
+
{ name: "cleanupListOfValues", description: "Round list of numeric values" },
|
|
148
|
+
{
|
|
149
|
+
name: "cleanupEnableBackground",
|
|
150
|
+
description: "Remove or fix enable-background attribute",
|
|
151
|
+
},
|
|
94
152
|
// Remove plugins
|
|
95
|
-
{ name:
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{ name:
|
|
101
|
-
{ name:
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ name:
|
|
107
|
-
{
|
|
108
|
-
|
|
153
|
+
{ name: "removeDoctype", description: "Remove DOCTYPE declaration" },
|
|
154
|
+
{
|
|
155
|
+
name: "removeXMLProcInst",
|
|
156
|
+
description: "Remove XML processing instructions",
|
|
157
|
+
},
|
|
158
|
+
{ name: "removeComments", description: "Remove comments" },
|
|
159
|
+
{ name: "removeMetadata", description: "Remove <metadata> elements" },
|
|
160
|
+
{
|
|
161
|
+
name: "removeTitle",
|
|
162
|
+
description: "Remove <title> elements (not in default)",
|
|
163
|
+
},
|
|
164
|
+
{ name: "removeDesc", description: "Remove <desc> elements" },
|
|
165
|
+
{
|
|
166
|
+
name: "removeEditorsNSData",
|
|
167
|
+
description: "Remove editor namespaces, elements, and attributes",
|
|
168
|
+
},
|
|
169
|
+
{ name: "removeEmptyAttrs", description: "Remove empty attributes" },
|
|
170
|
+
{
|
|
171
|
+
name: "removeEmptyContainers",
|
|
172
|
+
description: "Remove empty container elements",
|
|
173
|
+
},
|
|
174
|
+
{ name: "removeEmptyText", description: "Remove empty text elements" },
|
|
175
|
+
{ name: "removeHiddenElements", description: "Remove hidden elements" },
|
|
176
|
+
{ name: "removeUselessDefs", description: "Remove unused <defs> content" },
|
|
177
|
+
{
|
|
178
|
+
name: "removeUnknownsAndDefaults",
|
|
179
|
+
description: "Remove unknown elements and default attribute values",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "removeNonInheritableGroupAttrs",
|
|
183
|
+
description: "Remove non-inheritable presentation attributes from groups",
|
|
184
|
+
},
|
|
109
185
|
// Convert plugins
|
|
110
|
-
{ name:
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{
|
|
186
|
+
{ name: "convertShapesToPath", description: "Convert basic shapes to paths" },
|
|
187
|
+
{
|
|
188
|
+
name: "convertPathData",
|
|
189
|
+
description: "Optimize path data: convert, remove useless, etc.",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "convertTransform",
|
|
193
|
+
description: "Collapse multiple transforms into one, convert matrices",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "convertColors",
|
|
197
|
+
description: "Convert color values to shorter form",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "convertStyleToAttrs",
|
|
201
|
+
description: "Convert style to presentation attributes (not in default)",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "convertEllipseToCircle",
|
|
205
|
+
description: "Convert ellipse to circle when rx equals ry",
|
|
206
|
+
},
|
|
116
207
|
// Structure plugins
|
|
117
|
-
{ name:
|
|
118
|
-
{ name:
|
|
119
|
-
{
|
|
120
|
-
|
|
208
|
+
{ name: "collapseGroups", description: "Collapse useless groups" },
|
|
209
|
+
{ name: "mergePaths", description: "Merge multiple paths into one" },
|
|
210
|
+
{
|
|
211
|
+
name: "moveGroupAttrsToElems",
|
|
212
|
+
description: "Move group attributes to contained elements",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "moveElemsAttrsToGroup",
|
|
216
|
+
description: "Move common element attributes to parent group",
|
|
217
|
+
},
|
|
121
218
|
// Style plugins
|
|
122
|
-
{ name:
|
|
123
|
-
{
|
|
219
|
+
{ name: "minifyStyles", description: "Minify <style> elements content" },
|
|
220
|
+
{
|
|
221
|
+
name: "inlineStyles",
|
|
222
|
+
description: "Inline styles from <style> to element style attributes",
|
|
223
|
+
},
|
|
124
224
|
// Sort plugins
|
|
125
|
-
{
|
|
126
|
-
|
|
225
|
+
{
|
|
226
|
+
name: "sortAttrs",
|
|
227
|
+
description: "Sort attributes for better gzip compression",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "sortDefsChildren",
|
|
231
|
+
description: "Sort children of <defs> for better gzip compression",
|
|
232
|
+
},
|
|
127
233
|
];
|
|
128
234
|
|
|
129
235
|
// ============================================================================
|
|
@@ -132,79 +238,131 @@ const OPTIMIZATIONS = [
|
|
|
132
238
|
// ============================================================================
|
|
133
239
|
const DEFAULT_PIPELINE = [
|
|
134
240
|
// 1-6: Initial cleanup (matching SVGO preset-default order)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
241
|
+
"removeDoctype",
|
|
242
|
+
"removeXMLProcInst",
|
|
243
|
+
"removeComments",
|
|
138
244
|
// removeDeprecatedAttrs - not implemented (rarely needed)
|
|
139
|
-
|
|
140
|
-
|
|
245
|
+
"removeMetadata",
|
|
246
|
+
"removeEditorsNSData",
|
|
141
247
|
// 7-11: Style processing
|
|
142
|
-
|
|
248
|
+
"cleanupAttributes",
|
|
143
249
|
// mergeStyles - not implemented
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
250
|
+
"inlineStyles",
|
|
251
|
+
"minifyStyles",
|
|
252
|
+
"cleanupIds",
|
|
147
253
|
// 12-18: Remove unnecessary elements
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
254
|
+
"removeUselessDefs",
|
|
255
|
+
"cleanupNumericValues",
|
|
256
|
+
"convertColors",
|
|
257
|
+
"removeUnknownsAndDefaults",
|
|
258
|
+
"removeNonInheritableGroupAttrs",
|
|
153
259
|
// removeUselessStrokeAndFill - not implemented
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
260
|
+
"cleanupEnableBackground",
|
|
261
|
+
"removeHiddenElements",
|
|
262
|
+
"removeEmptyText",
|
|
157
263
|
// 19-27: Convert and optimize
|
|
158
264
|
// NOTE: convertShapesToPath removed - SVGO only converts when it saves bytes
|
|
159
265
|
// Our version converts all shapes which often increases size
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
266
|
+
"convertEllipseToCircle",
|
|
267
|
+
"moveElemsAttrsToGroup",
|
|
268
|
+
"moveGroupAttrsToElems",
|
|
269
|
+
"collapseGroups",
|
|
270
|
+
"convertPathData",
|
|
271
|
+
"convertTransform",
|
|
166
272
|
// 28-34: Final cleanup
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
273
|
+
"removeEmptyAttrs",
|
|
274
|
+
"removeEmptyContainers",
|
|
275
|
+
"mergePaths",
|
|
170
276
|
// removeUnusedNS - not implemented
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
277
|
+
"sortAttrs",
|
|
278
|
+
"sortDefsChildren",
|
|
279
|
+
"removeDesc",
|
|
174
280
|
];
|
|
175
281
|
|
|
176
282
|
// ============================================================================
|
|
177
283
|
// PATH UTILITIES
|
|
178
284
|
// ============================================================================
|
|
179
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Normalize path separators to forward slashes.
|
|
287
|
+
* @param {string} p - Path to normalize
|
|
288
|
+
* @returns {string} Normalized path
|
|
289
|
+
*/
|
|
290
|
+
function normalizePath(p) {
|
|
291
|
+
return p.replace(/\\/g, "/");
|
|
292
|
+
}
|
|
180
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Resolve path to absolute path with normalized separators.
|
|
296
|
+
* @param {string} p - Path to resolve
|
|
297
|
+
* @returns {string} Absolute normalized path
|
|
298
|
+
*/
|
|
181
299
|
function resolvePath(p) {
|
|
182
|
-
return isAbsolute(p)
|
|
300
|
+
return isAbsolute(p)
|
|
301
|
+
? normalizePath(p)
|
|
302
|
+
: normalizePath(resolve(process.cwd(), p));
|
|
183
303
|
}
|
|
184
304
|
|
|
185
|
-
|
|
186
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Check if path is a directory.
|
|
307
|
+
* @param {string} p - Path to check
|
|
308
|
+
* @returns {boolean} True if directory exists
|
|
309
|
+
*/
|
|
310
|
+
function isDir(p) {
|
|
311
|
+
try {
|
|
312
|
+
return statSync(p).isDirectory();
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
187
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Check if path is a file.
|
|
320
|
+
* @param {string} p - Path to check
|
|
321
|
+
* @returns {boolean} True if file exists
|
|
322
|
+
*/
|
|
323
|
+
function isFile(p) {
|
|
324
|
+
try {
|
|
325
|
+
return statSync(p).isFile();
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Ensure directory exists, creating it if necessary.
|
|
333
|
+
* @param {string} dir - Directory path
|
|
334
|
+
* @returns {void}
|
|
335
|
+
*/
|
|
188
336
|
function ensureDir(dir) {
|
|
189
337
|
if (!existsSync(dir)) {
|
|
190
338
|
mkdirSync(dir, { recursive: true });
|
|
191
339
|
}
|
|
192
340
|
}
|
|
193
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Get all SVG files in directory with optional exclusion patterns.
|
|
344
|
+
* @param {string} dir - Directory path
|
|
345
|
+
* @param {boolean} recursive - Whether to search recursively
|
|
346
|
+
* @param {string[]} exclude - Array of exclusion patterns
|
|
347
|
+
* @returns {string[]} Array of SVG file paths
|
|
348
|
+
*/
|
|
194
349
|
function getSvgFiles(dir, recursive = false, exclude = []) {
|
|
195
350
|
const files = [];
|
|
196
351
|
function scan(d) {
|
|
197
352
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
198
353
|
const fullPath = join(d, entry.name);
|
|
199
354
|
// Check exclusion patterns
|
|
200
|
-
const shouldExclude = exclude.some(pattern => {
|
|
355
|
+
const shouldExclude = exclude.some((pattern) => {
|
|
201
356
|
const regex = new RegExp(pattern);
|
|
202
357
|
return regex.test(fullPath) || regex.test(entry.name);
|
|
203
358
|
});
|
|
204
359
|
if (shouldExclude) continue;
|
|
205
360
|
|
|
206
361
|
if (entry.isDirectory() && recursive) scan(fullPath);
|
|
207
|
-
else if (
|
|
362
|
+
else if (
|
|
363
|
+
entry.isFile() &&
|
|
364
|
+
CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())
|
|
365
|
+
) {
|
|
208
366
|
files.push(normalizePath(fullPath));
|
|
209
367
|
}
|
|
210
368
|
}
|
|
@@ -216,17 +374,33 @@ function getSvgFiles(dir, recursive = false, exclude = []) {
|
|
|
216
374
|
// ============================================================================
|
|
217
375
|
// SVG OPTIMIZATION
|
|
218
376
|
// ============================================================================
|
|
377
|
+
/**
|
|
378
|
+
* Optimize SVG content using default pipeline.
|
|
379
|
+
* @param {string} content - SVG content
|
|
380
|
+
* @param {Object} options - Optimization options
|
|
381
|
+
* @returns {Promise<string>} Optimized SVG content
|
|
382
|
+
*/
|
|
219
383
|
async function optimizeSvg(content, options = {}) {
|
|
220
384
|
const doc = parseSVG(content);
|
|
221
385
|
const pipeline = DEFAULT_PIPELINE;
|
|
222
386
|
|
|
387
|
+
// Detect SVG 2 features BEFORE optimization pipeline removes them
|
|
388
|
+
// (removeUnknownsAndDefaults strips elements not in SVG 1.1 spec)
|
|
389
|
+
let svg2Features = null;
|
|
390
|
+
if (options.svg2Polyfills) {
|
|
391
|
+
svg2Features = detectSVG2Features(doc);
|
|
392
|
+
}
|
|
393
|
+
|
|
223
394
|
// Run optimization pipeline
|
|
224
395
|
for (const pluginName of pipeline) {
|
|
225
396
|
const fn = SVGToolbox[pluginName];
|
|
226
|
-
if (fn && typeof fn ===
|
|
397
|
+
if (fn && typeof fn === "function") {
|
|
227
398
|
try {
|
|
228
|
-
await fn(doc, {
|
|
229
|
-
|
|
399
|
+
await fn(doc, {
|
|
400
|
+
precision: options.precision,
|
|
401
|
+
preserveNamespaces: options.preserveNamespaces,
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
230
404
|
// Skip failed optimizations silently
|
|
231
405
|
}
|
|
232
406
|
}
|
|
@@ -236,21 +410,27 @@ async function optimizeSvg(content, options = {}) {
|
|
|
236
410
|
if (options.multipass) {
|
|
237
411
|
for (const pluginName of pipeline) {
|
|
238
412
|
const fn = SVGToolbox[pluginName];
|
|
239
|
-
if (fn && typeof fn ===
|
|
413
|
+
if (fn && typeof fn === "function") {
|
|
240
414
|
try {
|
|
241
|
-
await fn(doc, {
|
|
242
|
-
|
|
415
|
+
await fn(doc, {
|
|
416
|
+
precision: options.precision,
|
|
417
|
+
preserveNamespaces: options.preserveNamespaces,
|
|
418
|
+
});
|
|
419
|
+
} catch {
|
|
243
420
|
// Skip failed optimizations silently
|
|
244
421
|
}
|
|
245
422
|
}
|
|
246
423
|
}
|
|
247
424
|
}
|
|
248
425
|
|
|
249
|
-
// Inject SVG 2 polyfills if requested
|
|
250
|
-
if (options.svg2Polyfills) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
426
|
+
// Inject SVG 2 polyfills if requested (using pre-detected features)
|
|
427
|
+
if (options.svg2Polyfills && svg2Features) {
|
|
428
|
+
if (
|
|
429
|
+
svg2Features.meshGradients.length > 0 ||
|
|
430
|
+
svg2Features.hatches.length > 0
|
|
431
|
+
) {
|
|
432
|
+
// Pass pre-detected features since pipeline may have removed SVG2 elements
|
|
433
|
+
injectPolyfills(doc, { features: svg2Features });
|
|
254
434
|
}
|
|
255
435
|
}
|
|
256
436
|
|
|
@@ -264,78 +444,140 @@ async function optimizeSvg(content, options = {}) {
|
|
|
264
444
|
}
|
|
265
445
|
|
|
266
446
|
// Handle EOL
|
|
267
|
-
if (options.eol ===
|
|
268
|
-
result = result.replace(/\n/g,
|
|
447
|
+
if (options.eol === "crlf") {
|
|
448
|
+
result = result.replace(/\n/g, "\r\n");
|
|
269
449
|
}
|
|
270
450
|
|
|
271
451
|
// Final newline
|
|
272
|
-
if (options.finalNewline && !result.endsWith(
|
|
273
|
-
result +=
|
|
452
|
+
if (options.finalNewline && !result.endsWith("\n")) {
|
|
453
|
+
result += "\n";
|
|
274
454
|
}
|
|
275
455
|
|
|
276
456
|
return result;
|
|
277
457
|
}
|
|
278
458
|
|
|
279
459
|
/**
|
|
280
|
-
* Minify XML output while keeping it valid
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* -
|
|
460
|
+
* Minify XML output while keeping it valid.
|
|
461
|
+
* KEEPS XML declaration (ensures valid SVG), removes whitespace between tags,
|
|
462
|
+
* and collapses multiple spaces.
|
|
463
|
+
* @param {string} xml - XML content to minify
|
|
464
|
+
* @returns {string} Minified XML
|
|
284
465
|
*/
|
|
285
466
|
function minifyXml(xml) {
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
467
|
+
return (
|
|
468
|
+
xml
|
|
469
|
+
// Remove newlines and collapse whitespace between tags
|
|
470
|
+
.replace(/>\s+</g, "><")
|
|
471
|
+
// Remove leading/trailing whitespace
|
|
472
|
+
.trim()
|
|
473
|
+
);
|
|
291
474
|
}
|
|
292
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Prettify XML with indentation.
|
|
478
|
+
* @param {string} xml - XML content to prettify
|
|
479
|
+
* @param {number} indent - Number of spaces per indent level
|
|
480
|
+
* @returns {string} Prettified XML
|
|
481
|
+
*/
|
|
293
482
|
function prettifyXml(xml, indent = 2) {
|
|
294
483
|
// Simple XML prettifier
|
|
295
|
-
const indentStr =
|
|
296
|
-
let formatted =
|
|
484
|
+
const indentStr = " ".repeat(indent);
|
|
485
|
+
let formatted = "";
|
|
297
486
|
let depth = 0;
|
|
298
487
|
|
|
299
488
|
// Split on tags
|
|
300
|
-
xml
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
489
|
+
xml
|
|
490
|
+
.replace(/>\s*</g, ">\n<")
|
|
491
|
+
.split("\n")
|
|
492
|
+
.forEach((originalLine) => {
|
|
493
|
+
const line = originalLine.trim();
|
|
494
|
+
if (!line) return;
|
|
495
|
+
|
|
496
|
+
// Decrease depth for closing tags
|
|
497
|
+
if (line.startsWith("</")) {
|
|
498
|
+
depth = Math.max(0, depth - 1);
|
|
499
|
+
}
|
|
310
500
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
501
|
+
formatted += indentStr.repeat(depth) + line + "\n";
|
|
502
|
+
|
|
503
|
+
// Increase depth for opening tags (not self-closing)
|
|
504
|
+
if (
|
|
505
|
+
line.startsWith("<") &&
|
|
506
|
+
!line.startsWith("</") &&
|
|
507
|
+
!line.startsWith("<?") &&
|
|
508
|
+
!line.startsWith("<!") &&
|
|
509
|
+
!line.endsWith("/>") &&
|
|
510
|
+
!line.includes("</")
|
|
511
|
+
) {
|
|
512
|
+
depth++;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
317
515
|
|
|
318
516
|
return formatted.trim();
|
|
319
517
|
}
|
|
320
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Convert SVG content to data URI.
|
|
521
|
+
* @param {string} content - SVG content
|
|
522
|
+
* @param {string} format - Format: 'base64', 'enc', or 'unenc'
|
|
523
|
+
* @returns {string} Data URI string
|
|
524
|
+
*/
|
|
321
525
|
function toDataUri(content, format) {
|
|
322
|
-
if (format ===
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
|
|
526
|
+
if (format === "base64") {
|
|
527
|
+
return (
|
|
528
|
+
"data:image/svg+xml;base64," + Buffer.from(content).toString("base64")
|
|
529
|
+
);
|
|
530
|
+
} else if (format === "enc") {
|
|
531
|
+
return "data:image/svg+xml," + encodeURIComponent(content);
|
|
326
532
|
} else {
|
|
327
|
-
return
|
|
533
|
+
return "data:image/svg+xml," + content;
|
|
328
534
|
}
|
|
329
535
|
}
|
|
330
536
|
|
|
331
537
|
// ============================================================================
|
|
332
538
|
// PROCESS FILES
|
|
333
539
|
// ============================================================================
|
|
540
|
+
/**
|
|
541
|
+
* Process single SVG file with optimization.
|
|
542
|
+
* @param {string} inputPath - Input file path
|
|
543
|
+
* @param {string} outputPath - Output file path
|
|
544
|
+
* @param {Object} options - Processing options
|
|
545
|
+
* @returns {Promise<Object>} Processing result with success status and metrics
|
|
546
|
+
*/
|
|
334
547
|
async function processFile(inputPath, outputPath, options) {
|
|
335
548
|
try {
|
|
336
|
-
|
|
549
|
+
let content = readFileSync(inputPath, "utf8");
|
|
337
550
|
const originalSize = Buffer.byteLength(content);
|
|
338
551
|
|
|
552
|
+
// Apply embedding if enabled
|
|
553
|
+
if (options.embed) {
|
|
554
|
+
const doc = parseSVG(content);
|
|
555
|
+
const embedOptions = {
|
|
556
|
+
images: options.embedImages,
|
|
557
|
+
externalSVGs: options.embedExternalSVGs,
|
|
558
|
+
externalSVGMode: options.embedExternalSVGMode,
|
|
559
|
+
css: options.embedCSS,
|
|
560
|
+
fonts: options.embedFonts,
|
|
561
|
+
scripts: options.embedScripts,
|
|
562
|
+
audio: options.embedAudio,
|
|
563
|
+
subsetFonts: options.embedSubsetFonts,
|
|
564
|
+
recursive: options.embedRecursive,
|
|
565
|
+
maxRecursionDepth: options.embedMaxRecursionDepth,
|
|
566
|
+
timeout: options.embedTimeout,
|
|
567
|
+
onMissingResource: options.embedOnMissingResource,
|
|
568
|
+
baseDir: dirname(inputPath),
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
if (SVGToolbox.embedExternalDependencies) {
|
|
572
|
+
await SVGToolbox.embedExternalDependencies(doc, embedOptions);
|
|
573
|
+
content = serializeSVG(doc);
|
|
574
|
+
} else {
|
|
575
|
+
log(
|
|
576
|
+
`${colors.yellow}Warning:${colors.reset} embedExternalDependencies not available in svg-toolbox`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
339
581
|
const optimized = await optimizeSvg(content, options);
|
|
340
582
|
const optimizedSize = Buffer.byteLength(optimized);
|
|
341
583
|
|
|
@@ -344,17 +586,25 @@ async function processFile(inputPath, outputPath, options) {
|
|
|
344
586
|
output = toDataUri(optimized, options.datauri);
|
|
345
587
|
}
|
|
346
588
|
|
|
347
|
-
if (outputPath ===
|
|
589
|
+
if (outputPath === "-") {
|
|
348
590
|
process.stdout.write(output);
|
|
349
591
|
} else {
|
|
350
592
|
ensureDir(dirname(outputPath));
|
|
351
|
-
writeFileSync(outputPath, output,
|
|
593
|
+
writeFileSync(outputPath, output, "utf8");
|
|
352
594
|
}
|
|
353
595
|
|
|
354
596
|
const savings = originalSize - optimizedSize;
|
|
355
597
|
const percent = ((savings / originalSize) * 100).toFixed(1);
|
|
356
598
|
|
|
357
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
success: true,
|
|
601
|
+
originalSize,
|
|
602
|
+
optimizedSize,
|
|
603
|
+
savings,
|
|
604
|
+
percent,
|
|
605
|
+
inputPath,
|
|
606
|
+
outputPath,
|
|
607
|
+
};
|
|
358
608
|
} catch (error) {
|
|
359
609
|
return { success: false, error: error.message, inputPath };
|
|
360
610
|
}
|
|
@@ -363,6 +613,10 @@ async function processFile(inputPath, outputPath, options) {
|
|
|
363
613
|
// ============================================================================
|
|
364
614
|
// HELP
|
|
365
615
|
// ============================================================================
|
|
616
|
+
/**
|
|
617
|
+
* Display help message.
|
|
618
|
+
* @returns {void}
|
|
619
|
+
*/
|
|
366
620
|
function showHelp() {
|
|
367
621
|
console.log(`Usage: svgm [options] [INPUT...]
|
|
368
622
|
|
|
@@ -397,156 +651,363 @@ Options:
|
|
|
397
651
|
--show-plugins Show available plugins and exit
|
|
398
652
|
--preserve-ns <NS,...> Preserve vendor namespaces (inkscape, sodipodi,
|
|
399
653
|
illustrator, figma, etc.). Comma-separated.
|
|
654
|
+
--preserve-vendor Keep all vendor prefixes and editor namespaces
|
|
400
655
|
--svg2-polyfills Inject JavaScript polyfills for SVG 2 features
|
|
401
656
|
(mesh gradients, hatches) for browser support
|
|
657
|
+
--no-minify-polyfills Use full (non-minified) polyfills for debugging
|
|
402
658
|
--no-color Output plain text without color
|
|
403
659
|
-h, --help Display help for command
|
|
404
660
|
|
|
661
|
+
Embed Options:
|
|
662
|
+
--config <path> Load settings from YAML configuration file
|
|
663
|
+
--embed, --embed-all Enable all embedding options
|
|
664
|
+
--embed-images Embed external images as data URIs
|
|
665
|
+
--embed-external-svgs Embed external SVG files
|
|
666
|
+
--embed-svg-mode <mode> Mode for external SVGs: 'extract' or 'full'
|
|
667
|
+
--embed-css Embed external CSS files
|
|
668
|
+
--embed-fonts Embed external font files
|
|
669
|
+
--embed-scripts Embed external JavaScript files
|
|
670
|
+
--embed-audio Embed external audio files
|
|
671
|
+
--embed-subset-fonts Subset fonts to used glyphs only
|
|
672
|
+
--embed-recursive Recursively embed dependencies
|
|
673
|
+
--embed-max-depth <n> Maximum recursion depth (default: 10)
|
|
674
|
+
--embed-timeout <ms> Timeout for external resources (default: 30000)
|
|
675
|
+
--embed-on-missing <mode> Handle missing resources: 'warn', 'fail', 'skip'
|
|
676
|
+
|
|
405
677
|
Examples:
|
|
406
678
|
svgm input.svg -o output.svg
|
|
407
679
|
svgm -f ./icons/ -o ./optimized/
|
|
408
680
|
svgm input.svg --pretty --indent 4
|
|
409
681
|
svgm -p 2 --multipass input.svg
|
|
682
|
+
svgm input.svg --embed-all -o output.svg
|
|
683
|
+
svgm input.svg --config svgm.yml -o output.svg
|
|
410
684
|
|
|
411
685
|
Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
|
|
412
686
|
}
|
|
413
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Display version number.
|
|
690
|
+
* @returns {void}
|
|
691
|
+
*/
|
|
414
692
|
function showVersion() {
|
|
415
693
|
console.log(VERSION);
|
|
416
694
|
}
|
|
417
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Display available optimization plugins.
|
|
698
|
+
* @returns {void}
|
|
699
|
+
*/
|
|
418
700
|
function showPlugins() {
|
|
419
|
-
console.log(
|
|
701
|
+
console.log("\nAvailable optimizations:\n");
|
|
420
702
|
for (const opt of OPTIMIZATIONS) {
|
|
421
|
-
console.log(
|
|
703
|
+
console.log(
|
|
704
|
+
` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`,
|
|
705
|
+
);
|
|
422
706
|
}
|
|
423
707
|
console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
|
|
424
708
|
}
|
|
425
709
|
|
|
710
|
+
// ============================================================================
|
|
711
|
+
// CONFIG FILE LOADING
|
|
712
|
+
// ============================================================================
|
|
713
|
+
/**
|
|
714
|
+
* Load and parse configuration file (YAML).
|
|
715
|
+
* @param {string} configPath - Path to config file
|
|
716
|
+
* @returns {Object} Parsed configuration
|
|
717
|
+
*/
|
|
718
|
+
function loadConfigFile(configPath) {
|
|
719
|
+
try {
|
|
720
|
+
const absolutePath = resolvePath(configPath);
|
|
721
|
+
if (!existsSync(absolutePath)) {
|
|
722
|
+
logError(`Config file not found: ${configPath}`);
|
|
723
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
724
|
+
}
|
|
725
|
+
const content = readFileSync(absolutePath, "utf8");
|
|
726
|
+
const loadedConfig = yaml.load(content);
|
|
727
|
+
|
|
728
|
+
// Convert YAML config structure to CLI config structure
|
|
729
|
+
const result = {};
|
|
730
|
+
|
|
731
|
+
if (loadedConfig.embed) {
|
|
732
|
+
const embedCfg = loadedConfig.embed;
|
|
733
|
+
if (embedCfg.images !== undefined) result.embedImages = embedCfg.images;
|
|
734
|
+
if (embedCfg.externalSVGs !== undefined)
|
|
735
|
+
result.embedExternalSVGs = embedCfg.externalSVGs;
|
|
736
|
+
if (embedCfg.externalSVGMode !== undefined)
|
|
737
|
+
result.embedExternalSVGMode = embedCfg.externalSVGMode;
|
|
738
|
+
if (embedCfg.css !== undefined) result.embedCSS = embedCfg.css;
|
|
739
|
+
if (embedCfg.fonts !== undefined) result.embedFonts = embedCfg.fonts;
|
|
740
|
+
if (embedCfg.scripts !== undefined)
|
|
741
|
+
result.embedScripts = embedCfg.scripts;
|
|
742
|
+
if (embedCfg.audio !== undefined) result.embedAudio = embedCfg.audio;
|
|
743
|
+
if (embedCfg.subsetFonts !== undefined)
|
|
744
|
+
result.embedSubsetFonts = embedCfg.subsetFonts;
|
|
745
|
+
if (embedCfg.recursive !== undefined)
|
|
746
|
+
result.embedRecursive = embedCfg.recursive;
|
|
747
|
+
if (embedCfg.maxRecursionDepth !== undefined)
|
|
748
|
+
result.embedMaxRecursionDepth = embedCfg.maxRecursionDepth;
|
|
749
|
+
if (embedCfg.timeout !== undefined)
|
|
750
|
+
result.embedTimeout = embedCfg.timeout;
|
|
751
|
+
if (embedCfg.onMissingResource !== undefined)
|
|
752
|
+
result.embedOnMissingResource = embedCfg.onMissingResource;
|
|
753
|
+
|
|
754
|
+
// If any embed option is enabled, set embed flag
|
|
755
|
+
if (
|
|
756
|
+
Object.keys(result).some(
|
|
757
|
+
(key) => key.startsWith("embed") && result[key] === true,
|
|
758
|
+
)
|
|
759
|
+
) {
|
|
760
|
+
result.embed = true;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Support other config options if present
|
|
765
|
+
if (loadedConfig.precision !== undefined)
|
|
766
|
+
result.precision = loadedConfig.precision;
|
|
767
|
+
if (loadedConfig.multipass !== undefined)
|
|
768
|
+
result.multipass = loadedConfig.multipass;
|
|
769
|
+
if (loadedConfig.pretty !== undefined) result.pretty = loadedConfig.pretty;
|
|
770
|
+
if (loadedConfig.indent !== undefined) result.indent = loadedConfig.indent;
|
|
771
|
+
if (loadedConfig.quiet !== undefined) result.quiet = loadedConfig.quiet;
|
|
772
|
+
|
|
773
|
+
return result;
|
|
774
|
+
} catch (error) {
|
|
775
|
+
logError(`Failed to load config file: ${error.message}`);
|
|
776
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
426
780
|
// ============================================================================
|
|
427
781
|
// ARGUMENT PARSING
|
|
428
782
|
// ============================================================================
|
|
783
|
+
/**
|
|
784
|
+
* Parse command-line arguments.
|
|
785
|
+
* @param {string[]} args - Command-line arguments
|
|
786
|
+
* @returns {Object} Parsed configuration object
|
|
787
|
+
*/
|
|
429
788
|
function parseArgs(args) {
|
|
430
|
-
|
|
789
|
+
let cfg = { ...DEFAULT_CONFIG };
|
|
431
790
|
const inputs = [];
|
|
432
791
|
let i = 0;
|
|
433
792
|
|
|
434
793
|
while (i < args.length) {
|
|
435
|
-
|
|
794
|
+
let arg = args[i];
|
|
795
|
+
let argValue = null;
|
|
796
|
+
|
|
797
|
+
// Handle --arg=value format
|
|
798
|
+
if (arg.includes("=") && arg.startsWith("--")) {
|
|
799
|
+
const eqIdx = arg.indexOf("=");
|
|
800
|
+
argValue = arg.substring(eqIdx + 1);
|
|
801
|
+
arg = arg.substring(0, eqIdx);
|
|
802
|
+
}
|
|
436
803
|
|
|
437
804
|
switch (arg) {
|
|
438
|
-
case
|
|
439
|
-
case
|
|
805
|
+
case "-v":
|
|
806
|
+
case "--version":
|
|
440
807
|
showVersion();
|
|
441
808
|
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
442
809
|
break;
|
|
443
810
|
|
|
444
|
-
case
|
|
445
|
-
case
|
|
811
|
+
case "-h":
|
|
812
|
+
case "--help":
|
|
446
813
|
showHelp();
|
|
447
814
|
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
448
815
|
break;
|
|
449
816
|
|
|
450
|
-
case
|
|
451
|
-
case
|
|
817
|
+
case "-i":
|
|
818
|
+
case "--input":
|
|
452
819
|
i++;
|
|
453
|
-
while (i < args.length && !args[i].startsWith(
|
|
820
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
454
821
|
inputs.push(args[i]);
|
|
455
822
|
i++;
|
|
456
823
|
}
|
|
457
824
|
i--; // Back up one since the while loop went past
|
|
458
825
|
break;
|
|
459
826
|
|
|
460
|
-
case
|
|
461
|
-
case
|
|
827
|
+
case "-s":
|
|
828
|
+
case "--string":
|
|
462
829
|
cfg.string = args[++i];
|
|
463
830
|
break;
|
|
464
831
|
|
|
465
|
-
case
|
|
466
|
-
case
|
|
832
|
+
case "-f":
|
|
833
|
+
case "--folder":
|
|
467
834
|
cfg.folder = args[++i];
|
|
468
835
|
break;
|
|
469
836
|
|
|
470
|
-
case
|
|
471
|
-
case
|
|
837
|
+
case "-o":
|
|
838
|
+
case "--output": {
|
|
472
839
|
i++;
|
|
473
840
|
// Collect output(s)
|
|
474
841
|
const outputs = [];
|
|
475
|
-
while (i < args.length && !args[i].startsWith(
|
|
842
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
476
843
|
outputs.push(args[i]);
|
|
477
844
|
i++;
|
|
478
845
|
}
|
|
479
846
|
i--;
|
|
480
847
|
cfg.output = outputs.length === 1 ? outputs[0] : outputs;
|
|
481
848
|
break;
|
|
849
|
+
}
|
|
482
850
|
|
|
483
|
-
case
|
|
484
|
-
case
|
|
851
|
+
case "-p":
|
|
852
|
+
case "--precision":
|
|
485
853
|
cfg.precision = parseInt(args[++i], 10);
|
|
486
854
|
break;
|
|
487
855
|
|
|
488
|
-
case
|
|
856
|
+
case "--datauri":
|
|
489
857
|
cfg.datauri = args[++i];
|
|
490
858
|
break;
|
|
491
859
|
|
|
492
|
-
case
|
|
860
|
+
case "--multipass":
|
|
493
861
|
cfg.multipass = true;
|
|
494
862
|
break;
|
|
495
863
|
|
|
496
|
-
case
|
|
864
|
+
case "--pretty":
|
|
497
865
|
cfg.pretty = true;
|
|
498
866
|
break;
|
|
499
867
|
|
|
500
|
-
case
|
|
868
|
+
case "--indent":
|
|
501
869
|
cfg.indent = parseInt(args[++i], 10);
|
|
502
870
|
break;
|
|
503
871
|
|
|
504
|
-
case
|
|
872
|
+
case "--eol":
|
|
505
873
|
cfg.eol = args[++i];
|
|
506
874
|
break;
|
|
507
875
|
|
|
508
|
-
case
|
|
876
|
+
case "--final-newline":
|
|
509
877
|
cfg.finalNewline = true;
|
|
510
878
|
break;
|
|
511
879
|
|
|
512
|
-
case
|
|
513
|
-
case
|
|
880
|
+
case "-r":
|
|
881
|
+
case "--recursive":
|
|
514
882
|
cfg.recursive = true;
|
|
515
883
|
break;
|
|
516
884
|
|
|
517
|
-
case
|
|
885
|
+
case "--exclude":
|
|
518
886
|
i++;
|
|
519
|
-
while (i < args.length && !args[i].startsWith(
|
|
887
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
520
888
|
cfg.exclude.push(args[i]);
|
|
521
889
|
i++;
|
|
522
890
|
}
|
|
523
891
|
i--;
|
|
524
892
|
break;
|
|
525
893
|
|
|
526
|
-
case
|
|
527
|
-
case
|
|
894
|
+
case "-q":
|
|
895
|
+
case "--quiet":
|
|
528
896
|
cfg.quiet = true;
|
|
529
897
|
break;
|
|
530
898
|
|
|
531
|
-
case
|
|
899
|
+
case "--show-plugins":
|
|
532
900
|
cfg.showPlugins = true;
|
|
533
901
|
break;
|
|
534
902
|
|
|
535
|
-
case
|
|
536
|
-
|
|
537
|
-
|
|
903
|
+
case "--preserve-ns":
|
|
904
|
+
{
|
|
905
|
+
const val = argValue || args[++i];
|
|
906
|
+
if (!val) {
|
|
907
|
+
logError(
|
|
908
|
+
"--preserve-ns requires a comma-separated list of namespaces",
|
|
909
|
+
);
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
cfg.preserveNamespaces = val
|
|
913
|
+
.split(",")
|
|
914
|
+
.map((s) => s.trim().toLowerCase())
|
|
915
|
+
.filter((s) => s.length > 0);
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
|
|
919
|
+
case "--preserve-vendor":
|
|
920
|
+
cfg.preserveVendor = true;
|
|
538
921
|
break;
|
|
539
922
|
|
|
540
|
-
case
|
|
923
|
+
case "--svg2-polyfills":
|
|
541
924
|
cfg.svg2Polyfills = true;
|
|
542
925
|
break;
|
|
543
926
|
|
|
544
|
-
case
|
|
927
|
+
case "--no-minify-polyfills":
|
|
928
|
+
cfg.noMinifyPolyfills = true;
|
|
929
|
+
setPolyfillMinification(false);
|
|
930
|
+
break;
|
|
931
|
+
|
|
932
|
+
case "--no-color":
|
|
545
933
|
// Already handled in colors initialization
|
|
546
934
|
break;
|
|
547
935
|
|
|
936
|
+
case "--config":
|
|
937
|
+
cfg.configFile = args[++i];
|
|
938
|
+
break;
|
|
939
|
+
|
|
940
|
+
case "--embed":
|
|
941
|
+
case "--embed-all":
|
|
942
|
+
cfg.embed = true;
|
|
943
|
+
cfg.embedImages = true;
|
|
944
|
+
cfg.embedExternalSVGs = true;
|
|
945
|
+
cfg.embedCSS = true;
|
|
946
|
+
cfg.embedFonts = true;
|
|
947
|
+
cfg.embedScripts = true;
|
|
948
|
+
cfg.embedAudio = true;
|
|
949
|
+
cfg.embedSubsetFonts = true;
|
|
950
|
+
cfg.embedRecursive = true;
|
|
951
|
+
break;
|
|
952
|
+
|
|
953
|
+
case "--embed-images":
|
|
954
|
+
cfg.embed = true;
|
|
955
|
+
cfg.embedImages = true;
|
|
956
|
+
break;
|
|
957
|
+
|
|
958
|
+
case "--embed-external-svgs":
|
|
959
|
+
cfg.embed = true;
|
|
960
|
+
cfg.embedExternalSVGs = true;
|
|
961
|
+
break;
|
|
962
|
+
|
|
963
|
+
case "--embed-svg-mode":
|
|
964
|
+
cfg.embedExternalSVGMode = args[++i];
|
|
965
|
+
break;
|
|
966
|
+
|
|
967
|
+
case "--embed-css":
|
|
968
|
+
cfg.embed = true;
|
|
969
|
+
cfg.embedCSS = true;
|
|
970
|
+
break;
|
|
971
|
+
|
|
972
|
+
case "--embed-fonts":
|
|
973
|
+
cfg.embed = true;
|
|
974
|
+
cfg.embedFonts = true;
|
|
975
|
+
break;
|
|
976
|
+
|
|
977
|
+
case "--embed-scripts":
|
|
978
|
+
cfg.embed = true;
|
|
979
|
+
cfg.embedScripts = true;
|
|
980
|
+
break;
|
|
981
|
+
|
|
982
|
+
case "--embed-audio":
|
|
983
|
+
cfg.embed = true;
|
|
984
|
+
cfg.embedAudio = true;
|
|
985
|
+
break;
|
|
986
|
+
|
|
987
|
+
case "--embed-subset-fonts":
|
|
988
|
+
cfg.embed = true;
|
|
989
|
+
cfg.embedSubsetFonts = true;
|
|
990
|
+
break;
|
|
991
|
+
|
|
992
|
+
case "--embed-recursive":
|
|
993
|
+
cfg.embed = true;
|
|
994
|
+
cfg.embedRecursive = true;
|
|
995
|
+
break;
|
|
996
|
+
|
|
997
|
+
case "--embed-max-depth":
|
|
998
|
+
cfg.embedMaxRecursionDepth = parseInt(args[++i], 10);
|
|
999
|
+
break;
|
|
1000
|
+
|
|
1001
|
+
case "--embed-timeout":
|
|
1002
|
+
cfg.embedTimeout = parseInt(args[++i], 10);
|
|
1003
|
+
break;
|
|
1004
|
+
|
|
1005
|
+
case "--embed-on-missing":
|
|
1006
|
+
cfg.embedOnMissingResource = args[++i];
|
|
1007
|
+
break;
|
|
1008
|
+
|
|
548
1009
|
default:
|
|
549
|
-
if (arg.startsWith(
|
|
1010
|
+
if (arg.startsWith("-")) {
|
|
550
1011
|
logError(`Unknown option: ${arg}`);
|
|
551
1012
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
552
1013
|
}
|
|
@@ -556,12 +1017,87 @@ function parseArgs(args) {
|
|
|
556
1017
|
}
|
|
557
1018
|
|
|
558
1019
|
cfg.inputs = inputs;
|
|
1020
|
+
|
|
1021
|
+
// Validate numeric arguments
|
|
1022
|
+
if (
|
|
1023
|
+
cfg.precision !== undefined &&
|
|
1024
|
+
(isNaN(cfg.precision) || cfg.precision < 0 || cfg.precision > 20)
|
|
1025
|
+
) {
|
|
1026
|
+
logError("--precision must be a number between 0 and 20");
|
|
1027
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1028
|
+
}
|
|
1029
|
+
if (
|
|
1030
|
+
cfg.indent !== undefined &&
|
|
1031
|
+
(isNaN(cfg.indent) || cfg.indent < 0 || cfg.indent > 16)
|
|
1032
|
+
) {
|
|
1033
|
+
logError("--indent must be a number between 0 and 16");
|
|
1034
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1035
|
+
}
|
|
1036
|
+
if (
|
|
1037
|
+
cfg.embedMaxRecursionDepth !== undefined &&
|
|
1038
|
+
(isNaN(cfg.embedMaxRecursionDepth) ||
|
|
1039
|
+
cfg.embedMaxRecursionDepth < 1 ||
|
|
1040
|
+
cfg.embedMaxRecursionDepth > 100)
|
|
1041
|
+
) {
|
|
1042
|
+
logError("--embed-max-depth must be a number between 1 and 100");
|
|
1043
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1044
|
+
}
|
|
1045
|
+
if (
|
|
1046
|
+
cfg.embedTimeout !== undefined &&
|
|
1047
|
+
(isNaN(cfg.embedTimeout) ||
|
|
1048
|
+
cfg.embedTimeout < 1000 ||
|
|
1049
|
+
cfg.embedTimeout > 300000)
|
|
1050
|
+
) {
|
|
1051
|
+
logError("--embed-timeout must be a number between 1000 and 300000 (ms)");
|
|
1052
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Validate enum arguments (only check if explicitly set, not null/undefined defaults)
|
|
1056
|
+
const validEol = ["lf", "crlf"];
|
|
1057
|
+
if (cfg.eol != null && !validEol.includes(cfg.eol)) {
|
|
1058
|
+
logError(`--eol must be one of: ${validEol.join(", ")}`);
|
|
1059
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1060
|
+
}
|
|
1061
|
+
const validDatauri = ["base64", "enc", "unenc"];
|
|
1062
|
+
if (cfg.datauri != null && !validDatauri.includes(cfg.datauri)) {
|
|
1063
|
+
logError(`--datauri must be one of: ${validDatauri.join(", ")}`);
|
|
1064
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1065
|
+
}
|
|
1066
|
+
const validSvgMode = ["extract", "full"];
|
|
1067
|
+
if (
|
|
1068
|
+
cfg.embedExternalSVGMode != null &&
|
|
1069
|
+
cfg.embedExternalSVGMode !== "extract" &&
|
|
1070
|
+
!validSvgMode.includes(cfg.embedExternalSVGMode)
|
|
1071
|
+
) {
|
|
1072
|
+
logError(`--embed-svg-mode must be one of: ${validSvgMode.join(", ")}`);
|
|
1073
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1074
|
+
}
|
|
1075
|
+
const validOnMissing = ["warn", "fail", "skip"];
|
|
1076
|
+
if (
|
|
1077
|
+
cfg.embedOnMissingResource != null &&
|
|
1078
|
+
!validOnMissing.includes(cfg.embedOnMissingResource)
|
|
1079
|
+
) {
|
|
1080
|
+
logError(`--embed-on-missing must be one of: ${validOnMissing.join(", ")}`);
|
|
1081
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Load config file if specified and merge with CLI options (CLI takes precedence)
|
|
1085
|
+
if (cfg.configFile) {
|
|
1086
|
+
const fileConfig = loadConfigFile(cfg.configFile);
|
|
1087
|
+
// Merge: file config first, then CLI overrides
|
|
1088
|
+
cfg = { ...DEFAULT_CONFIG, ...fileConfig, ...cfg };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
559
1091
|
return cfg;
|
|
560
1092
|
}
|
|
561
1093
|
|
|
562
1094
|
// ============================================================================
|
|
563
1095
|
// MAIN
|
|
564
1096
|
// ============================================================================
|
|
1097
|
+
/**
|
|
1098
|
+
* Main entry point for CLI.
|
|
1099
|
+
* @returns {Promise<void>}
|
|
1100
|
+
*/
|
|
565
1101
|
async function main() {
|
|
566
1102
|
const args = process.argv.slice(2);
|
|
567
1103
|
|
|
@@ -587,15 +1123,32 @@ async function main() {
|
|
|
587
1123
|
datauri: config.datauri,
|
|
588
1124
|
preserveNamespaces: config.preserveNamespaces,
|
|
589
1125
|
svg2Polyfills: config.svg2Polyfills,
|
|
1126
|
+
noMinifyPolyfills: config.noMinifyPolyfills, // Pass through for pipeline consistency
|
|
1127
|
+
// Embed options
|
|
1128
|
+
embed: config.embed,
|
|
1129
|
+
embedImages: config.embedImages,
|
|
1130
|
+
embedExternalSVGs: config.embedExternalSVGs,
|
|
1131
|
+
embedExternalSVGMode: config.embedExternalSVGMode,
|
|
1132
|
+
embedCSS: config.embedCSS,
|
|
1133
|
+
embedFonts: config.embedFonts,
|
|
1134
|
+
embedScripts: config.embedScripts,
|
|
1135
|
+
embedAudio: config.embedAudio,
|
|
1136
|
+
embedSubsetFonts: config.embedSubsetFonts,
|
|
1137
|
+
embedRecursive: config.embedRecursive,
|
|
1138
|
+
embedMaxRecursionDepth: config.embedMaxRecursionDepth,
|
|
1139
|
+
embedTimeout: config.embedTimeout,
|
|
1140
|
+
embedOnMissingResource: config.embedOnMissingResource,
|
|
590
1141
|
};
|
|
591
1142
|
|
|
592
1143
|
// Handle string input
|
|
593
1144
|
if (config.string) {
|
|
594
1145
|
try {
|
|
595
1146
|
const result = await optimizeSvg(config.string, options);
|
|
596
|
-
const output = config.datauri
|
|
597
|
-
|
|
598
|
-
|
|
1147
|
+
const output = config.datauri
|
|
1148
|
+
? toDataUri(result, config.datauri)
|
|
1149
|
+
: result;
|
|
1150
|
+
if (config.output && config.output !== "-") {
|
|
1151
|
+
writeFileSync(config.output, output, "utf8");
|
|
599
1152
|
log(`${colors.green}Done!${colors.reset}`);
|
|
600
1153
|
} else {
|
|
601
1154
|
process.stdout.write(output);
|
|
@@ -621,9 +1174,9 @@ async function main() {
|
|
|
621
1174
|
|
|
622
1175
|
// Add explicit inputs
|
|
623
1176
|
for (const input of config.inputs) {
|
|
624
|
-
if (input ===
|
|
1177
|
+
if (input === "-") {
|
|
625
1178
|
// STDIN handling would go here
|
|
626
|
-
logError(
|
|
1179
|
+
logError("STDIN not yet supported");
|
|
627
1180
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
628
1181
|
}
|
|
629
1182
|
const resolved = resolvePath(input);
|
|
@@ -638,7 +1191,7 @@ async function main() {
|
|
|
638
1191
|
}
|
|
639
1192
|
|
|
640
1193
|
if (files.length === 0) {
|
|
641
|
-
logError(
|
|
1194
|
+
logError("No input files");
|
|
642
1195
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
643
1196
|
}
|
|
644
1197
|
|
|
@@ -653,8 +1206,8 @@ async function main() {
|
|
|
653
1206
|
let outputPath;
|
|
654
1207
|
|
|
655
1208
|
if (config.output) {
|
|
656
|
-
if (config.output ===
|
|
657
|
-
outputPath =
|
|
1209
|
+
if (config.output === "-") {
|
|
1210
|
+
outputPath = "-";
|
|
658
1211
|
} else if (Array.isArray(config.output)) {
|
|
659
1212
|
outputPath = config.output[i] || config.output[0];
|
|
660
1213
|
} else if (files.length > 1 || isDir(resolvePath(config.output))) {
|
|
@@ -675,8 +1228,10 @@ async function main() {
|
|
|
675
1228
|
totalOriginal += result.originalSize;
|
|
676
1229
|
totalOptimized += result.optimizedSize;
|
|
677
1230
|
|
|
678
|
-
if (outputPath !==
|
|
679
|
-
log(
|
|
1231
|
+
if (outputPath !== "-") {
|
|
1232
|
+
log(
|
|
1233
|
+
`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`,
|
|
1234
|
+
);
|
|
680
1235
|
}
|
|
681
1236
|
} else {
|
|
682
1237
|
errorCount++;
|
|
@@ -687,9 +1242,14 @@ async function main() {
|
|
|
687
1242
|
// Summary
|
|
688
1243
|
if (files.length > 1 && !config.quiet) {
|
|
689
1244
|
const totalSavings = totalOriginal - totalOptimized;
|
|
690
|
-
const totalPercent =
|
|
691
|
-
|
|
692
|
-
console.log(
|
|
1245
|
+
const totalPercent =
|
|
1246
|
+
totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
|
|
1247
|
+
console.log(
|
|
1248
|
+
`\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`,
|
|
1249
|
+
);
|
|
1250
|
+
console.log(
|
|
1251
|
+
`${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`,
|
|
1252
|
+
);
|
|
693
1253
|
}
|
|
694
1254
|
|
|
695
1255
|
if (errorCount > 0) {
|