@emasoft/svg-matrix 1.0.28 → 1.0.30
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 +985 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +723 -180
- 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 +18 -7
- 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 +22 -18
- 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 +16381 -3370
- package/src/svg2-polyfills.js +93 -224
- 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));
|
|
303
|
+
}
|
|
304
|
+
|
|
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
|
+
}
|
|
183
316
|
}
|
|
184
317
|
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
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,6 +374,12 @@ 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;
|
|
@@ -230,10 +394,13 @@ async function optimizeSvg(content, options = {}) {
|
|
|
230
394
|
// Run optimization pipeline
|
|
231
395
|
for (const pluginName of pipeline) {
|
|
232
396
|
const fn = SVGToolbox[pluginName];
|
|
233
|
-
if (fn && typeof fn ===
|
|
397
|
+
if (fn && typeof fn === "function") {
|
|
234
398
|
try {
|
|
235
|
-
await fn(doc, {
|
|
236
|
-
|
|
399
|
+
await fn(doc, {
|
|
400
|
+
precision: options.precision,
|
|
401
|
+
preserveNamespaces: options.preserveNamespaces,
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
237
404
|
// Skip failed optimizations silently
|
|
238
405
|
}
|
|
239
406
|
}
|
|
@@ -243,10 +410,13 @@ async function optimizeSvg(content, options = {}) {
|
|
|
243
410
|
if (options.multipass) {
|
|
244
411
|
for (const pluginName of pipeline) {
|
|
245
412
|
const fn = SVGToolbox[pluginName];
|
|
246
|
-
if (fn && typeof fn ===
|
|
413
|
+
if (fn && typeof fn === "function") {
|
|
247
414
|
try {
|
|
248
|
-
await fn(doc, {
|
|
249
|
-
|
|
415
|
+
await fn(doc, {
|
|
416
|
+
precision: options.precision,
|
|
417
|
+
preserveNamespaces: options.preserveNamespaces,
|
|
418
|
+
});
|
|
419
|
+
} catch {
|
|
250
420
|
// Skip failed optimizations silently
|
|
251
421
|
}
|
|
252
422
|
}
|
|
@@ -255,7 +425,10 @@ async function optimizeSvg(content, options = {}) {
|
|
|
255
425
|
|
|
256
426
|
// Inject SVG 2 polyfills if requested (using pre-detected features)
|
|
257
427
|
if (options.svg2Polyfills && svg2Features) {
|
|
258
|
-
if (
|
|
428
|
+
if (
|
|
429
|
+
svg2Features.meshGradients.length > 0 ||
|
|
430
|
+
svg2Features.hatches.length > 0
|
|
431
|
+
) {
|
|
259
432
|
// Pass pre-detected features since pipeline may have removed SVG2 elements
|
|
260
433
|
injectPolyfills(doc, { features: svg2Features });
|
|
261
434
|
}
|
|
@@ -271,78 +444,140 @@ async function optimizeSvg(content, options = {}) {
|
|
|
271
444
|
}
|
|
272
445
|
|
|
273
446
|
// Handle EOL
|
|
274
|
-
if (options.eol ===
|
|
275
|
-
result = result.replace(/\n/g,
|
|
447
|
+
if (options.eol === "crlf") {
|
|
448
|
+
result = result.replace(/\n/g, "\r\n");
|
|
276
449
|
}
|
|
277
450
|
|
|
278
451
|
// Final newline
|
|
279
|
-
if (options.finalNewline && !result.endsWith(
|
|
280
|
-
result +=
|
|
452
|
+
if (options.finalNewline && !result.endsWith("\n")) {
|
|
453
|
+
result += "\n";
|
|
281
454
|
}
|
|
282
455
|
|
|
283
456
|
return result;
|
|
284
457
|
}
|
|
285
458
|
|
|
286
459
|
/**
|
|
287
|
-
* Minify XML output while keeping it valid
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* -
|
|
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
|
|
291
465
|
*/
|
|
292
466
|
function minifyXml(xml) {
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
467
|
+
return (
|
|
468
|
+
xml
|
|
469
|
+
// Remove newlines and collapse whitespace between tags
|
|
470
|
+
.replace(/>\s+</g, "><")
|
|
471
|
+
// Remove leading/trailing whitespace
|
|
472
|
+
.trim()
|
|
473
|
+
);
|
|
298
474
|
}
|
|
299
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
|
+
*/
|
|
300
482
|
function prettifyXml(xml, indent = 2) {
|
|
301
483
|
// Simple XML prettifier
|
|
302
|
-
const indentStr =
|
|
303
|
-
let formatted =
|
|
484
|
+
const indentStr = " ".repeat(indent);
|
|
485
|
+
let formatted = "";
|
|
304
486
|
let depth = 0;
|
|
305
487
|
|
|
306
488
|
// Split on tags
|
|
307
|
-
xml
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
}
|
|
317
500
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
});
|
|
324
515
|
|
|
325
516
|
return formatted.trim();
|
|
326
517
|
}
|
|
327
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
|
+
*/
|
|
328
525
|
function toDataUri(content, format) {
|
|
329
|
-
if (format ===
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
|
|
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);
|
|
333
532
|
} else {
|
|
334
|
-
return
|
|
533
|
+
return "data:image/svg+xml," + content;
|
|
335
534
|
}
|
|
336
535
|
}
|
|
337
536
|
|
|
338
537
|
// ============================================================================
|
|
339
538
|
// PROCESS FILES
|
|
340
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
|
+
*/
|
|
341
547
|
async function processFile(inputPath, outputPath, options) {
|
|
342
548
|
try {
|
|
343
|
-
|
|
549
|
+
let content = readFileSync(inputPath, "utf8");
|
|
344
550
|
const originalSize = Buffer.byteLength(content);
|
|
345
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
|
+
|
|
346
581
|
const optimized = await optimizeSvg(content, options);
|
|
347
582
|
const optimizedSize = Buffer.byteLength(optimized);
|
|
348
583
|
|
|
@@ -351,17 +586,25 @@ async function processFile(inputPath, outputPath, options) {
|
|
|
351
586
|
output = toDataUri(optimized, options.datauri);
|
|
352
587
|
}
|
|
353
588
|
|
|
354
|
-
if (outputPath ===
|
|
589
|
+
if (outputPath === "-") {
|
|
355
590
|
process.stdout.write(output);
|
|
356
591
|
} else {
|
|
357
592
|
ensureDir(dirname(outputPath));
|
|
358
|
-
writeFileSync(outputPath, output,
|
|
593
|
+
writeFileSync(outputPath, output, "utf8");
|
|
359
594
|
}
|
|
360
595
|
|
|
361
596
|
const savings = originalSize - optimizedSize;
|
|
362
597
|
const percent = ((savings / originalSize) * 100).toFixed(1);
|
|
363
598
|
|
|
364
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
success: true,
|
|
601
|
+
originalSize,
|
|
602
|
+
optimizedSize,
|
|
603
|
+
savings,
|
|
604
|
+
percent,
|
|
605
|
+
inputPath,
|
|
606
|
+
outputPath,
|
|
607
|
+
};
|
|
365
608
|
} catch (error) {
|
|
366
609
|
return { success: false, error: error.message, inputPath };
|
|
367
610
|
}
|
|
@@ -370,6 +613,10 @@ async function processFile(inputPath, outputPath, options) {
|
|
|
370
613
|
// ============================================================================
|
|
371
614
|
// HELP
|
|
372
615
|
// ============================================================================
|
|
616
|
+
/**
|
|
617
|
+
* Display help message.
|
|
618
|
+
* @returns {void}
|
|
619
|
+
*/
|
|
373
620
|
function showHelp() {
|
|
374
621
|
console.log(`Usage: svgm [options] [INPUT...]
|
|
375
622
|
|
|
@@ -404,37 +651,142 @@ Options:
|
|
|
404
651
|
--show-plugins Show available plugins and exit
|
|
405
652
|
--preserve-ns <NS,...> Preserve vendor namespaces (inkscape, sodipodi,
|
|
406
653
|
illustrator, figma, etc.). Comma-separated.
|
|
654
|
+
--preserve-vendor Keep all vendor prefixes and editor namespaces
|
|
407
655
|
--svg2-polyfills Inject JavaScript polyfills for SVG 2 features
|
|
408
656
|
(mesh gradients, hatches) for browser support
|
|
657
|
+
--no-minify-polyfills Use full (non-minified) polyfills for debugging
|
|
409
658
|
--no-color Output plain text without color
|
|
410
659
|
-h, --help Display help for command
|
|
411
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
|
+
|
|
412
677
|
Examples:
|
|
413
678
|
svgm input.svg -o output.svg
|
|
414
679
|
svgm -f ./icons/ -o ./optimized/
|
|
415
680
|
svgm input.svg --pretty --indent 4
|
|
416
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
|
|
417
684
|
|
|
418
685
|
Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
|
|
419
686
|
}
|
|
420
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Display version number.
|
|
690
|
+
* @returns {void}
|
|
691
|
+
*/
|
|
421
692
|
function showVersion() {
|
|
422
693
|
console.log(VERSION);
|
|
423
694
|
}
|
|
424
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Display available optimization plugins.
|
|
698
|
+
* @returns {void}
|
|
699
|
+
*/
|
|
425
700
|
function showPlugins() {
|
|
426
|
-
console.log(
|
|
701
|
+
console.log("\nAvailable optimizations:\n");
|
|
427
702
|
for (const opt of OPTIMIZATIONS) {
|
|
428
|
-
console.log(
|
|
703
|
+
console.log(
|
|
704
|
+
` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`,
|
|
705
|
+
);
|
|
429
706
|
}
|
|
430
707
|
console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
|
|
431
708
|
}
|
|
432
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
|
+
|
|
433
780
|
// ============================================================================
|
|
434
781
|
// ARGUMENT PARSING
|
|
435
782
|
// ============================================================================
|
|
783
|
+
/**
|
|
784
|
+
* Parse command-line arguments.
|
|
785
|
+
* @param {string[]} args - Command-line arguments
|
|
786
|
+
* @returns {Object} Parsed configuration object
|
|
787
|
+
*/
|
|
436
788
|
function parseArgs(args) {
|
|
437
|
-
|
|
789
|
+
let cfg = { ...DEFAULT_CONFIG };
|
|
438
790
|
const inputs = [];
|
|
439
791
|
let i = 0;
|
|
440
792
|
|
|
@@ -443,127 +795,219 @@ function parseArgs(args) {
|
|
|
443
795
|
let argValue = null;
|
|
444
796
|
|
|
445
797
|
// Handle --arg=value format
|
|
446
|
-
if (arg.includes(
|
|
447
|
-
const eqIdx = arg.indexOf(
|
|
798
|
+
if (arg.includes("=") && arg.startsWith("--")) {
|
|
799
|
+
const eqIdx = arg.indexOf("=");
|
|
448
800
|
argValue = arg.substring(eqIdx + 1);
|
|
449
801
|
arg = arg.substring(0, eqIdx);
|
|
450
802
|
}
|
|
451
803
|
|
|
452
804
|
switch (arg) {
|
|
453
|
-
case
|
|
454
|
-
case
|
|
805
|
+
case "-v":
|
|
806
|
+
case "--version":
|
|
455
807
|
showVersion();
|
|
456
808
|
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
457
809
|
break;
|
|
458
810
|
|
|
459
|
-
case
|
|
460
|
-
case
|
|
811
|
+
case "-h":
|
|
812
|
+
case "--help":
|
|
461
813
|
showHelp();
|
|
462
814
|
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
463
815
|
break;
|
|
464
816
|
|
|
465
|
-
case
|
|
466
|
-
case
|
|
817
|
+
case "-i":
|
|
818
|
+
case "--input":
|
|
467
819
|
i++;
|
|
468
|
-
while (i < args.length && !args[i].startsWith(
|
|
820
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
469
821
|
inputs.push(args[i]);
|
|
470
822
|
i++;
|
|
471
823
|
}
|
|
472
824
|
i--; // Back up one since the while loop went past
|
|
473
825
|
break;
|
|
474
826
|
|
|
475
|
-
case
|
|
476
|
-
case
|
|
827
|
+
case "-s":
|
|
828
|
+
case "--string":
|
|
477
829
|
cfg.string = args[++i];
|
|
478
830
|
break;
|
|
479
831
|
|
|
480
|
-
case
|
|
481
|
-
case
|
|
832
|
+
case "-f":
|
|
833
|
+
case "--folder":
|
|
482
834
|
cfg.folder = args[++i];
|
|
483
835
|
break;
|
|
484
836
|
|
|
485
|
-
case
|
|
486
|
-
case
|
|
837
|
+
case "-o":
|
|
838
|
+
case "--output": {
|
|
487
839
|
i++;
|
|
488
840
|
// Collect output(s)
|
|
489
841
|
const outputs = [];
|
|
490
|
-
while (i < args.length && !args[i].startsWith(
|
|
842
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
491
843
|
outputs.push(args[i]);
|
|
492
844
|
i++;
|
|
493
845
|
}
|
|
494
846
|
i--;
|
|
495
847
|
cfg.output = outputs.length === 1 ? outputs[0] : outputs;
|
|
496
848
|
break;
|
|
849
|
+
}
|
|
497
850
|
|
|
498
|
-
case
|
|
499
|
-
case
|
|
851
|
+
case "-p":
|
|
852
|
+
case "--precision":
|
|
500
853
|
cfg.precision = parseInt(args[++i], 10);
|
|
501
854
|
break;
|
|
502
855
|
|
|
503
|
-
case
|
|
856
|
+
case "--datauri":
|
|
504
857
|
cfg.datauri = args[++i];
|
|
505
858
|
break;
|
|
506
859
|
|
|
507
|
-
case
|
|
860
|
+
case "--multipass":
|
|
508
861
|
cfg.multipass = true;
|
|
509
862
|
break;
|
|
510
863
|
|
|
511
|
-
case
|
|
864
|
+
case "--pretty":
|
|
512
865
|
cfg.pretty = true;
|
|
513
866
|
break;
|
|
514
867
|
|
|
515
|
-
case
|
|
868
|
+
case "--indent":
|
|
516
869
|
cfg.indent = parseInt(args[++i], 10);
|
|
517
870
|
break;
|
|
518
871
|
|
|
519
|
-
case
|
|
872
|
+
case "--eol":
|
|
520
873
|
cfg.eol = args[++i];
|
|
521
874
|
break;
|
|
522
875
|
|
|
523
|
-
case
|
|
876
|
+
case "--final-newline":
|
|
524
877
|
cfg.finalNewline = true;
|
|
525
878
|
break;
|
|
526
879
|
|
|
527
|
-
case
|
|
528
|
-
case
|
|
880
|
+
case "-r":
|
|
881
|
+
case "--recursive":
|
|
529
882
|
cfg.recursive = true;
|
|
530
883
|
break;
|
|
531
884
|
|
|
532
|
-
case
|
|
885
|
+
case "--exclude":
|
|
533
886
|
i++;
|
|
534
|
-
while (i < args.length && !args[i].startsWith(
|
|
887
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
535
888
|
cfg.exclude.push(args[i]);
|
|
536
889
|
i++;
|
|
537
890
|
}
|
|
538
891
|
i--;
|
|
539
892
|
break;
|
|
540
893
|
|
|
541
|
-
case
|
|
542
|
-
case
|
|
894
|
+
case "-q":
|
|
895
|
+
case "--quiet":
|
|
543
896
|
cfg.quiet = true;
|
|
544
897
|
break;
|
|
545
898
|
|
|
546
|
-
case
|
|
899
|
+
case "--show-plugins":
|
|
547
900
|
cfg.showPlugins = true;
|
|
548
901
|
break;
|
|
549
902
|
|
|
550
|
-
case
|
|
903
|
+
case "--preserve-ns":
|
|
551
904
|
{
|
|
552
905
|
const val = argValue || args[++i];
|
|
553
|
-
|
|
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);
|
|
554
916
|
}
|
|
555
917
|
break;
|
|
556
918
|
|
|
557
|
-
case
|
|
919
|
+
case "--preserve-vendor":
|
|
920
|
+
cfg.preserveVendor = true;
|
|
921
|
+
break;
|
|
922
|
+
|
|
923
|
+
case "--svg2-polyfills":
|
|
558
924
|
cfg.svg2Polyfills = true;
|
|
559
925
|
break;
|
|
560
926
|
|
|
561
|
-
case
|
|
927
|
+
case "--no-minify-polyfills":
|
|
928
|
+
cfg.noMinifyPolyfills = true;
|
|
929
|
+
setPolyfillMinification(false);
|
|
930
|
+
break;
|
|
931
|
+
|
|
932
|
+
case "--no-color":
|
|
562
933
|
// Already handled in colors initialization
|
|
563
934
|
break;
|
|
564
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
|
+
|
|
565
1009
|
default:
|
|
566
|
-
if (arg.startsWith(
|
|
1010
|
+
if (arg.startsWith("-")) {
|
|
567
1011
|
logError(`Unknown option: ${arg}`);
|
|
568
1012
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
569
1013
|
}
|
|
@@ -573,12 +1017,87 @@ function parseArgs(args) {
|
|
|
573
1017
|
}
|
|
574
1018
|
|
|
575
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
|
+
|
|
576
1091
|
return cfg;
|
|
577
1092
|
}
|
|
578
1093
|
|
|
579
1094
|
// ============================================================================
|
|
580
1095
|
// MAIN
|
|
581
1096
|
// ============================================================================
|
|
1097
|
+
/**
|
|
1098
|
+
* Main entry point for CLI.
|
|
1099
|
+
* @returns {Promise<void>}
|
|
1100
|
+
*/
|
|
582
1101
|
async function main() {
|
|
583
1102
|
const args = process.argv.slice(2);
|
|
584
1103
|
|
|
@@ -604,15 +1123,32 @@ async function main() {
|
|
|
604
1123
|
datauri: config.datauri,
|
|
605
1124
|
preserveNamespaces: config.preserveNamespaces,
|
|
606
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,
|
|
607
1141
|
};
|
|
608
1142
|
|
|
609
1143
|
// Handle string input
|
|
610
1144
|
if (config.string) {
|
|
611
1145
|
try {
|
|
612
1146
|
const result = await optimizeSvg(config.string, options);
|
|
613
|
-
const output = config.datauri
|
|
614
|
-
|
|
615
|
-
|
|
1147
|
+
const output = config.datauri
|
|
1148
|
+
? toDataUri(result, config.datauri)
|
|
1149
|
+
: result;
|
|
1150
|
+
if (config.output && config.output !== "-") {
|
|
1151
|
+
writeFileSync(config.output, output, "utf8");
|
|
616
1152
|
log(`${colors.green}Done!${colors.reset}`);
|
|
617
1153
|
} else {
|
|
618
1154
|
process.stdout.write(output);
|
|
@@ -638,9 +1174,9 @@ async function main() {
|
|
|
638
1174
|
|
|
639
1175
|
// Add explicit inputs
|
|
640
1176
|
for (const input of config.inputs) {
|
|
641
|
-
if (input ===
|
|
1177
|
+
if (input === "-") {
|
|
642
1178
|
// STDIN handling would go here
|
|
643
|
-
logError(
|
|
1179
|
+
logError("STDIN not yet supported");
|
|
644
1180
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
645
1181
|
}
|
|
646
1182
|
const resolved = resolvePath(input);
|
|
@@ -655,7 +1191,7 @@ async function main() {
|
|
|
655
1191
|
}
|
|
656
1192
|
|
|
657
1193
|
if (files.length === 0) {
|
|
658
|
-
logError(
|
|
1194
|
+
logError("No input files");
|
|
659
1195
|
process.exit(CONSTANTS.EXIT_ERROR);
|
|
660
1196
|
}
|
|
661
1197
|
|
|
@@ -670,8 +1206,8 @@ async function main() {
|
|
|
670
1206
|
let outputPath;
|
|
671
1207
|
|
|
672
1208
|
if (config.output) {
|
|
673
|
-
if (config.output ===
|
|
674
|
-
outputPath =
|
|
1209
|
+
if (config.output === "-") {
|
|
1210
|
+
outputPath = "-";
|
|
675
1211
|
} else if (Array.isArray(config.output)) {
|
|
676
1212
|
outputPath = config.output[i] || config.output[0];
|
|
677
1213
|
} else if (files.length > 1 || isDir(resolvePath(config.output))) {
|
|
@@ -692,8 +1228,10 @@ async function main() {
|
|
|
692
1228
|
totalOriginal += result.originalSize;
|
|
693
1229
|
totalOptimized += result.optimizedSize;
|
|
694
1230
|
|
|
695
|
-
if (outputPath !==
|
|
696
|
-
log(
|
|
1231
|
+
if (outputPath !== "-") {
|
|
1232
|
+
log(
|
|
1233
|
+
`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`,
|
|
1234
|
+
);
|
|
697
1235
|
}
|
|
698
1236
|
} else {
|
|
699
1237
|
errorCount++;
|
|
@@ -704,9 +1242,14 @@ async function main() {
|
|
|
704
1242
|
// Summary
|
|
705
1243
|
if (files.length > 1 && !config.quiet) {
|
|
706
1244
|
const totalSavings = totalOriginal - totalOptimized;
|
|
707
|
-
const totalPercent =
|
|
708
|
-
|
|
709
|
-
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
|
+
);
|
|
710
1253
|
}
|
|
711
1254
|
|
|
712
1255
|
if (errorCount > 0) {
|