@emasoft/svg-matrix 1.0.21 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/svgm.js +631 -0
- package/package.json +2 -1
- package/src/index.js +2 -2
package/bin/svgm.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview SVGO-compatible CLI for @emasoft/svg-matrix
|
|
4
|
+
* Drop-in replacement for SVGO with arbitrary-precision transforms.
|
|
5
|
+
*
|
|
6
|
+
* Usage mirrors SVGO exactly:
|
|
7
|
+
* svgm input.svg # optimize in place
|
|
8
|
+
* svgm input.svg -o output.svg # optimize to output
|
|
9
|
+
* svgm -f folder/ -o out/ # batch folder
|
|
10
|
+
* svgm -p 3 input.svg # set precision
|
|
11
|
+
*
|
|
12
|
+
* @module bin/svgm
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
17
|
+
import { join, dirname, basename, extname, resolve, isAbsolute } from 'path';
|
|
18
|
+
|
|
19
|
+
// Import library modules
|
|
20
|
+
import { VERSION } from '../src/index.js';
|
|
21
|
+
import * as SVGToolbox from '../src/svg-toolbox.js';
|
|
22
|
+
import { parseSVG, serializeSVG } from '../src/svg-parser.js';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// CONSTANTS
|
|
26
|
+
// ============================================================================
|
|
27
|
+
const CONSTANTS = {
|
|
28
|
+
DEFAULT_PRECISION: 6,
|
|
29
|
+
MAX_PRECISION: 50,
|
|
30
|
+
MIN_PRECISION: 0,
|
|
31
|
+
MAX_FILE_SIZE_BYTES: 50 * 1024 * 1024,
|
|
32
|
+
EXIT_SUCCESS: 0,
|
|
33
|
+
EXIT_ERROR: 1,
|
|
34
|
+
SVG_EXTENSIONS: ['.svg', '.svgz'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// COLORS (respects NO_COLOR env)
|
|
39
|
+
// ============================================================================
|
|
40
|
+
const colors = process.env.NO_COLOR !== undefined || process.argv.includes('--no-color') ? {
|
|
41
|
+
reset: '', red: '', yellow: '', green: '', cyan: '', dim: '', bright: '',
|
|
42
|
+
} : {
|
|
43
|
+
reset: '\x1b[0m', red: '\x1b[31m', yellow: '\x1b[33m',
|
|
44
|
+
green: '\x1b[32m', cyan: '\x1b[36m', dim: '\x1b[2m', bright: '\x1b[1m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// CONFIGURATION
|
|
49
|
+
// ============================================================================
|
|
50
|
+
const DEFAULT_CONFIG = {
|
|
51
|
+
inputs: [],
|
|
52
|
+
folder: null,
|
|
53
|
+
output: null,
|
|
54
|
+
string: null,
|
|
55
|
+
precision: null, // null means use default from plugins
|
|
56
|
+
multipass: false,
|
|
57
|
+
pretty: false,
|
|
58
|
+
indent: 2,
|
|
59
|
+
eol: null,
|
|
60
|
+
finalNewline: false,
|
|
61
|
+
recursive: false,
|
|
62
|
+
exclude: [],
|
|
63
|
+
quiet: false,
|
|
64
|
+
datauri: null,
|
|
65
|
+
showPlugins: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let config = { ...DEFAULT_CONFIG };
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// LOGGING
|
|
72
|
+
// ============================================================================
|
|
73
|
+
function log(msg) {
|
|
74
|
+
if (!config.quiet) console.log(msg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function logError(msg) {
|
|
78
|
+
console.error(`${colors.red}error:${colors.reset} ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// AVAILABLE OPTIMIZATIONS (like SVGO plugins)
|
|
83
|
+
// ============================================================================
|
|
84
|
+
const OPTIMIZATIONS = [
|
|
85
|
+
{ name: 'cleanupIds', description: 'Remove unused and minify used IDs' },
|
|
86
|
+
{ name: 'cleanupNumericValues', description: 'Round numeric values, remove default values' },
|
|
87
|
+
{ name: 'cleanupListOfValues', description: 'Round list of numeric values' },
|
|
88
|
+
{ name: 'cleanupAttributes', description: 'Remove unnecessary attributes' },
|
|
89
|
+
{ name: 'removeDoctype', description: 'Remove DOCTYPE declaration' },
|
|
90
|
+
{ name: 'removeXMLProcInst', description: 'Remove XML processing instructions' },
|
|
91
|
+
{ name: 'removeComments', description: 'Remove comments' },
|
|
92
|
+
{ name: 'removeMetadata', description: 'Remove <metadata> elements' },
|
|
93
|
+
{ name: 'removeTitle', description: 'Remove <title> elements' },
|
|
94
|
+
{ name: 'removeDesc', description: 'Remove <desc> elements' },
|
|
95
|
+
{ name: 'removeEditorsNSData', description: 'Remove editor namespaces and data' },
|
|
96
|
+
{ name: 'removeEmptyAttrs', description: 'Remove empty attributes' },
|
|
97
|
+
{ name: 'removeEmptyContainers', description: 'Remove empty containers' },
|
|
98
|
+
{ name: 'removeEmptyText', description: 'Remove empty text elements' },
|
|
99
|
+
{ name: 'removeHiddenElements', description: 'Remove hidden elements' },
|
|
100
|
+
{ name: 'removeUselessDefs', description: 'Remove unused <defs> content' },
|
|
101
|
+
{ name: 'removeUnknownsAndDefaults', description: 'Remove unknown elements and default attributes' },
|
|
102
|
+
{ name: 'convertShapesToPath', description: 'Convert shapes to paths' },
|
|
103
|
+
{ name: 'convertPathData', description: 'Optimize path data' },
|
|
104
|
+
{ name: 'convertTransform', description: 'Collapse multiple transforms' },
|
|
105
|
+
{ name: 'convertColors', description: 'Convert color values to shorter form' },
|
|
106
|
+
{ name: 'convertStyleToAttrs', description: 'Convert style to attributes' },
|
|
107
|
+
{ name: 'convertEllipseToCircle', description: 'Convert ellipse to circle when rx=ry' },
|
|
108
|
+
{ name: 'collapseGroups', description: 'Collapse useless groups' },
|
|
109
|
+
{ name: 'mergePaths', description: 'Merge multiple paths into one' },
|
|
110
|
+
{ name: 'moveGroupAttrsToElems', description: 'Move group attributes to elements' },
|
|
111
|
+
{ name: 'moveElemsAttrsToGroup', description: 'Move common element attributes to group' },
|
|
112
|
+
{ name: 'minifyStyles', description: 'Minify <style> content' },
|
|
113
|
+
{ name: 'inlineStyles', description: 'Inline styles from <style>' },
|
|
114
|
+
{ name: 'sortAttrs', description: 'Sort attributes for better compression' },
|
|
115
|
+
{ name: 'sortDefsChildren', description: 'Sort <defs> children for better compression' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// Default optimization pipeline (matches SVGO defaults)
|
|
119
|
+
const DEFAULT_PIPELINE = [
|
|
120
|
+
'removeDoctype',
|
|
121
|
+
'removeXMLProcInst',
|
|
122
|
+
'removeComments',
|
|
123
|
+
'removeMetadata',
|
|
124
|
+
'removeEditorsNSData',
|
|
125
|
+
'cleanupAttributes',
|
|
126
|
+
'cleanupNumericValues',
|
|
127
|
+
'convertColors',
|
|
128
|
+
'removeUnknownsAndDefaults',
|
|
129
|
+
'removeEmptyAttrs',
|
|
130
|
+
'removeEmptyContainers',
|
|
131
|
+
'removeEmptyText',
|
|
132
|
+
'removeHiddenElements',
|
|
133
|
+
'removeUselessDefs',
|
|
134
|
+
'convertShapesToPath',
|
|
135
|
+
'convertPathData',
|
|
136
|
+
'convertTransform',
|
|
137
|
+
'collapseGroups',
|
|
138
|
+
'mergePaths',
|
|
139
|
+
'sortAttrs',
|
|
140
|
+
'cleanupIds',
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// PATH UTILITIES
|
|
145
|
+
// ============================================================================
|
|
146
|
+
function normalizePath(p) { return p.replace(/\\/g, '/'); }
|
|
147
|
+
|
|
148
|
+
function resolvePath(p) {
|
|
149
|
+
return isAbsolute(p) ? normalizePath(p) : normalizePath(resolve(process.cwd(), p));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isDir(p) { try { return statSync(p).isDirectory(); } catch { return false; } }
|
|
153
|
+
function isFile(p) { try { return statSync(p).isFile(); } catch { return false; } }
|
|
154
|
+
|
|
155
|
+
function ensureDir(dir) {
|
|
156
|
+
if (!existsSync(dir)) {
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getSvgFiles(dir, recursive = false, exclude = []) {
|
|
162
|
+
const files = [];
|
|
163
|
+
function scan(d) {
|
|
164
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
165
|
+
const fullPath = join(d, entry.name);
|
|
166
|
+
// Check exclusion patterns
|
|
167
|
+
const shouldExclude = exclude.some(pattern => {
|
|
168
|
+
const regex = new RegExp(pattern);
|
|
169
|
+
return regex.test(fullPath) || regex.test(entry.name);
|
|
170
|
+
});
|
|
171
|
+
if (shouldExclude) continue;
|
|
172
|
+
|
|
173
|
+
if (entry.isDirectory() && recursive) scan(fullPath);
|
|
174
|
+
else if (entry.isFile() && CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())) {
|
|
175
|
+
files.push(normalizePath(fullPath));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
scan(dir);
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// SVG OPTIMIZATION
|
|
185
|
+
// ============================================================================
|
|
186
|
+
async function optimizeSvg(content, options = {}) {
|
|
187
|
+
const doc = parseSVG(content);
|
|
188
|
+
const pipeline = DEFAULT_PIPELINE;
|
|
189
|
+
|
|
190
|
+
// Run optimization pipeline
|
|
191
|
+
for (const pluginName of pipeline) {
|
|
192
|
+
const fn = SVGToolbox[pluginName];
|
|
193
|
+
if (fn && typeof fn === 'function') {
|
|
194
|
+
try {
|
|
195
|
+
await fn(doc, { precision: options.precision });
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Skip failed optimizations silently
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Multipass: run again if requested
|
|
203
|
+
if (options.multipass) {
|
|
204
|
+
for (const pluginName of pipeline) {
|
|
205
|
+
const fn = SVGToolbox[pluginName];
|
|
206
|
+
if (fn && typeof fn === 'function') {
|
|
207
|
+
try {
|
|
208
|
+
await fn(doc, { precision: options.precision });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Skip failed optimizations silently
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let result = serializeSVG(doc);
|
|
217
|
+
|
|
218
|
+
// Pretty print if requested
|
|
219
|
+
if (options.pretty) {
|
|
220
|
+
result = prettifyXml(result, options.indent);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle EOL
|
|
224
|
+
if (options.eol === 'crlf') {
|
|
225
|
+
result = result.replace(/\n/g, '\r\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Final newline
|
|
229
|
+
if (options.finalNewline && !result.endsWith('\n')) {
|
|
230
|
+
result += '\n';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function prettifyXml(xml, indent = 2) {
|
|
237
|
+
// Simple XML prettifier
|
|
238
|
+
const indentStr = ' '.repeat(indent);
|
|
239
|
+
let formatted = '';
|
|
240
|
+
let depth = 0;
|
|
241
|
+
|
|
242
|
+
// Split on tags
|
|
243
|
+
xml.replace(/>\s*</g, '>\n<').split('\n').forEach(line => {
|
|
244
|
+
line = line.trim();
|
|
245
|
+
if (!line) return;
|
|
246
|
+
|
|
247
|
+
// Decrease depth for closing tags
|
|
248
|
+
if (line.startsWith('</')) {
|
|
249
|
+
depth = Math.max(0, depth - 1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
formatted += indentStr.repeat(depth) + line + '\n';
|
|
253
|
+
|
|
254
|
+
// Increase depth for opening tags (not self-closing)
|
|
255
|
+
if (line.startsWith('<') && !line.startsWith('</') && !line.startsWith('<?') &&
|
|
256
|
+
!line.startsWith('<!') && !line.endsWith('/>') && !line.includes('</')) {
|
|
257
|
+
depth++;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return formatted.trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toDataUri(content, format) {
|
|
265
|
+
if (format === 'base64') {
|
|
266
|
+
return 'data:image/svg+xml;base64,' + Buffer.from(content).toString('base64');
|
|
267
|
+
} else if (format === 'enc') {
|
|
268
|
+
return 'data:image/svg+xml,' + encodeURIComponent(content);
|
|
269
|
+
} else {
|
|
270
|
+
return 'data:image/svg+xml,' + content;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// PROCESS FILES
|
|
276
|
+
// ============================================================================
|
|
277
|
+
async function processFile(inputPath, outputPath, options) {
|
|
278
|
+
try {
|
|
279
|
+
const content = readFileSync(inputPath, 'utf8');
|
|
280
|
+
const originalSize = Buffer.byteLength(content);
|
|
281
|
+
|
|
282
|
+
const optimized = await optimizeSvg(content, options);
|
|
283
|
+
const optimizedSize = Buffer.byteLength(optimized);
|
|
284
|
+
|
|
285
|
+
let output = optimized;
|
|
286
|
+
if (options.datauri) {
|
|
287
|
+
output = toDataUri(optimized, options.datauri);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (outputPath === '-') {
|
|
291
|
+
process.stdout.write(output);
|
|
292
|
+
} else {
|
|
293
|
+
ensureDir(dirname(outputPath));
|
|
294
|
+
writeFileSync(outputPath, output, 'utf8');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const savings = originalSize - optimizedSize;
|
|
298
|
+
const percent = ((savings / originalSize) * 100).toFixed(1);
|
|
299
|
+
|
|
300
|
+
return { success: true, originalSize, optimizedSize, savings, percent, inputPath, outputPath };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { success: false, error: error.message, inputPath };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// HELP
|
|
308
|
+
// ============================================================================
|
|
309
|
+
function showHelp() {
|
|
310
|
+
console.log(`Usage: svgm [options] [INPUT...]
|
|
311
|
+
|
|
312
|
+
SVGM is an SVGO-compatible CLI powered by svg-matrix for arbitrary-precision
|
|
313
|
+
SVG optimization.
|
|
314
|
+
|
|
315
|
+
Arguments:
|
|
316
|
+
INPUT Input files (or use -i, -f, -s)
|
|
317
|
+
|
|
318
|
+
Options:
|
|
319
|
+
-v, --version Output the version number
|
|
320
|
+
-i, --input <INPUT...> Input files, "-" for STDIN
|
|
321
|
+
-s, --string <STRING> Input SVG data string
|
|
322
|
+
-f, --folder <FOLDER> Input folder, optimize and rewrite all *.svg files
|
|
323
|
+
-o, --output <OUTPUT...> Output file or folder (by default same as input),
|
|
324
|
+
"-" for STDOUT
|
|
325
|
+
-p, --precision <INTEGER> Set number of digits in the fractional part,
|
|
326
|
+
overrides plugins params
|
|
327
|
+
--datauri <FORMAT> Output as Data URI string (base64), URI encoded
|
|
328
|
+
(enc) or unencoded (unenc)
|
|
329
|
+
--multipass Pass over SVGs multiple times to ensure all
|
|
330
|
+
optimizations are applied
|
|
331
|
+
--pretty Make SVG pretty printed
|
|
332
|
+
--indent <INTEGER> Indent number when pretty printing SVGs
|
|
333
|
+
--eol <EOL> Line break to use when outputting SVG: lf, crlf
|
|
334
|
+
--final-newline Ensure SVG ends with a line break
|
|
335
|
+
-r, --recursive Use with '--folder'. Optimizes *.svg files in
|
|
336
|
+
folders recursively.
|
|
337
|
+
--exclude <PATTERN...> Use with '--folder'. Exclude files matching
|
|
338
|
+
regular expression pattern.
|
|
339
|
+
-q, --quiet Only output error messages
|
|
340
|
+
--show-plugins Show available plugins and exit
|
|
341
|
+
--no-color Output plain text without color
|
|
342
|
+
-h, --help Display help for command
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
svgm input.svg -o output.svg
|
|
346
|
+
svgm -f ./icons/ -o ./optimized/
|
|
347
|
+
svgm input.svg --pretty --indent 4
|
|
348
|
+
svgm -p 2 --multipass input.svg
|
|
349
|
+
|
|
350
|
+
Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function showVersion() {
|
|
354
|
+
console.log(VERSION);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function showPlugins() {
|
|
358
|
+
console.log('\nAvailable optimizations:\n');
|
|
359
|
+
for (const opt of OPTIMIZATIONS) {
|
|
360
|
+
console.log(` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`);
|
|
361
|
+
}
|
|
362
|
+
console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// ARGUMENT PARSING
|
|
367
|
+
// ============================================================================
|
|
368
|
+
function parseArgs(args) {
|
|
369
|
+
const cfg = { ...DEFAULT_CONFIG };
|
|
370
|
+
const inputs = [];
|
|
371
|
+
let i = 0;
|
|
372
|
+
|
|
373
|
+
while (i < args.length) {
|
|
374
|
+
const arg = args[i];
|
|
375
|
+
|
|
376
|
+
switch (arg) {
|
|
377
|
+
case '-v':
|
|
378
|
+
case '--version':
|
|
379
|
+
showVersion();
|
|
380
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case '-h':
|
|
384
|
+
case '--help':
|
|
385
|
+
showHelp();
|
|
386
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
case '-i':
|
|
390
|
+
case '--input':
|
|
391
|
+
i++;
|
|
392
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
393
|
+
inputs.push(args[i]);
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
i--; // Back up one since the while loop went past
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case '-s':
|
|
400
|
+
case '--string':
|
|
401
|
+
cfg.string = args[++i];
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case '-f':
|
|
405
|
+
case '--folder':
|
|
406
|
+
cfg.folder = args[++i];
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case '-o':
|
|
410
|
+
case '--output':
|
|
411
|
+
i++;
|
|
412
|
+
// Collect output(s)
|
|
413
|
+
const outputs = [];
|
|
414
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
415
|
+
outputs.push(args[i]);
|
|
416
|
+
i++;
|
|
417
|
+
}
|
|
418
|
+
i--;
|
|
419
|
+
cfg.output = outputs.length === 1 ? outputs[0] : outputs;
|
|
420
|
+
break;
|
|
421
|
+
|
|
422
|
+
case '-p':
|
|
423
|
+
case '--precision':
|
|
424
|
+
cfg.precision = parseInt(args[++i], 10);
|
|
425
|
+
break;
|
|
426
|
+
|
|
427
|
+
case '--datauri':
|
|
428
|
+
cfg.datauri = args[++i];
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case '--multipass':
|
|
432
|
+
cfg.multipass = true;
|
|
433
|
+
break;
|
|
434
|
+
|
|
435
|
+
case '--pretty':
|
|
436
|
+
cfg.pretty = true;
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case '--indent':
|
|
440
|
+
cfg.indent = parseInt(args[++i], 10);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case '--eol':
|
|
444
|
+
cfg.eol = args[++i];
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case '--final-newline':
|
|
448
|
+
cfg.finalNewline = true;
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case '-r':
|
|
452
|
+
case '--recursive':
|
|
453
|
+
cfg.recursive = true;
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case '--exclude':
|
|
457
|
+
i++;
|
|
458
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
459
|
+
cfg.exclude.push(args[i]);
|
|
460
|
+
i++;
|
|
461
|
+
}
|
|
462
|
+
i--;
|
|
463
|
+
break;
|
|
464
|
+
|
|
465
|
+
case '-q':
|
|
466
|
+
case '--quiet':
|
|
467
|
+
cfg.quiet = true;
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case '--show-plugins':
|
|
471
|
+
cfg.showPlugins = true;
|
|
472
|
+
break;
|
|
473
|
+
|
|
474
|
+
case '--no-color':
|
|
475
|
+
// Already handled in colors initialization
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
default:
|
|
479
|
+
if (arg.startsWith('-')) {
|
|
480
|
+
logError(`Unknown option: ${arg}`);
|
|
481
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
482
|
+
}
|
|
483
|
+
inputs.push(arg);
|
|
484
|
+
}
|
|
485
|
+
i++;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
cfg.inputs = inputs;
|
|
489
|
+
return cfg;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ============================================================================
|
|
493
|
+
// MAIN
|
|
494
|
+
// ============================================================================
|
|
495
|
+
async function main() {
|
|
496
|
+
const args = process.argv.slice(2);
|
|
497
|
+
|
|
498
|
+
if (args.length === 0) {
|
|
499
|
+
showHelp();
|
|
500
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
config = parseArgs(args);
|
|
504
|
+
|
|
505
|
+
if (config.showPlugins) {
|
|
506
|
+
showPlugins();
|
|
507
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const options = {
|
|
511
|
+
precision: config.precision,
|
|
512
|
+
multipass: config.multipass,
|
|
513
|
+
pretty: config.pretty,
|
|
514
|
+
indent: config.indent,
|
|
515
|
+
eol: config.eol,
|
|
516
|
+
finalNewline: config.finalNewline,
|
|
517
|
+
datauri: config.datauri,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Handle string input
|
|
521
|
+
if (config.string) {
|
|
522
|
+
try {
|
|
523
|
+
const result = await optimizeSvg(config.string, options);
|
|
524
|
+
const output = config.datauri ? toDataUri(result, config.datauri) : result;
|
|
525
|
+
if (config.output && config.output !== '-') {
|
|
526
|
+
writeFileSync(config.output, output, 'utf8');
|
|
527
|
+
log(`${colors.green}Done!${colors.reset}`);
|
|
528
|
+
} else {
|
|
529
|
+
process.stdout.write(output);
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
logError(e.message);
|
|
533
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Gather input files
|
|
539
|
+
let files = [];
|
|
540
|
+
|
|
541
|
+
if (config.folder) {
|
|
542
|
+
const folderPath = resolvePath(config.folder);
|
|
543
|
+
if (!isDir(folderPath)) {
|
|
544
|
+
logError(`Folder not found: ${config.folder}`);
|
|
545
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
546
|
+
}
|
|
547
|
+
files = getSvgFiles(folderPath, config.recursive, config.exclude);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Add explicit inputs
|
|
551
|
+
for (const input of config.inputs) {
|
|
552
|
+
if (input === '-') {
|
|
553
|
+
// STDIN handling would go here
|
|
554
|
+
logError('STDIN not yet supported');
|
|
555
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
556
|
+
}
|
|
557
|
+
const resolved = resolvePath(input);
|
|
558
|
+
if (isFile(resolved)) {
|
|
559
|
+
files.push(resolved);
|
|
560
|
+
} else if (isDir(resolved)) {
|
|
561
|
+
files.push(...getSvgFiles(resolved, config.recursive, config.exclude));
|
|
562
|
+
} else {
|
|
563
|
+
logError(`File not found: ${input}`);
|
|
564
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (files.length === 0) {
|
|
569
|
+
logError('No input files');
|
|
570
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Process files
|
|
574
|
+
let totalOriginal = 0;
|
|
575
|
+
let totalOptimized = 0;
|
|
576
|
+
let successCount = 0;
|
|
577
|
+
let errorCount = 0;
|
|
578
|
+
|
|
579
|
+
for (let i = 0; i < files.length; i++) {
|
|
580
|
+
const inputPath = files[i];
|
|
581
|
+
let outputPath;
|
|
582
|
+
|
|
583
|
+
if (config.output) {
|
|
584
|
+
if (config.output === '-') {
|
|
585
|
+
outputPath = '-';
|
|
586
|
+
} else if (Array.isArray(config.output)) {
|
|
587
|
+
outputPath = config.output[i] || config.output[0];
|
|
588
|
+
} else if (files.length > 1 || isDir(resolvePath(config.output))) {
|
|
589
|
+
// Multiple files or output is a directory
|
|
590
|
+
outputPath = join(resolvePath(config.output), basename(inputPath));
|
|
591
|
+
} else {
|
|
592
|
+
outputPath = resolvePath(config.output);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
// In-place optimization (same as input)
|
|
596
|
+
outputPath = inputPath;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const result = await processFile(inputPath, outputPath, options);
|
|
600
|
+
|
|
601
|
+
if (result.success) {
|
|
602
|
+
successCount++;
|
|
603
|
+
totalOriginal += result.originalSize;
|
|
604
|
+
totalOptimized += result.optimizedSize;
|
|
605
|
+
|
|
606
|
+
if (outputPath !== '-') {
|
|
607
|
+
log(`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`);
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
errorCount++;
|
|
611
|
+
logError(`${basename(inputPath)}: ${result.error}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Summary
|
|
616
|
+
if (files.length > 1 && !config.quiet) {
|
|
617
|
+
const totalSavings = totalOriginal - totalOptimized;
|
|
618
|
+
const totalPercent = totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
|
|
619
|
+
console.log(`\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`);
|
|
620
|
+
console.log(`${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (errorCount > 0) {
|
|
624
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
main().catch((e) => {
|
|
629
|
+
logError(e.message);
|
|
630
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
631
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"bin": {
|
|
57
57
|
"svg-matrix": "bin/svg-matrix.js",
|
|
58
|
+
"svgm": "bin/svgm.js",
|
|
58
59
|
"svglinter": "bin/svglinter.cjs"
|
|
59
60
|
},
|
|
60
61
|
"engines": {
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* SVG path conversion, and 2D/3D affine transformations using Decimal.js.
|
|
6
6
|
*
|
|
7
7
|
* @module @emasoft/svg-matrix
|
|
8
|
-
* @version 1.0.
|
|
8
|
+
* @version 1.0.22
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -86,7 +86,7 @@ Decimal.set({ precision: 80 });
|
|
|
86
86
|
* Library version
|
|
87
87
|
* @constant {string}
|
|
88
88
|
*/
|
|
89
|
-
export const VERSION = '1.0.
|
|
89
|
+
export const VERSION = '1.0.22';
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Default precision for path output (decimal places)
|