@emasoft/svg-matrix 1.0.21 → 1.0.23
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 +679 -0
- package/package.json +2 -1
- package/src/index.js +2 -2
package/bin/svgm.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
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 (matching SVGO plugins)
|
|
83
|
+
// ============================================================================
|
|
84
|
+
const OPTIMIZATIONS = [
|
|
85
|
+
// Cleanup plugins
|
|
86
|
+
{ name: 'cleanupAttributes', description: 'Remove useless attributes' },
|
|
87
|
+
{ name: 'cleanupIds', description: 'Remove unused and minify used IDs' },
|
|
88
|
+
{ name: 'cleanupNumericValues', description: 'Round numeric values, remove default px units' },
|
|
89
|
+
{ name: 'cleanupListOfValues', description: 'Round list of numeric values' },
|
|
90
|
+
{ name: 'cleanupEnableBackground', description: 'Remove or fix enable-background attribute' },
|
|
91
|
+
// Remove plugins
|
|
92
|
+
{ name: 'removeDoctype', description: 'Remove DOCTYPE declaration' },
|
|
93
|
+
{ name: 'removeXMLProcInst', description: 'Remove XML processing instructions' },
|
|
94
|
+
{ name: 'removeComments', description: 'Remove comments' },
|
|
95
|
+
{ name: 'removeMetadata', description: 'Remove <metadata> elements' },
|
|
96
|
+
{ name: 'removeTitle', description: 'Remove <title> elements (not in default)' },
|
|
97
|
+
{ name: 'removeDesc', description: 'Remove <desc> elements' },
|
|
98
|
+
{ name: 'removeEditorsNSData', description: 'Remove editor namespaces, elements, and attributes' },
|
|
99
|
+
{ name: 'removeEmptyAttrs', description: 'Remove empty attributes' },
|
|
100
|
+
{ name: 'removeEmptyContainers', description: 'Remove empty container elements' },
|
|
101
|
+
{ name: 'removeEmptyText', description: 'Remove empty text elements' },
|
|
102
|
+
{ name: 'removeHiddenElements', description: 'Remove hidden elements' },
|
|
103
|
+
{ name: 'removeUselessDefs', description: 'Remove unused <defs> content' },
|
|
104
|
+
{ name: 'removeUnknownsAndDefaults', description: 'Remove unknown elements and default attribute values' },
|
|
105
|
+
{ name: 'removeNonInheritableGroupAttrs', description: 'Remove non-inheritable presentation attributes from groups' },
|
|
106
|
+
// Convert plugins
|
|
107
|
+
{ name: 'convertShapesToPath', description: 'Convert basic shapes to paths' },
|
|
108
|
+
{ name: 'convertPathData', description: 'Optimize path data: convert, remove useless, etc.' },
|
|
109
|
+
{ name: 'convertTransform', description: 'Collapse multiple transforms into one, convert matrices' },
|
|
110
|
+
{ name: 'convertColors', description: 'Convert color values to shorter form' },
|
|
111
|
+
{ name: 'convertStyleToAttrs', description: 'Convert style to presentation attributes (not in default)' },
|
|
112
|
+
{ name: 'convertEllipseToCircle', description: 'Convert ellipse to circle when rx equals ry' },
|
|
113
|
+
// Structure plugins
|
|
114
|
+
{ name: 'collapseGroups', description: 'Collapse useless groups' },
|
|
115
|
+
{ name: 'mergePaths', description: 'Merge multiple paths into one' },
|
|
116
|
+
{ name: 'moveGroupAttrsToElems', description: 'Move group attributes to contained elements' },
|
|
117
|
+
{ name: 'moveElemsAttrsToGroup', description: 'Move common element attributes to parent group' },
|
|
118
|
+
// Style plugins
|
|
119
|
+
{ name: 'minifyStyles', description: 'Minify <style> elements content' },
|
|
120
|
+
{ name: 'inlineStyles', description: 'Inline styles from <style> to element style attributes' },
|
|
121
|
+
// Sort plugins
|
|
122
|
+
{ name: 'sortAttrs', description: 'Sort attributes for better gzip compression' },
|
|
123
|
+
{ name: 'sortDefsChildren', description: 'Sort children of <defs> for better gzip compression' },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// DEFAULT PIPELINE - Matches SVGO preset-default exactly (34 plugins)
|
|
128
|
+
// Order matters! This is the exact order from SVGO's preset-default.js
|
|
129
|
+
// ============================================================================
|
|
130
|
+
const DEFAULT_PIPELINE = [
|
|
131
|
+
// 1-6: Initial cleanup (matching SVGO preset-default order)
|
|
132
|
+
'removeDoctype',
|
|
133
|
+
'removeXMLProcInst',
|
|
134
|
+
'removeComments',
|
|
135
|
+
// removeDeprecatedAttrs - not implemented (rarely needed)
|
|
136
|
+
'removeMetadata',
|
|
137
|
+
'removeEditorsNSData',
|
|
138
|
+
// 7-11: Style processing
|
|
139
|
+
'cleanupAttributes',
|
|
140
|
+
// mergeStyles - not implemented
|
|
141
|
+
'inlineStyles',
|
|
142
|
+
'minifyStyles',
|
|
143
|
+
'cleanupIds',
|
|
144
|
+
// 12-18: Remove unnecessary elements
|
|
145
|
+
'removeUselessDefs',
|
|
146
|
+
'cleanupNumericValues',
|
|
147
|
+
'convertColors',
|
|
148
|
+
'removeUnknownsAndDefaults',
|
|
149
|
+
'removeNonInheritableGroupAttrs',
|
|
150
|
+
// removeUselessStrokeAndFill - not implemented
|
|
151
|
+
'cleanupEnableBackground',
|
|
152
|
+
'removeHiddenElements',
|
|
153
|
+
'removeEmptyText',
|
|
154
|
+
// 19-27: Convert and optimize
|
|
155
|
+
// NOTE: convertShapesToPath removed - SVGO only converts when it saves bytes
|
|
156
|
+
// Our version converts all shapes which often increases size
|
|
157
|
+
'convertEllipseToCircle',
|
|
158
|
+
'moveElemsAttrsToGroup',
|
|
159
|
+
'moveGroupAttrsToElems',
|
|
160
|
+
'collapseGroups',
|
|
161
|
+
'convertPathData',
|
|
162
|
+
'convertTransform',
|
|
163
|
+
// 28-34: Final cleanup
|
|
164
|
+
'removeEmptyAttrs',
|
|
165
|
+
'removeEmptyContainers',
|
|
166
|
+
'mergePaths',
|
|
167
|
+
// removeUnusedNS - not implemented
|
|
168
|
+
'sortAttrs',
|
|
169
|
+
'sortDefsChildren',
|
|
170
|
+
'removeDesc',
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// PATH UTILITIES
|
|
175
|
+
// ============================================================================
|
|
176
|
+
function normalizePath(p) { return p.replace(/\\/g, '/'); }
|
|
177
|
+
|
|
178
|
+
function resolvePath(p) {
|
|
179
|
+
return isAbsolute(p) ? normalizePath(p) : normalizePath(resolve(process.cwd(), p));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isDir(p) { try { return statSync(p).isDirectory(); } catch { return false; } }
|
|
183
|
+
function isFile(p) { try { return statSync(p).isFile(); } catch { return false; } }
|
|
184
|
+
|
|
185
|
+
function ensureDir(dir) {
|
|
186
|
+
if (!existsSync(dir)) {
|
|
187
|
+
mkdirSync(dir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getSvgFiles(dir, recursive = false, exclude = []) {
|
|
192
|
+
const files = [];
|
|
193
|
+
function scan(d) {
|
|
194
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
195
|
+
const fullPath = join(d, entry.name);
|
|
196
|
+
// Check exclusion patterns
|
|
197
|
+
const shouldExclude = exclude.some(pattern => {
|
|
198
|
+
const regex = new RegExp(pattern);
|
|
199
|
+
return regex.test(fullPath) || regex.test(entry.name);
|
|
200
|
+
});
|
|
201
|
+
if (shouldExclude) continue;
|
|
202
|
+
|
|
203
|
+
if (entry.isDirectory() && recursive) scan(fullPath);
|
|
204
|
+
else if (entry.isFile() && CONSTANTS.SVG_EXTENSIONS.includes(extname(entry.name).toLowerCase())) {
|
|
205
|
+
files.push(normalizePath(fullPath));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
scan(dir);
|
|
210
|
+
return files;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// SVG OPTIMIZATION
|
|
215
|
+
// ============================================================================
|
|
216
|
+
async function optimizeSvg(content, options = {}) {
|
|
217
|
+
const doc = parseSVG(content);
|
|
218
|
+
const pipeline = DEFAULT_PIPELINE;
|
|
219
|
+
|
|
220
|
+
// Run optimization pipeline
|
|
221
|
+
for (const pluginName of pipeline) {
|
|
222
|
+
const fn = SVGToolbox[pluginName];
|
|
223
|
+
if (fn && typeof fn === 'function') {
|
|
224
|
+
try {
|
|
225
|
+
await fn(doc, { precision: options.precision });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// Skip failed optimizations silently
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Multipass: run again if requested
|
|
233
|
+
if (options.multipass) {
|
|
234
|
+
for (const pluginName of pipeline) {
|
|
235
|
+
const fn = SVGToolbox[pluginName];
|
|
236
|
+
if (fn && typeof fn === 'function') {
|
|
237
|
+
try {
|
|
238
|
+
await fn(doc, { precision: options.precision });
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// Skip failed optimizations silently
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let result = serializeSVG(doc);
|
|
247
|
+
|
|
248
|
+
// Pretty print if requested, otherwise minify (SVGO default behavior)
|
|
249
|
+
if (options.pretty) {
|
|
250
|
+
result = prettifyXml(result, options.indent);
|
|
251
|
+
} else {
|
|
252
|
+
result = minifyXml(result);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle EOL
|
|
256
|
+
if (options.eol === 'crlf') {
|
|
257
|
+
result = result.replace(/\n/g, '\r\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Final newline
|
|
261
|
+
if (options.finalNewline && !result.endsWith('\n')) {
|
|
262
|
+
result += '\n';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Minify XML output (matching SVGO behavior)
|
|
270
|
+
* - Remove XML declaration
|
|
271
|
+
* - Remove whitespace between tags
|
|
272
|
+
* - Collapse multiple spaces
|
|
273
|
+
*/
|
|
274
|
+
function minifyXml(xml) {
|
|
275
|
+
return xml
|
|
276
|
+
// Remove XML declaration (SVGO removes it by default)
|
|
277
|
+
.replace(/<\?xml[^?]*\?>\s*/gi, '')
|
|
278
|
+
// Remove newlines and collapse whitespace between tags
|
|
279
|
+
.replace(/>\s+</g, '><')
|
|
280
|
+
// Remove leading/trailing whitespace
|
|
281
|
+
.trim();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function prettifyXml(xml, indent = 2) {
|
|
285
|
+
// Simple XML prettifier
|
|
286
|
+
const indentStr = ' '.repeat(indent);
|
|
287
|
+
let formatted = '';
|
|
288
|
+
let depth = 0;
|
|
289
|
+
|
|
290
|
+
// Split on tags
|
|
291
|
+
xml.replace(/>\s*</g, '>\n<').split('\n').forEach(line => {
|
|
292
|
+
line = line.trim();
|
|
293
|
+
if (!line) return;
|
|
294
|
+
|
|
295
|
+
// Decrease depth for closing tags
|
|
296
|
+
if (line.startsWith('</')) {
|
|
297
|
+
depth = Math.max(0, depth - 1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
formatted += indentStr.repeat(depth) + line + '\n';
|
|
301
|
+
|
|
302
|
+
// Increase depth for opening tags (not self-closing)
|
|
303
|
+
if (line.startsWith('<') && !line.startsWith('</') && !line.startsWith('<?') &&
|
|
304
|
+
!line.startsWith('<!') && !line.endsWith('/>') && !line.includes('</')) {
|
|
305
|
+
depth++;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return formatted.trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function toDataUri(content, format) {
|
|
313
|
+
if (format === 'base64') {
|
|
314
|
+
return 'data:image/svg+xml;base64,' + Buffer.from(content).toString('base64');
|
|
315
|
+
} else if (format === 'enc') {
|
|
316
|
+
return 'data:image/svg+xml,' + encodeURIComponent(content);
|
|
317
|
+
} else {
|
|
318
|
+
return 'data:image/svg+xml,' + content;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// PROCESS FILES
|
|
324
|
+
// ============================================================================
|
|
325
|
+
async function processFile(inputPath, outputPath, options) {
|
|
326
|
+
try {
|
|
327
|
+
const content = readFileSync(inputPath, 'utf8');
|
|
328
|
+
const originalSize = Buffer.byteLength(content);
|
|
329
|
+
|
|
330
|
+
const optimized = await optimizeSvg(content, options);
|
|
331
|
+
const optimizedSize = Buffer.byteLength(optimized);
|
|
332
|
+
|
|
333
|
+
let output = optimized;
|
|
334
|
+
if (options.datauri) {
|
|
335
|
+
output = toDataUri(optimized, options.datauri);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (outputPath === '-') {
|
|
339
|
+
process.stdout.write(output);
|
|
340
|
+
} else {
|
|
341
|
+
ensureDir(dirname(outputPath));
|
|
342
|
+
writeFileSync(outputPath, output, 'utf8');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const savings = originalSize - optimizedSize;
|
|
346
|
+
const percent = ((savings / originalSize) * 100).toFixed(1);
|
|
347
|
+
|
|
348
|
+
return { success: true, originalSize, optimizedSize, savings, percent, inputPath, outputPath };
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return { success: false, error: error.message, inputPath };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// HELP
|
|
356
|
+
// ============================================================================
|
|
357
|
+
function showHelp() {
|
|
358
|
+
console.log(`Usage: svgm [options] [INPUT...]
|
|
359
|
+
|
|
360
|
+
SVGM is an SVGO-compatible CLI powered by svg-matrix for arbitrary-precision
|
|
361
|
+
SVG optimization.
|
|
362
|
+
|
|
363
|
+
Arguments:
|
|
364
|
+
INPUT Input files (or use -i, -f, -s)
|
|
365
|
+
|
|
366
|
+
Options:
|
|
367
|
+
-v, --version Output the version number
|
|
368
|
+
-i, --input <INPUT...> Input files, "-" for STDIN
|
|
369
|
+
-s, --string <STRING> Input SVG data string
|
|
370
|
+
-f, --folder <FOLDER> Input folder, optimize and rewrite all *.svg files
|
|
371
|
+
-o, --output <OUTPUT...> Output file or folder (by default same as input),
|
|
372
|
+
"-" for STDOUT
|
|
373
|
+
-p, --precision <INTEGER> Set number of digits in the fractional part,
|
|
374
|
+
overrides plugins params
|
|
375
|
+
--datauri <FORMAT> Output as Data URI string (base64), URI encoded
|
|
376
|
+
(enc) or unencoded (unenc)
|
|
377
|
+
--multipass Pass over SVGs multiple times to ensure all
|
|
378
|
+
optimizations are applied
|
|
379
|
+
--pretty Make SVG pretty printed
|
|
380
|
+
--indent <INTEGER> Indent number when pretty printing SVGs
|
|
381
|
+
--eol <EOL> Line break to use when outputting SVG: lf, crlf
|
|
382
|
+
--final-newline Ensure SVG ends with a line break
|
|
383
|
+
-r, --recursive Use with '--folder'. Optimizes *.svg files in
|
|
384
|
+
folders recursively.
|
|
385
|
+
--exclude <PATTERN...> Use with '--folder'. Exclude files matching
|
|
386
|
+
regular expression pattern.
|
|
387
|
+
-q, --quiet Only output error messages
|
|
388
|
+
--show-plugins Show available plugins and exit
|
|
389
|
+
--no-color Output plain text without color
|
|
390
|
+
-h, --help Display help for command
|
|
391
|
+
|
|
392
|
+
Examples:
|
|
393
|
+
svgm input.svg -o output.svg
|
|
394
|
+
svgm -f ./icons/ -o ./optimized/
|
|
395
|
+
svgm input.svg --pretty --indent 4
|
|
396
|
+
svgm -p 2 --multipass input.svg
|
|
397
|
+
|
|
398
|
+
Docs: https://github.com/Emasoft/SVG-MATRIX#readme`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function showVersion() {
|
|
402
|
+
console.log(VERSION);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function showPlugins() {
|
|
406
|
+
console.log('\nAvailable optimizations:\n');
|
|
407
|
+
for (const opt of OPTIMIZATIONS) {
|
|
408
|
+
console.log(` ${colors.green}${opt.name.padEnd(30)}${colors.reset} ${opt.description}`);
|
|
409
|
+
}
|
|
410
|
+
console.log(`\nTotal: ${OPTIMIZATIONS.length} optimizations\n`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// ARGUMENT PARSING
|
|
415
|
+
// ============================================================================
|
|
416
|
+
function parseArgs(args) {
|
|
417
|
+
const cfg = { ...DEFAULT_CONFIG };
|
|
418
|
+
const inputs = [];
|
|
419
|
+
let i = 0;
|
|
420
|
+
|
|
421
|
+
while (i < args.length) {
|
|
422
|
+
const arg = args[i];
|
|
423
|
+
|
|
424
|
+
switch (arg) {
|
|
425
|
+
case '-v':
|
|
426
|
+
case '--version':
|
|
427
|
+
showVersion();
|
|
428
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case '-h':
|
|
432
|
+
case '--help':
|
|
433
|
+
showHelp();
|
|
434
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
435
|
+
break;
|
|
436
|
+
|
|
437
|
+
case '-i':
|
|
438
|
+
case '--input':
|
|
439
|
+
i++;
|
|
440
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
441
|
+
inputs.push(args[i]);
|
|
442
|
+
i++;
|
|
443
|
+
}
|
|
444
|
+
i--; // Back up one since the while loop went past
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case '-s':
|
|
448
|
+
case '--string':
|
|
449
|
+
cfg.string = args[++i];
|
|
450
|
+
break;
|
|
451
|
+
|
|
452
|
+
case '-f':
|
|
453
|
+
case '--folder':
|
|
454
|
+
cfg.folder = args[++i];
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case '-o':
|
|
458
|
+
case '--output':
|
|
459
|
+
i++;
|
|
460
|
+
// Collect output(s)
|
|
461
|
+
const outputs = [];
|
|
462
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
463
|
+
outputs.push(args[i]);
|
|
464
|
+
i++;
|
|
465
|
+
}
|
|
466
|
+
i--;
|
|
467
|
+
cfg.output = outputs.length === 1 ? outputs[0] : outputs;
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case '-p':
|
|
471
|
+
case '--precision':
|
|
472
|
+
cfg.precision = parseInt(args[++i], 10);
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case '--datauri':
|
|
476
|
+
cfg.datauri = args[++i];
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
case '--multipass':
|
|
480
|
+
cfg.multipass = true;
|
|
481
|
+
break;
|
|
482
|
+
|
|
483
|
+
case '--pretty':
|
|
484
|
+
cfg.pretty = true;
|
|
485
|
+
break;
|
|
486
|
+
|
|
487
|
+
case '--indent':
|
|
488
|
+
cfg.indent = parseInt(args[++i], 10);
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case '--eol':
|
|
492
|
+
cfg.eol = args[++i];
|
|
493
|
+
break;
|
|
494
|
+
|
|
495
|
+
case '--final-newline':
|
|
496
|
+
cfg.finalNewline = true;
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case '-r':
|
|
500
|
+
case '--recursive':
|
|
501
|
+
cfg.recursive = true;
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
case '--exclude':
|
|
505
|
+
i++;
|
|
506
|
+
while (i < args.length && !args[i].startsWith('-')) {
|
|
507
|
+
cfg.exclude.push(args[i]);
|
|
508
|
+
i++;
|
|
509
|
+
}
|
|
510
|
+
i--;
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case '-q':
|
|
514
|
+
case '--quiet':
|
|
515
|
+
cfg.quiet = true;
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
case '--show-plugins':
|
|
519
|
+
cfg.showPlugins = true;
|
|
520
|
+
break;
|
|
521
|
+
|
|
522
|
+
case '--no-color':
|
|
523
|
+
// Already handled in colors initialization
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
default:
|
|
527
|
+
if (arg.startsWith('-')) {
|
|
528
|
+
logError(`Unknown option: ${arg}`);
|
|
529
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
530
|
+
}
|
|
531
|
+
inputs.push(arg);
|
|
532
|
+
}
|
|
533
|
+
i++;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
cfg.inputs = inputs;
|
|
537
|
+
return cfg;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// MAIN
|
|
542
|
+
// ============================================================================
|
|
543
|
+
async function main() {
|
|
544
|
+
const args = process.argv.slice(2);
|
|
545
|
+
|
|
546
|
+
if (args.length === 0) {
|
|
547
|
+
showHelp();
|
|
548
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
config = parseArgs(args);
|
|
552
|
+
|
|
553
|
+
if (config.showPlugins) {
|
|
554
|
+
showPlugins();
|
|
555
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const options = {
|
|
559
|
+
precision: config.precision,
|
|
560
|
+
multipass: config.multipass,
|
|
561
|
+
pretty: config.pretty,
|
|
562
|
+
indent: config.indent,
|
|
563
|
+
eol: config.eol,
|
|
564
|
+
finalNewline: config.finalNewline,
|
|
565
|
+
datauri: config.datauri,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Handle string input
|
|
569
|
+
if (config.string) {
|
|
570
|
+
try {
|
|
571
|
+
const result = await optimizeSvg(config.string, options);
|
|
572
|
+
const output = config.datauri ? toDataUri(result, config.datauri) : result;
|
|
573
|
+
if (config.output && config.output !== '-') {
|
|
574
|
+
writeFileSync(config.output, output, 'utf8');
|
|
575
|
+
log(`${colors.green}Done!${colors.reset}`);
|
|
576
|
+
} else {
|
|
577
|
+
process.stdout.write(output);
|
|
578
|
+
}
|
|
579
|
+
} catch (e) {
|
|
580
|
+
logError(e.message);
|
|
581
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Gather input files
|
|
587
|
+
let files = [];
|
|
588
|
+
|
|
589
|
+
if (config.folder) {
|
|
590
|
+
const folderPath = resolvePath(config.folder);
|
|
591
|
+
if (!isDir(folderPath)) {
|
|
592
|
+
logError(`Folder not found: ${config.folder}`);
|
|
593
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
594
|
+
}
|
|
595
|
+
files = getSvgFiles(folderPath, config.recursive, config.exclude);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Add explicit inputs
|
|
599
|
+
for (const input of config.inputs) {
|
|
600
|
+
if (input === '-') {
|
|
601
|
+
// STDIN handling would go here
|
|
602
|
+
logError('STDIN not yet supported');
|
|
603
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
604
|
+
}
|
|
605
|
+
const resolved = resolvePath(input);
|
|
606
|
+
if (isFile(resolved)) {
|
|
607
|
+
files.push(resolved);
|
|
608
|
+
} else if (isDir(resolved)) {
|
|
609
|
+
files.push(...getSvgFiles(resolved, config.recursive, config.exclude));
|
|
610
|
+
} else {
|
|
611
|
+
logError(`File not found: ${input}`);
|
|
612
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (files.length === 0) {
|
|
617
|
+
logError('No input files');
|
|
618
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Process files
|
|
622
|
+
let totalOriginal = 0;
|
|
623
|
+
let totalOptimized = 0;
|
|
624
|
+
let successCount = 0;
|
|
625
|
+
let errorCount = 0;
|
|
626
|
+
|
|
627
|
+
for (let i = 0; i < files.length; i++) {
|
|
628
|
+
const inputPath = files[i];
|
|
629
|
+
let outputPath;
|
|
630
|
+
|
|
631
|
+
if (config.output) {
|
|
632
|
+
if (config.output === '-') {
|
|
633
|
+
outputPath = '-';
|
|
634
|
+
} else if (Array.isArray(config.output)) {
|
|
635
|
+
outputPath = config.output[i] || config.output[0];
|
|
636
|
+
} else if (files.length > 1 || isDir(resolvePath(config.output))) {
|
|
637
|
+
// Multiple files or output is a directory
|
|
638
|
+
outputPath = join(resolvePath(config.output), basename(inputPath));
|
|
639
|
+
} else {
|
|
640
|
+
outputPath = resolvePath(config.output);
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
// In-place optimization (same as input)
|
|
644
|
+
outputPath = inputPath;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const result = await processFile(inputPath, outputPath, options);
|
|
648
|
+
|
|
649
|
+
if (result.success) {
|
|
650
|
+
successCount++;
|
|
651
|
+
totalOriginal += result.originalSize;
|
|
652
|
+
totalOptimized += result.optimizedSize;
|
|
653
|
+
|
|
654
|
+
if (outputPath !== '-') {
|
|
655
|
+
log(`${colors.green}${basename(inputPath)}${colors.reset} - ${result.originalSize} B -> ${result.optimizedSize} B (${result.percent}% saved)`);
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
errorCount++;
|
|
659
|
+
logError(`${basename(inputPath)}: ${result.error}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Summary
|
|
664
|
+
if (files.length > 1 && !config.quiet) {
|
|
665
|
+
const totalSavings = totalOriginal - totalOptimized;
|
|
666
|
+
const totalPercent = totalOriginal > 0 ? ((totalSavings / totalOriginal) * 100).toFixed(1) : 0;
|
|
667
|
+
console.log(`\n${colors.bright}Total:${colors.reset} ${successCount} file(s) optimized, ${errorCount} error(s)`);
|
|
668
|
+
console.log(`${colors.bright}Savings:${colors.reset} ${totalOriginal} B -> ${totalOptimized} B (${totalPercent}% saved)`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (errorCount > 0) {
|
|
672
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
main().catch((e) => {
|
|
677
|
+
logError(e.message);
|
|
678
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
679
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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.23
|
|
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.23';
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Default precision for path output (decimal places)
|