lofter-lottie-opt 1.0.1
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 +371 -0
- package/dist/bundle/358.mjs +34 -0
- package/dist/bundle/506.mjs +301 -0
- package/dist/bundle/599.mjs +15335 -0
- package/dist/bundle/599.mjs.LICENSE.txt +22 -0
- package/dist/bundle/index.mjs +13631 -0
- package/dist/bundle/index.mjs.LICENSE.txt +13 -0
- package/dist/cli/index.mjs +3162 -0
- package/dist/esm/cli/index.d.ts +3 -0
- package/dist/esm/cli/index.d.ts.map +1 -0
- package/dist/esm/cli/validate.d.ts +3 -0
- package/dist/esm/cli/validate.d.ts.map +1 -0
- package/dist/esm/core/optimizer.d.ts +13 -0
- package/dist/esm/core/optimizer.d.ts.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.mjs +2048 -0
- package/dist/esm/plugins/compress-images/algorithm.d.ts +11 -0
- package/dist/esm/plugins/compress-images/algorithm.d.ts.map +1 -0
- package/dist/esm/plugins/compress-images/index.d.ts +14 -0
- package/dist/esm/plugins/compress-images/index.d.ts.map +1 -0
- package/dist/esm/plugins/compress-images/tinypng.d.ts +22 -0
- package/dist/esm/plugins/compress-images/tinypng.d.ts.map +1 -0
- package/dist/esm/plugins/compress-images/types.d.ts +106 -0
- package/dist/esm/plugins/compress-images/types.d.ts.map +1 -0
- package/dist/esm/plugins/compress-images/utils.d.ts +66 -0
- package/dist/esm/plugins/compress-images/utils.d.ts.map +1 -0
- package/dist/esm/plugins/index.d.ts +8 -0
- package/dist/esm/plugins/index.d.ts.map +1 -0
- package/dist/esm/plugins/minify-json/algorithm.d.ts +15 -0
- package/dist/esm/plugins/minify-json/algorithm.d.ts.map +1 -0
- package/dist/esm/plugins/minify-json/index.d.ts +9 -0
- package/dist/esm/plugins/minify-json/index.d.ts.map +1 -0
- package/dist/esm/plugins/minify-json/types.d.ts +55 -0
- package/dist/esm/plugins/minify-json/types.d.ts.map +1 -0
- package/dist/esm/plugins/remove-unused-assets/algorithm.d.ts +20 -0
- package/dist/esm/plugins/remove-unused-assets/algorithm.d.ts.map +1 -0
- package/dist/esm/plugins/remove-unused-assets/index.d.ts +18 -0
- package/dist/esm/plugins/remove-unused-assets/index.d.ts.map +1 -0
- package/dist/esm/plugins/remove-unused-assets/types.d.ts +52 -0
- package/dist/esm/plugins/remove-unused-assets/types.d.ts.map +1 -0
- package/dist/esm/plugins/simplify-paths/algorithm.d.ts +17 -0
- package/dist/esm/plugins/simplify-paths/algorithm.d.ts.map +1 -0
- package/dist/esm/plugins/simplify-paths/index.d.ts +16 -0
- package/dist/esm/plugins/simplify-paths/index.d.ts.map +1 -0
- package/dist/esm/plugins/simplify-paths/types.d.ts +101 -0
- package/dist/esm/plugins/simplify-paths/types.d.ts.map +1 -0
- package/dist/esm/testing/comparison-tool.d.ts +60 -0
- package/dist/esm/testing/comparison-tool.d.ts.map +1 -0
- package/dist/esm/types/index.d.ts +175 -0
- package/dist/esm/types/index.d.ts.map +1 -0
- package/dist/esm/types/plugin-common.d.ts +91 -0
- package/dist/esm/types/plugin-common.d.ts.map +1 -0
- package/dist/esm/utils/config-validator.d.ts +19 -0
- package/dist/esm/utils/config-validator.d.ts.map +1 -0
- package/dist/esm/utils/logger.d.ts +4 -0
- package/dist/esm/utils/logger.d.ts.map +1 -0
- package/dist/esm/utils/nos-client.d.ts +55 -0
- package/dist/esm/utils/nos-client.d.ts.map +1 -0
- package/dist/esm/utils/size.d.ts +5 -0
- package/dist/esm/utils/size.d.ts.map +1 -0
- package/dist/esm/validation/index.d.ts +67 -0
- package/dist/esm/validation/index.d.ts.map +1 -0
- package/dist/esm/validation/visual-validator.d.ts +61 -0
- package/dist/esm/validation/visual-validator.d.ts.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,3162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, promises, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { basename, dirname, join, resolve as external_path_resolve } from "path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import node_fs from "node:fs";
|
|
8
|
+
import node_path from "node:path";
|
|
9
|
+
import node_fetch from "node-fetch";
|
|
10
|
+
import node_os from "node:os";
|
|
11
|
+
import nconf from "nconf";
|
|
12
|
+
var __webpack_modules__ = {
|
|
13
|
+
sharp: function(module) {
|
|
14
|
+
module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("sharp");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var __webpack_module_cache__ = {};
|
|
18
|
+
function __webpack_require__(moduleId) {
|
|
19
|
+
var cachedModule = __webpack_module_cache__[moduleId];
|
|
20
|
+
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
21
|
+
var module = __webpack_module_cache__[moduleId] = {
|
|
22
|
+
exports: {}
|
|
23
|
+
};
|
|
24
|
+
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
25
|
+
return module.exports;
|
|
26
|
+
}
|
|
27
|
+
(()=>{
|
|
28
|
+
__webpack_require__.n = (module)=>{
|
|
29
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
30
|
+
__webpack_require__.d(getter, {
|
|
31
|
+
a: getter
|
|
32
|
+
});
|
|
33
|
+
return getter;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
(()=>{
|
|
37
|
+
__webpack_require__.d = (exports, definition)=>{
|
|
38
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) Object.defineProperty(exports, key, {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
get: definition[key]
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
(()=>{
|
|
45
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
46
|
+
})();
|
|
47
|
+
function createLogger(verbose = false) {
|
|
48
|
+
return {
|
|
49
|
+
debug: (message)=>{
|
|
50
|
+
if (verbose) console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
51
|
+
},
|
|
52
|
+
info: (message)=>{
|
|
53
|
+
console.log(chalk.blue(`[INFO] ${message}`));
|
|
54
|
+
},
|
|
55
|
+
warn: (message)=>{
|
|
56
|
+
console.log(chalk.yellow(`[WARN] ${message}`));
|
|
57
|
+
},
|
|
58
|
+
error: (message)=>{
|
|
59
|
+
console.log(chalk.red(`[ERROR] ${message}`));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function calculateSize(data) {
|
|
64
|
+
return Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
65
|
+
}
|
|
66
|
+
function formatSize(bytes) {
|
|
67
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
68
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
69
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
70
|
+
}
|
|
71
|
+
function computeReferencedAssetIds(json, opts) {
|
|
72
|
+
const assets = Array.isArray(json.assets) ? json.assets : [];
|
|
73
|
+
const rootLayers = Array.isArray(json.layers) ? json.layers : [];
|
|
74
|
+
const byId = new Map();
|
|
75
|
+
for (const a of assets)if (a && "string" == typeof a.id) byId.set(a.id, a);
|
|
76
|
+
const used = new Set();
|
|
77
|
+
const visitedPrecompIds = new Set();
|
|
78
|
+
const unknownRefs = new Set();
|
|
79
|
+
const queue = [
|
|
80
|
+
...rootLayers
|
|
81
|
+
];
|
|
82
|
+
while(queue.length > 0){
|
|
83
|
+
const layer = queue.shift();
|
|
84
|
+
const ty = layer.ty;
|
|
85
|
+
const refId = layer.refId;
|
|
86
|
+
if ((0 === ty || 2 === ty) && "string" == typeof refId && refId) if (byId.has(refId)) {
|
|
87
|
+
used.add(refId);
|
|
88
|
+
const asset = byId.get(refId);
|
|
89
|
+
if (Array.isArray(asset.layers) && asset.layers.length > 0) {
|
|
90
|
+
if (!visitedPrecompIds.has(refId)) {
|
|
91
|
+
visitedPrecompIds.add(refId);
|
|
92
|
+
for (const sub of asset.layers)queue.push(sub);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else unknownRefs.add(refId);
|
|
96
|
+
}
|
|
97
|
+
if (opts.preserveAssetIds && opts.preserveAssetIds.length > 0) for (const id of opts.preserveAssetIds)used.add(id);
|
|
98
|
+
return {
|
|
99
|
+
used,
|
|
100
|
+
unknownRefs
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function removeUnusedAssets(input, options = {}) {
|
|
104
|
+
const opts = {
|
|
105
|
+
preserveAssetIds: options.preserveAssetIds ?? [],
|
|
106
|
+
keepAssetsWithoutId: options.keepAssetsWithoutId ?? true,
|
|
107
|
+
keepUnreferencedPrecomps: options.keepUnreferencedPrecomps ?? false,
|
|
108
|
+
onLog: options.onLog ?? (()=>{})
|
|
109
|
+
};
|
|
110
|
+
const assets = Array.isArray(input.assets) ? input.assets : [];
|
|
111
|
+
const originalAssetCount = assets.length;
|
|
112
|
+
if (0 === originalAssetCount) return {
|
|
113
|
+
data: input,
|
|
114
|
+
stats: {
|
|
115
|
+
originalAssetCount,
|
|
116
|
+
keptAssetCount: 0,
|
|
117
|
+
removedAssetCount: 0,
|
|
118
|
+
unknownRefIds: [],
|
|
119
|
+
referencedAssetIds: [],
|
|
120
|
+
removedAssetIds: []
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const { used, unknownRefs } = computeReferencedAssetIds(input, opts);
|
|
124
|
+
const byId = new Map();
|
|
125
|
+
for (const a of assets)if (a && "string" == typeof a.id) byId.set(a.id, a);
|
|
126
|
+
const preservedIdSet = new Set(opts.preserveAssetIds);
|
|
127
|
+
const kept = [];
|
|
128
|
+
const removedIds = [];
|
|
129
|
+
for (const a of assets){
|
|
130
|
+
const id = a?.id;
|
|
131
|
+
if ("string" == typeof id && id) {
|
|
132
|
+
const isPrecomp = Array.isArray(a.layers);
|
|
133
|
+
const isReferenced = used.has(id) || preservedIdSet.has(id);
|
|
134
|
+
if (isReferenced) kept.push(a);
|
|
135
|
+
else if (opts.keepUnreferencedPrecomps && isPrecomp) kept.push(a);
|
|
136
|
+
else removedIds.push(id);
|
|
137
|
+
} else if (opts.keepAssetsWithoutId) kept.push(a);
|
|
138
|
+
else removedIds.push("(no-id)");
|
|
139
|
+
}
|
|
140
|
+
const assetsWithIds = kept.filter((asset)=>'string' == typeof asset?.id);
|
|
141
|
+
const output = {
|
|
142
|
+
...input,
|
|
143
|
+
assets: assetsWithIds
|
|
144
|
+
};
|
|
145
|
+
const stats = {
|
|
146
|
+
originalAssetCount,
|
|
147
|
+
keptAssetCount: kept.length,
|
|
148
|
+
removedAssetCount: originalAssetCount - kept.length,
|
|
149
|
+
unknownRefIds: Array.from(unknownRefs),
|
|
150
|
+
referencedAssetIds: Array.from(used),
|
|
151
|
+
removedAssetIds: removedIds
|
|
152
|
+
};
|
|
153
|
+
if (stats.unknownRefIds.length > 0) opts.onLog(`[remove-unused-assets] Warning: ${stats.unknownRefIds.length} unknown refId(s) found in layers: ${stats.unknownRefIds.join(", ")}`);
|
|
154
|
+
opts.onLog(`[remove-unused-assets] assets: ${originalAssetCount} \u{2192} ${kept.length} (removed ${stats.removedAssetCount})`);
|
|
155
|
+
return {
|
|
156
|
+
data: output,
|
|
157
|
+
stats
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const removeUnusedAssetsPlugin = {
|
|
161
|
+
name: 'remove-unused-assets',
|
|
162
|
+
description: 'Remove assets that are not (transitively) referenced by any layers using BFS traversal',
|
|
163
|
+
version: '2.0.0',
|
|
164
|
+
defaultOptions: {
|
|
165
|
+
preserveAssetIds: [],
|
|
166
|
+
keepAssetsWithoutId: true,
|
|
167
|
+
keepUnreferencedPrecomps: false
|
|
168
|
+
},
|
|
169
|
+
async apply (data, options, context) {
|
|
170
|
+
const sizeBefore = calculateSize(data);
|
|
171
|
+
const pluginOptions = {
|
|
172
|
+
...options,
|
|
173
|
+
onLog: context.logger?.info || (()=>{})
|
|
174
|
+
};
|
|
175
|
+
const result = removeUnusedAssets(data, pluginOptions);
|
|
176
|
+
const sizeAfter = calculateSize(result.data);
|
|
177
|
+
return {
|
|
178
|
+
data: result.data,
|
|
179
|
+
changed: result.stats.removedAssetCount > 0,
|
|
180
|
+
stats: {
|
|
181
|
+
itemsProcessed: result.stats.originalAssetCount,
|
|
182
|
+
itemsRemoved: result.stats.removedAssetCount,
|
|
183
|
+
sizeBefore,
|
|
184
|
+
sizeAfter
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function add(a, b) {
|
|
190
|
+
return [
|
|
191
|
+
a[0] + b[0],
|
|
192
|
+
a[1] + b[1]
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
function algorithm_sub(a, b) {
|
|
196
|
+
return [
|
|
197
|
+
a[0] - b[0],
|
|
198
|
+
a[1] - b[1]
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
function mul(a, s) {
|
|
202
|
+
return [
|
|
203
|
+
a[0] * s,
|
|
204
|
+
a[1] * s
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
function dot(a, b) {
|
|
208
|
+
return a[0] * b[0] + a[1] * b[1];
|
|
209
|
+
}
|
|
210
|
+
function len(a) {
|
|
211
|
+
return Math.hypot(a[0], a[1]);
|
|
212
|
+
}
|
|
213
|
+
function dist(a, b) {
|
|
214
|
+
return len(algorithm_sub(a, b));
|
|
215
|
+
}
|
|
216
|
+
function clamp01(x) {
|
|
217
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
218
|
+
}
|
|
219
|
+
function cubicBezier(p0, p1, p2, p3, t) {
|
|
220
|
+
const u = 1 - t;
|
|
221
|
+
const tt = t * t;
|
|
222
|
+
const uu = u * u;
|
|
223
|
+
const uuu = uu * u;
|
|
224
|
+
const ttt = tt * t;
|
|
225
|
+
const a = mul(p0, uuu);
|
|
226
|
+
const b = mul(p1, 3 * uu * t);
|
|
227
|
+
const c = mul(p2, 3 * u * tt);
|
|
228
|
+
const d = mul(p3, ttt);
|
|
229
|
+
return add(add(a, b), add(c, d));
|
|
230
|
+
}
|
|
231
|
+
function bboxOfVertices(vs) {
|
|
232
|
+
let minx = 1 / 0, miny = 1 / 0, maxx = -1 / 0, maxy = -1 / 0;
|
|
233
|
+
for (const [x, y] of vs){
|
|
234
|
+
if (x < minx) minx = x;
|
|
235
|
+
if (y < miny) miny = y;
|
|
236
|
+
if (x > maxx) maxx = x;
|
|
237
|
+
if (y > maxy) maxy = y;
|
|
238
|
+
}
|
|
239
|
+
const diag = Math.hypot(maxx - minx, maxy - miny) || 1;
|
|
240
|
+
return {
|
|
241
|
+
min: [
|
|
242
|
+
minx,
|
|
243
|
+
miny
|
|
244
|
+
],
|
|
245
|
+
max: [
|
|
246
|
+
maxx,
|
|
247
|
+
maxy
|
|
248
|
+
],
|
|
249
|
+
diag
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function angleDeg(a, b, c) {
|
|
253
|
+
const v1 = algorithm_sub(a, b);
|
|
254
|
+
const v2 = algorithm_sub(c, b);
|
|
255
|
+
const l1 = len(v1) || 1;
|
|
256
|
+
const l2 = len(v2) || 1;
|
|
257
|
+
let cos = 2 * clamp01((dot(v1, v2) / (l1 * l2) + 1) / 2) - 1;
|
|
258
|
+
const ang = Math.acos(Math.max(-1, Math.min(1, cos)));
|
|
259
|
+
return 180 * ang / Math.PI;
|
|
260
|
+
}
|
|
261
|
+
function modWrap(i, n) {
|
|
262
|
+
return (i % n + n) % n;
|
|
263
|
+
}
|
|
264
|
+
function catmullRomHandles(v, closed, tension = 1.0) {
|
|
265
|
+
const n = v.length;
|
|
266
|
+
const Ii = new Array(n);
|
|
267
|
+
const Oo = new Array(n);
|
|
268
|
+
const get = (idx)=>{
|
|
269
|
+
if (closed) return v[modWrap(idx, n)];
|
|
270
|
+
if (idx < 0) return v[0];
|
|
271
|
+
if (idx >= n) return v[n - 1];
|
|
272
|
+
return v[idx];
|
|
273
|
+
};
|
|
274
|
+
for(let k = 0; k < n; k++){
|
|
275
|
+
const p0 = get(k - 1);
|
|
276
|
+
const p1 = get(k);
|
|
277
|
+
const p2 = get(k + 1);
|
|
278
|
+
const p3 = get(k + 2);
|
|
279
|
+
const b1 = add(p1, mul(algorithm_sub(p2, p0), tension / 6));
|
|
280
|
+
const b2 = algorithm_sub(p2, mul(algorithm_sub(p3, p1), tension / 6));
|
|
281
|
+
Oo[k] = algorithm_sub(b1, p1);
|
|
282
|
+
Ii[modWrap(k + 1, n)] = algorithm_sub(b2, p2);
|
|
283
|
+
}
|
|
284
|
+
if (!closed) {
|
|
285
|
+
if (!Ii[0]) Ii[0] = [
|
|
286
|
+
0,
|
|
287
|
+
0
|
|
288
|
+
];
|
|
289
|
+
if (!Oo[n - 1]) Oo[n - 1] = [
|
|
290
|
+
0,
|
|
291
|
+
0
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
for(let k = 0; k < n; k++){
|
|
295
|
+
if (!Ii[k]) Ii[k] = [
|
|
296
|
+
0,
|
|
297
|
+
0
|
|
298
|
+
];
|
|
299
|
+
if (!Oo[k]) Oo[k] = [
|
|
300
|
+
0,
|
|
301
|
+
0
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
i: Ii,
|
|
306
|
+
o: Oo
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function perpendicularDistance(p, a, b) {
|
|
310
|
+
const ab = algorithm_sub(b, a);
|
|
311
|
+
const t = Math.max(0, Math.min(1, dot(algorithm_sub(p, a), ab) / (dot(ab, ab) || 1)));
|
|
312
|
+
const proj = add(a, mul(ab, t));
|
|
313
|
+
return dist(p, proj);
|
|
314
|
+
}
|
|
315
|
+
function rdpIndices(points, epsilon) {
|
|
316
|
+
if (points.length <= 2) return points.map((_, idx)=>idx);
|
|
317
|
+
const keep = new Array(points.length).fill(false);
|
|
318
|
+
keep[0] = true;
|
|
319
|
+
keep[points.length - 1] = true;
|
|
320
|
+
function recurse(start, end) {
|
|
321
|
+
let maxDist = -1;
|
|
322
|
+
let index = -1;
|
|
323
|
+
const a = points[start];
|
|
324
|
+
const b = points[end];
|
|
325
|
+
for(let i = start + 1; i < end; i++){
|
|
326
|
+
const d = perpendicularDistance(points[i], a, b);
|
|
327
|
+
if (d > maxDist) {
|
|
328
|
+
maxDist = d;
|
|
329
|
+
index = i;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (maxDist > epsilon && index >= 0) {
|
|
333
|
+
keep[index] = true;
|
|
334
|
+
recurse(start, index);
|
|
335
|
+
recurse(index, end);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
recurse(0, points.length - 1);
|
|
339
|
+
const out = [];
|
|
340
|
+
for(let i = 0; i < keep.length; i++)if (keep[i]) out.push(i);
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function rdpClosed(points, epsilon, mustKeep) {
|
|
344
|
+
if (points.length <= 2) return points.map((_, idx)=>idx);
|
|
345
|
+
const pts = points.slice();
|
|
346
|
+
pts.push(points[0]);
|
|
347
|
+
let kept = new Set(rdpIndices(pts, epsilon));
|
|
348
|
+
if (kept.has(pts.length - 1)) kept.delete(pts.length - 1);
|
|
349
|
+
if (mustKeep && mustKeep.size) for (const i of mustKeep)kept.add(i);
|
|
350
|
+
return Array.from(kept).sort((a, b)=>a - b);
|
|
351
|
+
}
|
|
352
|
+
function sampleShape(path, samplesPerSeg = 10) {
|
|
353
|
+
const { v, i, o, c } = path;
|
|
354
|
+
const n = v.length;
|
|
355
|
+
if (0 === n) return [];
|
|
356
|
+
const pts = [];
|
|
357
|
+
const segs = c ? n : n - 1;
|
|
358
|
+
for(let k = 0; k < segs; k++){
|
|
359
|
+
const a = v[k];
|
|
360
|
+
const b = v[modWrap(k + 1, n)];
|
|
361
|
+
const cp1 = add(v[k], o[k] || [
|
|
362
|
+
0,
|
|
363
|
+
0
|
|
364
|
+
]);
|
|
365
|
+
const cp2 = add(v[modWrap(k + 1, n)], i[modWrap(k + 1, n)] || [
|
|
366
|
+
0,
|
|
367
|
+
0
|
|
368
|
+
]);
|
|
369
|
+
const steps = Math.max(2, samplesPerSeg);
|
|
370
|
+
for(let s = 0; s <= steps; s++){
|
|
371
|
+
const t = s / steps;
|
|
372
|
+
pts.push(cubicBezier(a, cp1, cp2, b, t));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return pts;
|
|
376
|
+
}
|
|
377
|
+
function maxHausdorffLikeError(a, b, samplesPerSeg) {
|
|
378
|
+
const sa = sampleShape(a, samplesPerSeg);
|
|
379
|
+
const sb = sampleShape(b, samplesPerSeg);
|
|
380
|
+
const m = Math.max(sa.length, sb.length);
|
|
381
|
+
let maxErr = 0;
|
|
382
|
+
for(let i = 0; i < m; i++){
|
|
383
|
+
const pa = sa[i % sa.length];
|
|
384
|
+
const pb = sb[i % sb.length];
|
|
385
|
+
const d = dist(pa, pb);
|
|
386
|
+
if (d > maxErr) maxErr = d;
|
|
387
|
+
}
|
|
388
|
+
return maxErr;
|
|
389
|
+
}
|
|
390
|
+
function computeCornerSet(v, closed, angleDegThreshold) {
|
|
391
|
+
const n = v.length;
|
|
392
|
+
const keep = new Set();
|
|
393
|
+
for(let k = 0; k < n; k++){
|
|
394
|
+
const a = v[modWrap(k - 1, n)];
|
|
395
|
+
const b = v[k];
|
|
396
|
+
const c = v[modWrap(k + 1, n)];
|
|
397
|
+
let ang = angleDeg(a, b, c);
|
|
398
|
+
if (!closed && (0 === k || k === n - 1)) {
|
|
399
|
+
keep.add(k);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (ang < angleDegThreshold) keep.add(k);
|
|
403
|
+
}
|
|
404
|
+
return keep;
|
|
405
|
+
}
|
|
406
|
+
function simplifyStaticPath(inPath, opts) {
|
|
407
|
+
const { v, c } = inPath;
|
|
408
|
+
if (!v || v.length < opts.minPoints) return inPath;
|
|
409
|
+
const { diag } = bboxOfVertices(v);
|
|
410
|
+
const tolAbs = opts.relativeTolerance ? opts.tolerance * diag : opts.tolerance;
|
|
411
|
+
const cornerSet = opts.preserveCorners ? computeCornerSet(v, c, opts.cornerAngleDeg) : new Set();
|
|
412
|
+
const mustKeep = new Set(cornerSet);
|
|
413
|
+
if (!c) {
|
|
414
|
+
mustKeep.add(0);
|
|
415
|
+
mustKeep.add(v.length - 1);
|
|
416
|
+
}
|
|
417
|
+
const keptIdx = c ? rdpClosed(v, tolAbs, mustKeep) : Array.from(new Set([
|
|
418
|
+
...rdpIndices(v, tolAbs),
|
|
419
|
+
...mustKeep
|
|
420
|
+
])).sort((a, b)=>a - b);
|
|
421
|
+
if (keptIdx.length < opts.minPoints) return inPath;
|
|
422
|
+
const v2 = keptIdx.map((i)=>inPath.v[i]);
|
|
423
|
+
const { i: i2, o: o2 } = catmullRomHandles(v2, c, opts.tension);
|
|
424
|
+
const out = {
|
|
425
|
+
v: v2,
|
|
426
|
+
i: i2,
|
|
427
|
+
o: o2,
|
|
428
|
+
c
|
|
429
|
+
};
|
|
430
|
+
const err = maxHausdorffLikeError(inPath, out, opts.samplesPerSegment);
|
|
431
|
+
if (err > tolAbs) return inPath;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function tryRemoveIndexAcrossFrames(frames, j, minPoints, tolAbsPerFrame, samplesPerSeg, tension) {
|
|
435
|
+
const n = frames[0].v.length;
|
|
436
|
+
if (n <= minPoints) return {
|
|
437
|
+
ok: false
|
|
438
|
+
};
|
|
439
|
+
const outFrames = [];
|
|
440
|
+
for (const f of frames){
|
|
441
|
+
const { v, c } = f;
|
|
442
|
+
const nNow = v.length;
|
|
443
|
+
if (nNow !== n) return {
|
|
444
|
+
ok: false
|
|
445
|
+
};
|
|
446
|
+
if (nNow - 1 < minPoints) return {
|
|
447
|
+
ok: false
|
|
448
|
+
};
|
|
449
|
+
const keptIdx = [];
|
|
450
|
+
for(let k = 0; k < nNow; k++)if (k !== j) keptIdx.push(k);
|
|
451
|
+
const v2 = keptIdx.map((i)=>f.v[i]);
|
|
452
|
+
const { i: i2, o: o2 } = catmullRomHandles(v2, c, tension);
|
|
453
|
+
const candidate = {
|
|
454
|
+
v: v2,
|
|
455
|
+
i: i2,
|
|
456
|
+
o: o2,
|
|
457
|
+
c
|
|
458
|
+
};
|
|
459
|
+
const err = maxHausdorffLikeError(f, candidate, samplesPerSeg);
|
|
460
|
+
if (err > tolAbsPerFrame) return {
|
|
461
|
+
ok: false
|
|
462
|
+
};
|
|
463
|
+
outFrames.push(candidate);
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
framesOut: outFrames
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function simplifyAnimatedConsistent(keyframes, opts) {
|
|
471
|
+
const frames = keyframes.map((kf)=>kf.s && kf.s[0] ? kf.s[0] : null).filter(Boolean);
|
|
472
|
+
if (0 === frames.length) return keyframes;
|
|
473
|
+
const n0 = frames[0].v.length;
|
|
474
|
+
const closed0 = frames[0].c;
|
|
475
|
+
for (const f of frames)if (f.v.length !== n0 || f.c !== closed0) return keyframes;
|
|
476
|
+
if (n0 < opts.minPoints) return keyframes;
|
|
477
|
+
const tolAbsPerFrame = frames.map((f)=>{
|
|
478
|
+
const { diag } = bboxOfVertices(f.v);
|
|
479
|
+
return opts.relativeTolerance ? opts.tolerance * diag : opts.tolerance;
|
|
480
|
+
});
|
|
481
|
+
const cornerUnion = new Set();
|
|
482
|
+
if (opts.preserveCorners) for (const f of frames){
|
|
483
|
+
const cs = computeCornerSet(f.v, f.c, opts.cornerAngleDeg);
|
|
484
|
+
cs.forEach((i)=>cornerUnion.add(i));
|
|
485
|
+
}
|
|
486
|
+
if (!closed0) {
|
|
487
|
+
cornerUnion.add(0);
|
|
488
|
+
cornerUnion.add(n0 - 1);
|
|
489
|
+
}
|
|
490
|
+
function importanceAt(j) {
|
|
491
|
+
let sum = 0;
|
|
492
|
+
for(let idx = 0; idx < frames.length; idx++){
|
|
493
|
+
const f = frames[idx];
|
|
494
|
+
const n = f.v.length;
|
|
495
|
+
const a = f.v[modWrap(j - 1, n)];
|
|
496
|
+
const b = f.v[modWrap(j, n)];
|
|
497
|
+
const c = f.v[modWrap(j + 1, n)];
|
|
498
|
+
sum += perpendicularDistance(b, a, c);
|
|
499
|
+
}
|
|
500
|
+
return sum / frames.length;
|
|
501
|
+
}
|
|
502
|
+
let removable = [];
|
|
503
|
+
for(let j = 0; j < n0; j++)if (!cornerUnion.has(j)) removable.push(j);
|
|
504
|
+
removable.sort((a, b)=>importanceAt(a) - importanceAt(b));
|
|
505
|
+
let currentFrames = frames.map((f)=>({
|
|
506
|
+
v: f.v.slice(),
|
|
507
|
+
i: f.i.slice(),
|
|
508
|
+
o: f.o.slice(),
|
|
509
|
+
c: f.c
|
|
510
|
+
}));
|
|
511
|
+
for (const j0 of removable){
|
|
512
|
+
const nNow = currentFrames[0].v.length;
|
|
513
|
+
if (nNow <= opts.minPoints) break;
|
|
514
|
+
const targetPos = frames[0].v[j0];
|
|
515
|
+
let jCurr = -1;
|
|
516
|
+
let best = 1 / 0;
|
|
517
|
+
for(let j = 0; j < currentFrames[0].v.length; j++){
|
|
518
|
+
const d = dist(currentFrames[0].v[j], targetPos);
|
|
519
|
+
if (d < best) {
|
|
520
|
+
best = d;
|
|
521
|
+
jCurr = j;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (jCurr < 0 || 0 === jCurr || jCurr === currentFrames[0].v.length - 1 && !closed0) continue;
|
|
525
|
+
const trial = tryRemoveIndexAcrossFrames(currentFrames, jCurr, opts.minPoints, Math.max(...tolAbsPerFrame), opts.samplesPerSegment, opts.tension);
|
|
526
|
+
if (trial.ok && trial.framesOut) currentFrames = trial.framesOut;
|
|
527
|
+
}
|
|
528
|
+
let idx = 0;
|
|
529
|
+
for (const kf of keyframes)if (kf.s && kf.s[0]) {
|
|
530
|
+
const f = currentFrames[idx++];
|
|
531
|
+
kf.s = [
|
|
532
|
+
f
|
|
533
|
+
];
|
|
534
|
+
if (kf.e && kf.e[0]) kf.e = [
|
|
535
|
+
f
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
return keyframes;
|
|
539
|
+
}
|
|
540
|
+
function traverseShapes(items, fn, path = []) {
|
|
541
|
+
if (!items) return;
|
|
542
|
+
items.forEach((it, idx)=>{
|
|
543
|
+
const p = path.concat(idx);
|
|
544
|
+
if (it && "gr" === it.ty) traverseShapes(it.it, fn, p);
|
|
545
|
+
else if (it && "sh" === it.ty && it.ks) fn(it, p);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function simplifyPaths(input, options = {}) {
|
|
549
|
+
const opts = {
|
|
550
|
+
mode: options.mode ?? "static-only",
|
|
551
|
+
tolerance: options.tolerance ?? 1.0,
|
|
552
|
+
relativeTolerance: options.relativeTolerance ?? false,
|
|
553
|
+
minPoints: options.minPoints ?? 6,
|
|
554
|
+
preserveCorners: options.preserveCorners ?? true,
|
|
555
|
+
cornerAngleDeg: options.cornerAngleDeg ?? 150,
|
|
556
|
+
algorithm: "rdp",
|
|
557
|
+
samplesPerSegment: options.samplesPerSegment ?? 10,
|
|
558
|
+
tension: options.tension ?? 1.0,
|
|
559
|
+
onLog: options.onLog ?? (()=>{})
|
|
560
|
+
};
|
|
561
|
+
const out = {
|
|
562
|
+
...input,
|
|
563
|
+
layers: input.layers ? JSON.parse(JSON.stringify(input.layers)) : []
|
|
564
|
+
};
|
|
565
|
+
let totalPaths = 0;
|
|
566
|
+
let simplifiedPaths = 0;
|
|
567
|
+
let staticSimplified = 0;
|
|
568
|
+
let animatedSimplified = 0;
|
|
569
|
+
let totalBefore = 0;
|
|
570
|
+
let totalAfter = 0;
|
|
571
|
+
const warnings = [];
|
|
572
|
+
for (const layer of out.layers || [])if (4 === layer.ty) traverseShapes(layer.shapes, (node)=>{
|
|
573
|
+
const ks = node.ks;
|
|
574
|
+
if (!ks || !ks.k) return;
|
|
575
|
+
if (("static-only" === opts.mode || "all" === opts.mode) && 0 === ks.a) {
|
|
576
|
+
const path = ks.k;
|
|
577
|
+
totalPaths++;
|
|
578
|
+
totalBefore += path.v.length;
|
|
579
|
+
const newPath = simplifyStaticPath(path, opts);
|
|
580
|
+
node.ks.k = newPath;
|
|
581
|
+
totalAfter += newPath.v.length;
|
|
582
|
+
if (newPath !== path) {
|
|
583
|
+
simplifiedPaths++;
|
|
584
|
+
staticSimplified++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (("animated-consistent" === opts.mode || "all" === opts.mode) && 1 === ks.a) {
|
|
588
|
+
const arr = ks.k;
|
|
589
|
+
if (!Array.isArray(arr) || arr.length < 2) return;
|
|
590
|
+
if (arr.some((kf)=>!kf.s || !kf.s[0])) return void warnings.push("animated path with missing s[0] skipped");
|
|
591
|
+
totalPaths++;
|
|
592
|
+
const beforeN = arr[0].s[0].v.length;
|
|
593
|
+
totalBefore += beforeN;
|
|
594
|
+
const newArr = simplifyAnimatedConsistent(arr, opts);
|
|
595
|
+
node.ks.k = newArr;
|
|
596
|
+
const afterN = newArr[0].s[0].v.length;
|
|
597
|
+
totalAfter += afterN;
|
|
598
|
+
if (afterN < beforeN) {
|
|
599
|
+
simplifiedPaths++;
|
|
600
|
+
animatedSimplified++;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const avgReduction = totalBefore > 0 ? (totalBefore - totalAfter) / totalBefore * 100 : 0;
|
|
605
|
+
if (warnings.length) warnings.forEach((w)=>opts.onLog(`[simplify-paths] ${w}`));
|
|
606
|
+
opts.onLog(`[simplify-paths] paths: ${totalPaths}, simplified: ${simplifiedPaths} (static ${staticSimplified}, animated ${animatedSimplified}), avg vertex reduction: ${avgReduction.toFixed(2)}%`);
|
|
607
|
+
return {
|
|
608
|
+
data: out,
|
|
609
|
+
stats: {
|
|
610
|
+
totalPaths,
|
|
611
|
+
simplifiedPaths,
|
|
612
|
+
staticSimplified,
|
|
613
|
+
animatedSimplified,
|
|
614
|
+
avgVertexReduction: Number(avgReduction.toFixed(2)),
|
|
615
|
+
warnings
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const simplifyPathsPlugin = {
|
|
620
|
+
name: 'simplify-paths',
|
|
621
|
+
description: 'Simplify Lottie path data using RDP algorithm with Catmull-Rom handle regeneration',
|
|
622
|
+
version: '1.0.0',
|
|
623
|
+
defaultOptions: {
|
|
624
|
+
mode: 'static-only',
|
|
625
|
+
tolerance: 1.0,
|
|
626
|
+
relativeTolerance: false,
|
|
627
|
+
minPoints: 6,
|
|
628
|
+
preserveCorners: true,
|
|
629
|
+
cornerAngleDeg: 150,
|
|
630
|
+
algorithm: 'rdp',
|
|
631
|
+
samplesPerSegment: 10,
|
|
632
|
+
tension: 1.0
|
|
633
|
+
},
|
|
634
|
+
async apply (data, options, context) {
|
|
635
|
+
const sizeBefore = calculateSize(data);
|
|
636
|
+
let logMessages = [];
|
|
637
|
+
const onLog = (msg)=>{
|
|
638
|
+
if (context.logger) context.logger.debug(msg);
|
|
639
|
+
logMessages.push(msg);
|
|
640
|
+
};
|
|
641
|
+
const mergedOptions = {
|
|
642
|
+
...options,
|
|
643
|
+
onLog
|
|
644
|
+
};
|
|
645
|
+
const result = simplifyPaths(data, mergedOptions);
|
|
646
|
+
const sizeAfter = calculateSize(result.data);
|
|
647
|
+
return {
|
|
648
|
+
data: result.data,
|
|
649
|
+
changed: result.stats.simplifiedPaths > 0,
|
|
650
|
+
stats: {
|
|
651
|
+
itemsProcessed: result.stats.totalPaths,
|
|
652
|
+
itemsRemoved: result.stats.totalPaths - result.stats.simplifiedPaths,
|
|
653
|
+
sizeBefore,
|
|
654
|
+
sizeAfter
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
let lastRequestTime = 0;
|
|
660
|
+
const MIN_REQUEST_INTERVAL = 2000;
|
|
661
|
+
let isServiceUnavailable = false;
|
|
662
|
+
function sleep(ms) {
|
|
663
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
664
|
+
}
|
|
665
|
+
function getRandomIP() {
|
|
666
|
+
return Array.from(Array(4)).map(()=>Math.floor(255 * Math.random()).toString()).join('.');
|
|
667
|
+
}
|
|
668
|
+
async function downloadImg(url) {
|
|
669
|
+
const response = await node_fetch(url);
|
|
670
|
+
if (!response.ok) throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
|
|
671
|
+
const buffer = await response.arrayBuffer();
|
|
672
|
+
return Buffer.from(buffer);
|
|
673
|
+
}
|
|
674
|
+
async function compress(file) {
|
|
675
|
+
if (isServiceUnavailable) throw new Error('TinyPNG service is temporarily unavailable (marked as failed)');
|
|
676
|
+
async function attemptRequest() {
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
const timeSinceLastRequest = now - lastRequestTime;
|
|
679
|
+
if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) {
|
|
680
|
+
const waitTime = MIN_REQUEST_INTERVAL - timeSinceLastRequest;
|
|
681
|
+
await sleep(waitTime);
|
|
682
|
+
}
|
|
683
|
+
lastRequestTime = Date.now();
|
|
684
|
+
const response = await node_fetch('https://tinify.cn/backend/opt/shrink', {
|
|
685
|
+
method: 'POST',
|
|
686
|
+
headers: {
|
|
687
|
+
'Cache-Control': 'no-cache',
|
|
688
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
689
|
+
'X-Forwarded-For': getRandomIP(),
|
|
690
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
|
|
691
|
+
Referer: 'https://tinify.cn',
|
|
692
|
+
Origin: 'https://tinify.cn'
|
|
693
|
+
},
|
|
694
|
+
body: file
|
|
695
|
+
});
|
|
696
|
+
if (429 === response.status) throw new Error(`Rate limited (429): ${response.statusText}`);
|
|
697
|
+
if (!response.ok) throw new Error(`TinyPNG compression failed: ${response.status} ${response.statusText}`);
|
|
698
|
+
const result = await response.json();
|
|
699
|
+
if (!result?.output?.url) throw new Error('TinyPNG response missing download URL');
|
|
700
|
+
const compressedBuffer = await downloadImg(result.output.url);
|
|
701
|
+
return [
|
|
702
|
+
compressedBuffer,
|
|
703
|
+
'png'
|
|
704
|
+
];
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
return await attemptRequest();
|
|
708
|
+
} catch (error) {
|
|
709
|
+
const errorMessage = error.message;
|
|
710
|
+
if (errorMessage.includes('Rate limited (429)')) {
|
|
711
|
+
console.log('TinyPNG rate limited, waiting 5s before retry...');
|
|
712
|
+
await sleep(5000);
|
|
713
|
+
try {
|
|
714
|
+
return await attemptRequest();
|
|
715
|
+
} catch (retryError) {
|
|
716
|
+
isServiceUnavailable = true;
|
|
717
|
+
console.log('TinyPNG service marked as unavailable after retry failure');
|
|
718
|
+
throw new Error(`TinyPNG failed after retry: ${retryError.message}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
let NosClient = null;
|
|
725
|
+
async function loadNosClient() {
|
|
726
|
+
if (NosClient) return NosClient;
|
|
727
|
+
try {
|
|
728
|
+
const sdk = await import("@nos-sdk/nos-node-sdk");
|
|
729
|
+
NosClient = sdk.NosClient;
|
|
730
|
+
return NosClient;
|
|
731
|
+
} catch (error) {
|
|
732
|
+
throw new Error("NOS SDK \u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u5B89\u88C5 @nos-sdk/nos-node-sdk");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const NOS_CONF_PATH = node_path.resolve(node_os.homedir(), ".nos-conf", "config.json");
|
|
736
|
+
class NOSConf {
|
|
737
|
+
constructor(){
|
|
738
|
+
this.nconf = nconf;
|
|
739
|
+
this.nconf.env().file({
|
|
740
|
+
file: NOS_CONF_PATH
|
|
741
|
+
});
|
|
742
|
+
NOSConf.init();
|
|
743
|
+
}
|
|
744
|
+
static init() {
|
|
745
|
+
const dirPath = node_path.dirname(NOS_CONF_PATH);
|
|
746
|
+
node_fs.mkdirSync(dirPath, {
|
|
747
|
+
recursive: true
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
getConfig() {
|
|
751
|
+
return {
|
|
752
|
+
accessKey: this.nconf.get("accessKey"),
|
|
753
|
+
accessSecret: this.nconf.get("accessSecret"),
|
|
754
|
+
endpoint: this.nconf.get("endpoint"),
|
|
755
|
+
defaultBucket: this.nconf.get("defaultBucket"),
|
|
756
|
+
host: this.nconf.get("host"),
|
|
757
|
+
protocol: this.nconf.get("protocol")
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
setConfig(config) {
|
|
761
|
+
if (config.accessKey) this.nconf.set("accessKey", config.accessKey);
|
|
762
|
+
if (config.accessSecret) this.nconf.set("accessSecret", config.accessSecret);
|
|
763
|
+
if (config.endpoint) this.nconf.set("endpoint", config.endpoint);
|
|
764
|
+
if (config.defaultBucket) this.nconf.set("defaultBucket", config.defaultBucket);
|
|
765
|
+
if (config.host) this.nconf.set("host", config.host);
|
|
766
|
+
if (config.protocol) this.nconf.set("protocol", config.protocol);
|
|
767
|
+
this.nconf.save(void 0);
|
|
768
|
+
}
|
|
769
|
+
reset() {
|
|
770
|
+
this.nconf.reset();
|
|
771
|
+
this.nconf.save(void 0);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const nosConf = new NOSConf();
|
|
775
|
+
let client = null;
|
|
776
|
+
const upload = async (file)=>{
|
|
777
|
+
if (!client) {
|
|
778
|
+
const NosClientClass = await loadNosClient();
|
|
779
|
+
client = new NosClientClass(nosConf.getConfig());
|
|
780
|
+
}
|
|
781
|
+
await client.putObject({
|
|
782
|
+
objectKey: file.name,
|
|
783
|
+
body: file.data
|
|
784
|
+
});
|
|
785
|
+
const fileUrl = `${nosConf.getConfig().endpoint}/${file.name}`;
|
|
786
|
+
return fileUrl;
|
|
787
|
+
};
|
|
788
|
+
async function uploadToCDN(buffer, fileName, config, assetId) {
|
|
789
|
+
if (!config.enabled) throw new Error("CDN \u4E0A\u4F20\u672A\u542F\u7528");
|
|
790
|
+
const timestamp = new Date().getTime();
|
|
791
|
+
const finalFileName = config.generateFileName ? config.generateFileName(fileName, assetId, timestamp) : `${config.prefix || "lottie-assets"}/${timestamp}/${fileName}`;
|
|
792
|
+
return await upload({
|
|
793
|
+
data: buffer,
|
|
794
|
+
name: finalFileName
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
var external_sharp_ = __webpack_require__("sharp");
|
|
798
|
+
var external_sharp_default = /*#__PURE__*/ __webpack_require__.n(external_sharp_);
|
|
799
|
+
const external_ssim_js_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("ssim.js");
|
|
800
|
+
var external_ssim_js_default = /*#__PURE__*/ __webpack_require__.n(external_ssim_js_namespaceObject);
|
|
801
|
+
const DEFAULT_LEVEL = "standard";
|
|
802
|
+
const LEVEL_THRESHOLDS = {
|
|
803
|
+
mild: 0.995,
|
|
804
|
+
standard: 0.990,
|
|
805
|
+
aggressive: 0.975
|
|
806
|
+
};
|
|
807
|
+
const PRESETS = {
|
|
808
|
+
mild: {
|
|
809
|
+
webpQ: 90,
|
|
810
|
+
webpNearQ: 85,
|
|
811
|
+
alphaQ: 90,
|
|
812
|
+
pngQ: 100,
|
|
813
|
+
jpegQ: 86
|
|
814
|
+
},
|
|
815
|
+
standard: {
|
|
816
|
+
webpQ: 85,
|
|
817
|
+
webpNearQ: 72,
|
|
818
|
+
alphaQ: 85,
|
|
819
|
+
pngQ: 100,
|
|
820
|
+
jpegQ: 82
|
|
821
|
+
},
|
|
822
|
+
aggressive: {
|
|
823
|
+
webpQ: 70,
|
|
824
|
+
webpNearQ: 68,
|
|
825
|
+
alphaQ: 80,
|
|
826
|
+
pngQ: 100,
|
|
827
|
+
jpegQ: 78
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
async function compressImage(input, options = {}) {
|
|
831
|
+
const level = options.level ?? DEFAULT_LEVEL;
|
|
832
|
+
const threshold = options.ssimThreshold ?? LEVEL_THRESHOLDS[level];
|
|
833
|
+
const allow = new Set(options.allowFormats ?? [
|
|
834
|
+
"png",
|
|
835
|
+
"webp-lossy",
|
|
836
|
+
"webp-near",
|
|
837
|
+
"jpeg"
|
|
838
|
+
]);
|
|
839
|
+
const forceEffort = options.forceEffort;
|
|
840
|
+
const base = external_sharp_default()(input, {
|
|
841
|
+
limitInputPixels: false
|
|
842
|
+
});
|
|
843
|
+
const meta = await base.metadata();
|
|
844
|
+
const hasAlpha = Boolean(meta.hasAlpha || meta.channels && meta.channels >= 4);
|
|
845
|
+
const candidates = await buildCandidates(base, hasAlpha, level, allow, forceEffort);
|
|
846
|
+
const tried = [];
|
|
847
|
+
for (const c of candidates){
|
|
848
|
+
const buf = await c.encode();
|
|
849
|
+
const s = await calcSSIM(input, buf);
|
|
850
|
+
tried.push({
|
|
851
|
+
format: c.outFormat,
|
|
852
|
+
variant: c.variant,
|
|
853
|
+
label: c.label,
|
|
854
|
+
bytes: buf.byteLength,
|
|
855
|
+
ssim: s,
|
|
856
|
+
buffer: buf
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const pick = tried.sort((a, b)=>a.bytes - b.bytes)[0];
|
|
860
|
+
return {
|
|
861
|
+
...pick,
|
|
862
|
+
level,
|
|
863
|
+
threshold,
|
|
864
|
+
hasAlpha,
|
|
865
|
+
tried
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function buildCandidates(base, hasAlpha, level, allow, forceEffort) {
|
|
869
|
+
const list = [];
|
|
870
|
+
const p = PRESETS[level];
|
|
871
|
+
const pngEffort = forceEffort ?? 10;
|
|
872
|
+
const webpEffort = forceEffort ?? 6;
|
|
873
|
+
if (allow.has("png")) list.push({
|
|
874
|
+
variant: "png8",
|
|
875
|
+
outFormat: "png",
|
|
876
|
+
label: `png8 q${p.pngQ} e${pngEffort}`,
|
|
877
|
+
encode: ()=>base.clone().png({
|
|
878
|
+
palette: true,
|
|
879
|
+
quality: p.pngQ,
|
|
880
|
+
compressionLevel: 9,
|
|
881
|
+
effort: pngEffort,
|
|
882
|
+
progressive: false
|
|
883
|
+
}).toBuffer()
|
|
884
|
+
});
|
|
885
|
+
if (allow.has("webp-lossy")) list.push({
|
|
886
|
+
variant: "webp-lossy",
|
|
887
|
+
outFormat: "webp",
|
|
888
|
+
label: `webp-lossy q${p.webpQ}${hasAlpha ? ` aq${p.alphaQ}` : ""} e${webpEffort}`,
|
|
889
|
+
encode: ()=>base.clone().webp({
|
|
890
|
+
quality: p.webpQ,
|
|
891
|
+
alphaQuality: hasAlpha ? p.alphaQ : void 0,
|
|
892
|
+
effort: webpEffort,
|
|
893
|
+
smartSubsample: true
|
|
894
|
+
}).toBuffer()
|
|
895
|
+
});
|
|
896
|
+
if (allow.has("webp-near")) list.push({
|
|
897
|
+
variant: "webp-near",
|
|
898
|
+
outFormat: "webp",
|
|
899
|
+
label: `webp-nearLossless q${p.webpNearQ} e${webpEffort}`,
|
|
900
|
+
encode: ()=>base.clone().webp({
|
|
901
|
+
nearLossless: true,
|
|
902
|
+
quality: p.webpNearQ,
|
|
903
|
+
effort: webpEffort
|
|
904
|
+
}).toBuffer()
|
|
905
|
+
});
|
|
906
|
+
if (!hasAlpha && allow.has("jpeg")) list.push({
|
|
907
|
+
variant: "jpeg",
|
|
908
|
+
outFormat: "jpeg",
|
|
909
|
+
label: `jpeg q${p.jpegQ} mozjpeg`,
|
|
910
|
+
encode: ()=>base.clone().jpeg({
|
|
911
|
+
quality: p.jpegQ,
|
|
912
|
+
mozjpeg: true,
|
|
913
|
+
progressive: true
|
|
914
|
+
}).toBuffer()
|
|
915
|
+
});
|
|
916
|
+
return list;
|
|
917
|
+
}
|
|
918
|
+
async function calcSSIM(orig, cand) {
|
|
919
|
+
const origSharp = external_sharp_default()(orig);
|
|
920
|
+
const candSharp = external_sharp_default()(cand);
|
|
921
|
+
const origMeta = await origSharp.metadata();
|
|
922
|
+
const { width, height } = origMeta;
|
|
923
|
+
if (!width || !height) throw new Error("Unable to get image dimensions");
|
|
924
|
+
const origRawData = await origSharp.resize(width, height).ensureAlpha().raw().toBuffer();
|
|
925
|
+
const candRawData = await candSharp.resize(width, height).ensureAlpha().raw().toBuffer();
|
|
926
|
+
const origImageData = {
|
|
927
|
+
data: new Uint8ClampedArray(origRawData),
|
|
928
|
+
width,
|
|
929
|
+
height
|
|
930
|
+
};
|
|
931
|
+
const candImageData = {
|
|
932
|
+
data: new Uint8ClampedArray(candRawData),
|
|
933
|
+
width,
|
|
934
|
+
height
|
|
935
|
+
};
|
|
936
|
+
const { mssim } = external_ssim_js_default()(origImageData, candImageData, {
|
|
937
|
+
ssim: "original",
|
|
938
|
+
downsample: "original",
|
|
939
|
+
windowSize: 11,
|
|
940
|
+
bitDepth: 8
|
|
941
|
+
});
|
|
942
|
+
return "number" == typeof mssim ? mssim : 0;
|
|
943
|
+
}
|
|
944
|
+
function isDataURL(s) {
|
|
945
|
+
return !!s && /^data:image\/[^;]+;base64,/.test(s);
|
|
946
|
+
}
|
|
947
|
+
function parseDataURL(s) {
|
|
948
|
+
const [h, b] = s.split(",", 2);
|
|
949
|
+
const mime = h.slice(5, h.indexOf(";"));
|
|
950
|
+
return {
|
|
951
|
+
mime,
|
|
952
|
+
buf: Buffer.from(b, "base64")
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function mimeFromExt(ext) {
|
|
956
|
+
ext = ext.toLowerCase();
|
|
957
|
+
if ("png" === ext) return "image/png";
|
|
958
|
+
if ("jpg" === ext || "jpeg" === ext) return "image/jpeg";
|
|
959
|
+
if ("webp" === ext) return "image/webp";
|
|
960
|
+
if ("avif" === ext) return "image/avif";
|
|
961
|
+
if ("gif" === ext) return "image/gif";
|
|
962
|
+
if ("svg" === ext) return "image/svg+xml";
|
|
963
|
+
return "application/octet-stream";
|
|
964
|
+
}
|
|
965
|
+
function extFromMime(m) {
|
|
966
|
+
m = m.toLowerCase();
|
|
967
|
+
if (m.includes("png")) return "png";
|
|
968
|
+
if (m.includes("jpeg")) return "jpg";
|
|
969
|
+
if (m.includes("webp")) return "webp";
|
|
970
|
+
if (m.includes("avif")) return "avif";
|
|
971
|
+
if (m.includes("gif")) return "gif";
|
|
972
|
+
if (m.includes("svg")) return "svg";
|
|
973
|
+
return "bin";
|
|
974
|
+
}
|
|
975
|
+
function toDataURL(mime, buf) {
|
|
976
|
+
return `data:${mime};base64,` + buf.toString("base64");
|
|
977
|
+
}
|
|
978
|
+
function hash(buf) {
|
|
979
|
+
let h = 2166136261;
|
|
980
|
+
for(let i = 0; i < buf.length; i++){
|
|
981
|
+
h ^= buf[i];
|
|
982
|
+
h = Math.imul(h, 16777619);
|
|
983
|
+
}
|
|
984
|
+
return (h >>> 0).toString(16);
|
|
985
|
+
}
|
|
986
|
+
async function loadSharp() {
|
|
987
|
+
try {
|
|
988
|
+
return __webpack_require__("sharp");
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function estimateMaxScale(json) {
|
|
994
|
+
const m = new Map();
|
|
995
|
+
for (const ly of json.layers || []){
|
|
996
|
+
if (ly?.ty !== 2) continue;
|
|
997
|
+
const id = ly.refId;
|
|
998
|
+
if (!id) continue;
|
|
999
|
+
let mx = 100;
|
|
1000
|
+
const s = ly.ks?.s;
|
|
1001
|
+
if (s?.a === 0 && Array.isArray(s.k)) mx = Math.max(mx, s.k[0] ?? 100, s.k[1] ?? 100);
|
|
1002
|
+
else if (s?.a === 1 && Array.isArray(s.k)) for (const kf of s.k){
|
|
1003
|
+
const arr = kf?.s ?? kf?.e;
|
|
1004
|
+
if (Array.isArray(arr)) mx = Math.max(mx, arr[0] ?? 100, arr[1] ?? 100);
|
|
1005
|
+
}
|
|
1006
|
+
m.set(id, Math.max(m.get(id) ?? 100, mx));
|
|
1007
|
+
}
|
|
1008
|
+
return m;
|
|
1009
|
+
}
|
|
1010
|
+
async function compressImages(input, opt = {}) {
|
|
1011
|
+
const log = opt.onLog ?? (()=>{});
|
|
1012
|
+
const safeOpt = {
|
|
1013
|
+
...opt
|
|
1014
|
+
};
|
|
1015
|
+
delete safeOpt.onLog;
|
|
1016
|
+
log('Plugin configuration: ' + JSON.stringify(safeOpt, null, 2));
|
|
1017
|
+
const sharp = await loadSharp();
|
|
1018
|
+
log('Sharp library loaded: ' + !!sharp);
|
|
1019
|
+
if (!sharp) {
|
|
1020
|
+
const warn = "[compress-images] \u672A\u5B89\u88C5 sharp\uFF0C\u8DF3\u8FC7\u538B\u7F29";
|
|
1021
|
+
log(warn);
|
|
1022
|
+
return {
|
|
1023
|
+
data: input,
|
|
1024
|
+
stats: {
|
|
1025
|
+
total: 0,
|
|
1026
|
+
optimized: 0,
|
|
1027
|
+
before: 0,
|
|
1028
|
+
after: 0,
|
|
1029
|
+
saved: 0,
|
|
1030
|
+
savedPct: 0,
|
|
1031
|
+
perAsset: [],
|
|
1032
|
+
warnings: [
|
|
1033
|
+
warn
|
|
1034
|
+
]
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const cfg = {
|
|
1039
|
+
enableWebp: opt.enableWebp ?? true,
|
|
1040
|
+
enableAvif: opt.enableAvif ?? false,
|
|
1041
|
+
quality: opt.quality ?? "balanced",
|
|
1042
|
+
longEdge: opt.longEdge ?? 0,
|
|
1043
|
+
embed: opt.embed ?? true,
|
|
1044
|
+
baseDir: opt.baseDir ?? process.cwd(),
|
|
1045
|
+
outDir: opt.outDir ?? node_path.join(process.cwd(), "lottie-assets"),
|
|
1046
|
+
useTinyPNG: opt.useTinyPNG ?? false,
|
|
1047
|
+
ssimOptions: opt.ssimOptions ?? {
|
|
1048
|
+
level: "standard"
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
if (!cfg.embed && !node_fs.existsSync(cfg.outDir)) node_fs.mkdirSync(cfg.outDir, {
|
|
1052
|
+
recursive: true
|
|
1053
|
+
});
|
|
1054
|
+
const out = JSON.parse(JSON.stringify(input));
|
|
1055
|
+
const stats = {
|
|
1056
|
+
total: 0,
|
|
1057
|
+
optimized: 0,
|
|
1058
|
+
before: 0,
|
|
1059
|
+
after: 0,
|
|
1060
|
+
saved: 0,
|
|
1061
|
+
savedPct: 0,
|
|
1062
|
+
perAsset: [],
|
|
1063
|
+
warnings: []
|
|
1064
|
+
};
|
|
1065
|
+
const maxScale = estimateMaxScale(input);
|
|
1066
|
+
if (!Array.isArray(out.assets)) return {
|
|
1067
|
+
data: out,
|
|
1068
|
+
stats
|
|
1069
|
+
};
|
|
1070
|
+
for (const a of out.assets){
|
|
1071
|
+
const src = a?.p;
|
|
1072
|
+
if (!src) continue;
|
|
1073
|
+
let buf, mime = "", ext = "";
|
|
1074
|
+
try {
|
|
1075
|
+
if (isDataURL(src)) {
|
|
1076
|
+
const p = parseDataURL(src);
|
|
1077
|
+
buf = p.buf;
|
|
1078
|
+
mime = p.mime;
|
|
1079
|
+
ext = extFromMime(mime);
|
|
1080
|
+
} else {
|
|
1081
|
+
const full = node_path.join(cfg.baseDir, a.u ?? "", src);
|
|
1082
|
+
if (!node_fs.existsSync(full)) continue;
|
|
1083
|
+
buf = node_fs.readFileSync(full);
|
|
1084
|
+
ext = node_path.extname(src).slice(1).toLowerCase();
|
|
1085
|
+
mime = mimeFromExt(ext);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
stats.warnings.push(`[compress-images] \u{8BFB}\u{53D6}\u{5931}\u{8D25} ${a.id ?? ""}: ${e.message}`);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (!buf || /svg|gif/.test(ext) || /svg|gif/.test(mime)) {
|
|
1092
|
+
log(`Skipping asset ${a.id}: unsupported format (${ext}/${mime})`);
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
if (buf.byteLength < 4096) {
|
|
1096
|
+
log(`Skipping asset ${a.id}: file too small (${buf.byteLength} bytes < 4096 bytes threshold)`);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
stats.total++;
|
|
1100
|
+
stats.before += buf.byteLength;
|
|
1101
|
+
let originalBuf = buf;
|
|
1102
|
+
let originalExt = ext;
|
|
1103
|
+
let originalMime = mime;
|
|
1104
|
+
try {
|
|
1105
|
+
if (cfg.useTinyPNG) {
|
|
1106
|
+
const fileSizeKB = buf.byteLength / 1024;
|
|
1107
|
+
try {
|
|
1108
|
+
log(`Using TinyPNG for asset ${a.id} (${Math.round(fileSizeKB)}KB)`);
|
|
1109
|
+
const [tinyPngBuf, tinyPngExt] = await compress(buf);
|
|
1110
|
+
log(`TinyPNG compressed: ${Math.round(buf.byteLength / 1024)}KB -> ${Math.round(tinyPngBuf.byteLength / 1024)}KB`);
|
|
1111
|
+
buf = tinyPngBuf;
|
|
1112
|
+
ext = tinyPngExt;
|
|
1113
|
+
mime = mimeFromExt(ext);
|
|
1114
|
+
originalBuf = buf;
|
|
1115
|
+
originalExt = ext;
|
|
1116
|
+
originalMime = mime;
|
|
1117
|
+
} catch (tinyError) {
|
|
1118
|
+
log(`TinyPNG failed for asset ${a.id}: ${tinyError.message}, falling back to original`);
|
|
1119
|
+
stats.warnings.push(`[compress-images] TinyPNG \u{5931}\u{8D25} ${a.id ?? ""}: ${tinyError.message}, \u{4F7F}\u{7528}\u{539F}\u{59CB}\u{56FE}\u{7247}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const meta = await sharp(buf).metadata();
|
|
1123
|
+
const hasAlpha = !!meta.hasAlpha;
|
|
1124
|
+
let W = meta.width || 0, H = meta.height || 0;
|
|
1125
|
+
if (cfg.longEdge && Math.max(W, H) > cfg.longEdge) {
|
|
1126
|
+
const s = cfg.longEdge / Math.max(W, H);
|
|
1127
|
+
W = Math.round(W * s);
|
|
1128
|
+
H = Math.round(H * s);
|
|
1129
|
+
}
|
|
1130
|
+
const ms = maxScale.get(a.id || "") ?? 100;
|
|
1131
|
+
if (ms > 100 && meta.width && meta.height) {
|
|
1132
|
+
const minW = Math.round(meta.width * ms / 100), minH = Math.round(meta.height * ms / 100);
|
|
1133
|
+
W = Math.max(W, Math.min(minW, meta.width));
|
|
1134
|
+
H = Math.max(H, Math.min(minH, meta.height));
|
|
1135
|
+
}
|
|
1136
|
+
log(`Using SSIM compression algorithm for asset ${a.id} (level: ${cfg.ssimOptions.level})`);
|
|
1137
|
+
const ssimOptions = {
|
|
1138
|
+
level: cfg.ssimOptions.level,
|
|
1139
|
+
ssimThreshold: cfg.ssimOptions.ssimThreshold,
|
|
1140
|
+
allowFormats: [],
|
|
1141
|
+
fallbackToBestSSIM: cfg.ssimOptions.fallbackToBestSSIM,
|
|
1142
|
+
forceEffort: cfg.ssimOptions.forceEffort
|
|
1143
|
+
};
|
|
1144
|
+
if (cfg.enableWebp) ssimOptions.allowFormats.push("webp-lossy", "webp-near");
|
|
1145
|
+
if (!hasAlpha) ssimOptions.allowFormats.push("jpeg");
|
|
1146
|
+
ssimOptions.allowFormats.push("png");
|
|
1147
|
+
let resizedBuf = buf;
|
|
1148
|
+
if (W && H && (W < (meta.width || 0) || H < (meta.height || 0))) {
|
|
1149
|
+
resizedBuf = await sharp(buf).rotate().withMetadata({
|
|
1150
|
+
exif: void 0,
|
|
1151
|
+
icc: void 0
|
|
1152
|
+
}).resize(W, H, {
|
|
1153
|
+
fit: "inside",
|
|
1154
|
+
withoutEnlargement: true
|
|
1155
|
+
}).png().toBuffer();
|
|
1156
|
+
log(`Resized image from ${meta.width}x${meta.height} to ${W}x${H}`);
|
|
1157
|
+
}
|
|
1158
|
+
const ssimResult = await compressImage(resizedBuf, ssimOptions);
|
|
1159
|
+
const useOriginal = ssimResult.bytes >= originalBuf.byteLength;
|
|
1160
|
+
let finalBuf, finalExt, finalMime, finalBytes;
|
|
1161
|
+
let finalSSIM;
|
|
1162
|
+
let resized = !!(W && H && (W < (meta.width || 0) || H < (meta.height || 0)));
|
|
1163
|
+
if (useOriginal) {
|
|
1164
|
+
log(`SSIM compressed result (${Math.round(ssimResult.bytes / 1024)}KB, SSIM: ${ssimResult.ssim.toFixed(3)}) is not better than original (${Math.round(originalBuf.byteLength / 1024)}KB), using original`);
|
|
1165
|
+
finalBuf = originalBuf;
|
|
1166
|
+
finalExt = originalExt;
|
|
1167
|
+
finalMime = originalMime;
|
|
1168
|
+
finalBytes = originalBuf.byteLength;
|
|
1169
|
+
finalSSIM = 1.0;
|
|
1170
|
+
resized = false;
|
|
1171
|
+
} else {
|
|
1172
|
+
log(`SSIM result: ${ssimResult.label} - ${Math.round(ssimResult.bytes / 1024)}KB (SSIM: ${ssimResult.ssim.toFixed(3)}, threshold: ${ssimResult.threshold})`);
|
|
1173
|
+
finalBuf = ssimResult.buffer;
|
|
1174
|
+
finalExt = "jpeg" === ssimResult.format ? "jpg" : ssimResult.format;
|
|
1175
|
+
finalMime = mimeFromExt(finalExt);
|
|
1176
|
+
finalBytes = ssimResult.bytes;
|
|
1177
|
+
finalSSIM = ssimResult.ssim;
|
|
1178
|
+
}
|
|
1179
|
+
if (cfg.embed) {
|
|
1180
|
+
a.u = "";
|
|
1181
|
+
a.p = toDataURL(finalMime, finalBuf);
|
|
1182
|
+
a.e = 1;
|
|
1183
|
+
if (useOriginal) {
|
|
1184
|
+
a.w = a.w;
|
|
1185
|
+
a.h = a.h;
|
|
1186
|
+
} else {
|
|
1187
|
+
a.w = W || meta.width || a.w;
|
|
1188
|
+
a.h = H || meta.height || a.h;
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
const baseName = a.id ? `${a.id}.${finalExt}` : `${hash(finalBuf)}.${finalExt}`;
|
|
1192
|
+
if (opt.cdn?.enabled) try {
|
|
1193
|
+
log(`Uploading asset ${a.id} to CDN...`);
|
|
1194
|
+
const cdnUrl = await uploadToCDN(finalBuf, baseName, opt.cdn, a.id);
|
|
1195
|
+
log(`Asset ${a.id} uploaded to CDN: ${cdnUrl}`);
|
|
1196
|
+
a.u = "";
|
|
1197
|
+
a.p = cdnUrl;
|
|
1198
|
+
a.e = 0;
|
|
1199
|
+
} catch (cdnError) {
|
|
1200
|
+
log(`CDN upload failed for asset ${a.id}: ${cdnError.message}, falling back to local file`);
|
|
1201
|
+
stats.warnings.push(`[compress-images] CDN \u{4E0A}\u{4F20}\u{5931}\u{8D25} ${a.id ?? ""}: ${cdnError.message}, \u{4F7F}\u{7528}\u{672C}\u{5730}\u{6587}\u{4EF6}`);
|
|
1202
|
+
const outPath = node_path.join(cfg.outDir, baseName);
|
|
1203
|
+
node_fs.writeFileSync(outPath, finalBuf);
|
|
1204
|
+
a.u = node_path.relative(cfg.baseDir, cfg.outDir).replace(/\\/g, "/") + "/";
|
|
1205
|
+
a.p = baseName;
|
|
1206
|
+
a.e = 0;
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
const outPath = node_path.join(cfg.outDir, baseName);
|
|
1210
|
+
node_fs.writeFileSync(outPath, finalBuf);
|
|
1211
|
+
a.u = node_path.relative(cfg.baseDir, cfg.outDir).replace(/\\/g, "/") + "/";
|
|
1212
|
+
a.p = baseName;
|
|
1213
|
+
a.e = 0;
|
|
1214
|
+
}
|
|
1215
|
+
if (useOriginal) {
|
|
1216
|
+
a.w = a.w;
|
|
1217
|
+
a.h = a.h;
|
|
1218
|
+
} else {
|
|
1219
|
+
a.w = W || meta.width || a.w;
|
|
1220
|
+
a.h = H || meta.height || a.h;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
stats.optimized++;
|
|
1224
|
+
stats.after += finalBytes;
|
|
1225
|
+
stats.perAsset.push({
|
|
1226
|
+
id: a.id,
|
|
1227
|
+
from: useOriginal ? originalExt : extFromMime(mime),
|
|
1228
|
+
to: finalExt,
|
|
1229
|
+
orig: originalBuf.byteLength,
|
|
1230
|
+
out: finalBytes,
|
|
1231
|
+
ssim: finalSSIM,
|
|
1232
|
+
resized: useOriginal ? false : resized
|
|
1233
|
+
});
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
stats.after += buf.byteLength;
|
|
1236
|
+
stats.perAsset.push({
|
|
1237
|
+
id: a.id,
|
|
1238
|
+
from: extFromMime(mime),
|
|
1239
|
+
to: extFromMime(mime),
|
|
1240
|
+
orig: buf.byteLength,
|
|
1241
|
+
out: buf.byteLength
|
|
1242
|
+
});
|
|
1243
|
+
stats.warnings.push(`[compress-images] \u{5904}\u{7406}\u{5931}\u{8D25} ${a.id ?? ""}: ${e.message}`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
stats.saved = Math.max(0, stats.before - stats.after);
|
|
1247
|
+
stats.savedPct = stats.before ? +(stats.saved / stats.before * 100).toFixed(2) : 0;
|
|
1248
|
+
return {
|
|
1249
|
+
data: out,
|
|
1250
|
+
stats
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
const compressImagesPlugin = {
|
|
1254
|
+
name: 'compress-images',
|
|
1255
|
+
description: 'Optimize Lottie image assets with simplified options: WebP/AVIF toggle, quality presets, and smart resizing',
|
|
1256
|
+
version: '2.0.0',
|
|
1257
|
+
defaultOptions: {
|
|
1258
|
+
enableWebp: true,
|
|
1259
|
+
enableAvif: false,
|
|
1260
|
+
quality: 'balanced',
|
|
1261
|
+
longEdge: 0,
|
|
1262
|
+
embed: true,
|
|
1263
|
+
baseDir: process.cwd(),
|
|
1264
|
+
outDir: 'lottie-assets',
|
|
1265
|
+
useTinyPNG: false,
|
|
1266
|
+
cdn: {
|
|
1267
|
+
enabled: false,
|
|
1268
|
+
prefix: 'lottie-assets'
|
|
1269
|
+
},
|
|
1270
|
+
ssimOptions: {
|
|
1271
|
+
level: 'standard'
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
async apply (data, options, context) {
|
|
1275
|
+
const sizeBefore = calculateSize(data);
|
|
1276
|
+
const pluginOptions = {
|
|
1277
|
+
...options,
|
|
1278
|
+
onLog: (msg)=>{
|
|
1279
|
+
if (context.logger) context.logger.info(`[compress-images] ${msg}`);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const result = await compressImages(data, pluginOptions);
|
|
1283
|
+
const sizeAfter = calculateSize(result.data);
|
|
1284
|
+
return {
|
|
1285
|
+
data: result.data,
|
|
1286
|
+
changed: result.stats.optimized > 0,
|
|
1287
|
+
stats: {
|
|
1288
|
+
itemsProcessed: result.stats.total,
|
|
1289
|
+
itemsRemoved: 0,
|
|
1290
|
+
sizeBefore,
|
|
1291
|
+
sizeAfter
|
|
1292
|
+
},
|
|
1293
|
+
warnings: result.stats.warnings
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
const DEFAULT_OPTIONS = {
|
|
1298
|
+
keepMatchName: true,
|
|
1299
|
+
keepIds: false,
|
|
1300
|
+
mergeNearbyNumbers: false,
|
|
1301
|
+
markTiny: false,
|
|
1302
|
+
precisionTargetKeys: [
|
|
1303
|
+
't',
|
|
1304
|
+
'sr',
|
|
1305
|
+
'to',
|
|
1306
|
+
'ti',
|
|
1307
|
+
'color',
|
|
1308
|
+
'ip',
|
|
1309
|
+
'op',
|
|
1310
|
+
'st',
|
|
1311
|
+
's',
|
|
1312
|
+
'e'
|
|
1313
|
+
],
|
|
1314
|
+
frameKeyDigits: 0,
|
|
1315
|
+
normalKeyDigits: 3,
|
|
1316
|
+
mergeStep: 0.001,
|
|
1317
|
+
colorQuantizePrecision: 255,
|
|
1318
|
+
roundThreshold: 0.01,
|
|
1319
|
+
removeMetadataKeys: true,
|
|
1320
|
+
enableGlobalPrecision: true,
|
|
1321
|
+
globalMaxDecimals: 3,
|
|
1322
|
+
compressNames: true,
|
|
1323
|
+
removeDefaultValues: true,
|
|
1324
|
+
customMetadataKeys: [],
|
|
1325
|
+
customDefaultValues: {},
|
|
1326
|
+
onLog: ()=>{}
|
|
1327
|
+
};
|
|
1328
|
+
function deepClone(x) {
|
|
1329
|
+
return JSON.parse(JSON.stringify(x));
|
|
1330
|
+
}
|
|
1331
|
+
function toBase36(n) {
|
|
1332
|
+
const chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
1333
|
+
if (0 === n) return '0';
|
|
1334
|
+
let q = Math.floor(Math.abs(n));
|
|
1335
|
+
let out = '';
|
|
1336
|
+
while(q > 0){
|
|
1337
|
+
const m = q % 36;
|
|
1338
|
+
out = chars[m] + out;
|
|
1339
|
+
q = (q - m) / 36;
|
|
1340
|
+
}
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
function walk(obj, visit) {
|
|
1344
|
+
const go = (node, key, parent)=>{
|
|
1345
|
+
visit(node, key, parent);
|
|
1346
|
+
if (!node || 'object' != typeof node) return;
|
|
1347
|
+
if (Array.isArray(node)) {
|
|
1348
|
+
for(let i = 0; i < node.length; i++)go(node[i], String(i), node);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
for (const k of Object.keys(node))go(node[k], k, node);
|
|
1352
|
+
};
|
|
1353
|
+
go(obj, null, null);
|
|
1354
|
+
}
|
|
1355
|
+
function roundNearInteger(v, eps = 0.01) {
|
|
1356
|
+
const r = Math.round(v);
|
|
1357
|
+
return Math.abs(v - r) <= eps ? r : v;
|
|
1358
|
+
}
|
|
1359
|
+
function roundTo(num, digits) {
|
|
1360
|
+
const f = Math.pow(10, digits);
|
|
1361
|
+
return Math.round(num * f) / f;
|
|
1362
|
+
}
|
|
1363
|
+
function quantize255(x, precision = 255) {
|
|
1364
|
+
const snapped = Math.round(x * precision) / precision;
|
|
1365
|
+
return roundTo(snapped, 3);
|
|
1366
|
+
}
|
|
1367
|
+
function applyMarkAndRound(json, mark, roundThreshold) {
|
|
1368
|
+
let marked = false;
|
|
1369
|
+
let rounded = 0;
|
|
1370
|
+
if (void 0 !== mark) {
|
|
1371
|
+
json.tiny = 'number' == typeof mark ? mark : 1;
|
|
1372
|
+
marked = true;
|
|
1373
|
+
}
|
|
1374
|
+
if ('number' == typeof json.fr) {
|
|
1375
|
+
const oldFr = json.fr;
|
|
1376
|
+
json.fr = roundNearInteger(json.fr, roundThreshold);
|
|
1377
|
+
if (oldFr !== json.fr) rounded++;
|
|
1378
|
+
}
|
|
1379
|
+
if ('number' == typeof json.op) {
|
|
1380
|
+
const oldOp = json.op;
|
|
1381
|
+
json.op = roundNearInteger(json.op, roundThreshold);
|
|
1382
|
+
if (oldOp !== json.op) rounded++;
|
|
1383
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
marked,
|
|
1386
|
+
rounded
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function shortenIdRef(json) {
|
|
1390
|
+
const ids = [];
|
|
1391
|
+
walk(json, (node, key)=>{
|
|
1392
|
+
if (!key || 'object' != typeof node) return;
|
|
1393
|
+
const v = node.id;
|
|
1394
|
+
if ('string' == typeof v) ids.push(v);
|
|
1395
|
+
});
|
|
1396
|
+
const uniq = Array.from(new Set(ids));
|
|
1397
|
+
const map = {};
|
|
1398
|
+
uniq.forEach((v, i)=>map[v] = toBase36(i));
|
|
1399
|
+
let shortened = 0;
|
|
1400
|
+
walk(json, (node, key)=>{
|
|
1401
|
+
if (!key || 'object' != typeof node) return;
|
|
1402
|
+
const o = node;
|
|
1403
|
+
if ('string' == typeof o.id && map[o.id]) {
|
|
1404
|
+
o.id = map[o.id];
|
|
1405
|
+
shortened++;
|
|
1406
|
+
}
|
|
1407
|
+
if ('string' == typeof o.refId && map[o.refId]) {
|
|
1408
|
+
o.refId = map[o.refId];
|
|
1409
|
+
shortened++;
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
return shortened;
|
|
1413
|
+
}
|
|
1414
|
+
function applyNumericPrecision(json, targetKeys, frameKeyDigits, normalKeyDigits, colorPrecision) {
|
|
1415
|
+
const TARGET_KEYS = new Set(targetKeys);
|
|
1416
|
+
let processed = 0;
|
|
1417
|
+
const walkWithParentKey = (obj, parentKey)=>{
|
|
1418
|
+
if (!obj || 'object' != typeof obj) return;
|
|
1419
|
+
if (Array.isArray(obj)) {
|
|
1420
|
+
for(let i = 0; i < obj.length; i++)walkWithParentKey(obj[i], null);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
for (const k of Object.keys(obj)){
|
|
1424
|
+
const val = obj[k];
|
|
1425
|
+
if ('k' === k && ('c' === parentKey || 'v' === parentKey) && Array.isArray(val)) {
|
|
1426
|
+
for(let i = 0; i < val.length; i++)if ('number' == typeof val[i]) {
|
|
1427
|
+
val[i] = quantize255(val[i], colorPrecision);
|
|
1428
|
+
processed++;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (TARGET_KEYS.has(k)) {
|
|
1432
|
+
if ('number' == typeof val) {
|
|
1433
|
+
const digits = 'ip' === k || 'op' === k ? frameKeyDigits : normalKeyDigits;
|
|
1434
|
+
obj[k] = roundTo(val, digits);
|
|
1435
|
+
processed++;
|
|
1436
|
+
} else if (Array.isArray(val)) {
|
|
1437
|
+
const digits = 'ip' === k || 'op' === k ? frameKeyDigits : normalKeyDigits;
|
|
1438
|
+
obj[k] = val.map((x)=>{
|
|
1439
|
+
if ('number' == typeof x) {
|
|
1440
|
+
processed++;
|
|
1441
|
+
return roundTo(x, digits);
|
|
1442
|
+
}
|
|
1443
|
+
return x;
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
walkWithParentKey(val, k);
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
walkWithParentKey(json, null);
|
|
1451
|
+
return processed;
|
|
1452
|
+
}
|
|
1453
|
+
function applyGlobalPrecision(json, enabled, maxDecimals) {
|
|
1454
|
+
if (!enabled) return 0;
|
|
1455
|
+
let processed = 0;
|
|
1456
|
+
walk(json, (node, key, parent)=>{
|
|
1457
|
+
if (!key || !parent || 'object' != typeof parent) return;
|
|
1458
|
+
const val = parent[key];
|
|
1459
|
+
if ('number' == typeof val && !Number.isInteger(val)) {
|
|
1460
|
+
const str = val.toString();
|
|
1461
|
+
const decimalIndex = str.indexOf('.');
|
|
1462
|
+
if (-1 !== decimalIndex && str.length - decimalIndex - 1 > maxDecimals) {
|
|
1463
|
+
parent[key] = roundTo(val, maxDecimals);
|
|
1464
|
+
processed++;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
return processed;
|
|
1469
|
+
}
|
|
1470
|
+
function removeMetadataKeys(json, shouldRemove, keepMatchName, customKeys = []) {
|
|
1471
|
+
const baseKeys = [
|
|
1472
|
+
'n'
|
|
1473
|
+
];
|
|
1474
|
+
if (!keepMatchName) baseKeys.push('mn');
|
|
1475
|
+
const metadataKeys = shouldRemove ? [
|
|
1476
|
+
'ix',
|
|
1477
|
+
'bm',
|
|
1478
|
+
'ddd',
|
|
1479
|
+
'np',
|
|
1480
|
+
'cix'
|
|
1481
|
+
] : [];
|
|
1482
|
+
const DELETE_KEYS = new Set([
|
|
1483
|
+
...baseKeys,
|
|
1484
|
+
...metadataKeys,
|
|
1485
|
+
...customKeys
|
|
1486
|
+
]);
|
|
1487
|
+
let deleted = 0;
|
|
1488
|
+
walk(json, (node, key, parent)=>{
|
|
1489
|
+
if (!key || !parent || 'object' != typeof parent) return;
|
|
1490
|
+
if (DELETE_KEYS.has(key)) {
|
|
1491
|
+
delete parent[key];
|
|
1492
|
+
deleted++;
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
return deleted;
|
|
1496
|
+
}
|
|
1497
|
+
function removeDefaultValues(json, shouldRemove, customDefaults = {}) {
|
|
1498
|
+
if (!shouldRemove) return {
|
|
1499
|
+
defaultValuesRemoved: 0,
|
|
1500
|
+
hiddenFalseRemoved: 0
|
|
1501
|
+
};
|
|
1502
|
+
const defaultValueMap = {
|
|
1503
|
+
bm: 0,
|
|
1504
|
+
ddd: 0,
|
|
1505
|
+
ao: 0,
|
|
1506
|
+
sr: 1,
|
|
1507
|
+
ks: null,
|
|
1508
|
+
...customDefaults
|
|
1509
|
+
};
|
|
1510
|
+
let defaultValuesRemoved = 0;
|
|
1511
|
+
let hiddenFalseRemoved = 0;
|
|
1512
|
+
walk(json, (node, key, parent)=>{
|
|
1513
|
+
if (!key || !parent || 'object' != typeof parent) return;
|
|
1514
|
+
const val = parent[key];
|
|
1515
|
+
if (key in defaultValueMap && val === defaultValueMap[key]) {
|
|
1516
|
+
delete parent[key];
|
|
1517
|
+
defaultValuesRemoved++;
|
|
1518
|
+
}
|
|
1519
|
+
if ('hd' === key && false === val) {
|
|
1520
|
+
delete parent[key];
|
|
1521
|
+
hiddenFalseRemoved++;
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
return {
|
|
1525
|
+
defaultValuesRemoved,
|
|
1526
|
+
hiddenFalseRemoved
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function compressNames(json, shouldCompress) {
|
|
1530
|
+
if (!shouldCompress) return 0;
|
|
1531
|
+
const names = [];
|
|
1532
|
+
walk(json, (node, key)=>{
|
|
1533
|
+
if ('nm' === key && 'string' == typeof node && node.length > 1) names.push(node);
|
|
1534
|
+
});
|
|
1535
|
+
const uniqueNames = Array.from(new Set(names));
|
|
1536
|
+
const compressChars = "\u4FEE\u63CF\u586B\u53D8\u5F62\u72B6\u52A8\u753B\u56FE\u5C42\u6548\u679C\u906E\u7F69\u8DEF\u5F84\u6587\u5B57\u989C\u8272\u9634\u5F71\u53D1\u5149\u5916\u8FB9\u5185\u8FB9\u9AD8\u4EAE\u4E2D";
|
|
1537
|
+
const nameMap = {};
|
|
1538
|
+
uniqueNames.forEach((name, index)=>{
|
|
1539
|
+
if (index < compressChars.length) nameMap[name] = compressChars[index];
|
|
1540
|
+
else nameMap[name] = String(index - compressChars.length);
|
|
1541
|
+
});
|
|
1542
|
+
let compressed = 0;
|
|
1543
|
+
walk(json, (node, key, parent)=>{
|
|
1544
|
+
if (!key || !parent || 'object' != typeof parent) return;
|
|
1545
|
+
if ('nm' === key && 'string' == typeof node && nameMap[node]) {
|
|
1546
|
+
parent[key] = nameMap[node];
|
|
1547
|
+
compressed++;
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
return compressed;
|
|
1551
|
+
}
|
|
1552
|
+
function mergeNearbyNumbers(json, step) {
|
|
1553
|
+
const nums = [];
|
|
1554
|
+
walk(json, (node)=>{
|
|
1555
|
+
if ('number' == typeof node) nums.push(node);
|
|
1556
|
+
});
|
|
1557
|
+
const uniq = Array.from(new Set(nums)).sort((a, b)=>a - b);
|
|
1558
|
+
const groups = [];
|
|
1559
|
+
let g = [];
|
|
1560
|
+
const isStep = (a, b)=>Number((b - a).toFixed(10)) === Number(step.toFixed(10));
|
|
1561
|
+
for(let i = 0; i < uniq.length; i++){
|
|
1562
|
+
const cur = uniq[i];
|
|
1563
|
+
const nxt = uniq[i + 1];
|
|
1564
|
+
if (void 0 !== nxt && isStep(cur, nxt)) {
|
|
1565
|
+
if (0 === g.length) g.push(cur);
|
|
1566
|
+
g.push(nxt);
|
|
1567
|
+
} else if (g.length > 0) {
|
|
1568
|
+
groups.push(g.slice());
|
|
1569
|
+
g = [];
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const replace = new Map();
|
|
1573
|
+
for (const arr of groups)for(let i = 1; i < arr.length; i += 2){
|
|
1574
|
+
const a = arr[i - 1], b = arr[i];
|
|
1575
|
+
const sa = String(a), sb = String(b);
|
|
1576
|
+
if (sa.length <= sb.length) replace.set(b, a);
|
|
1577
|
+
else replace.set(a, b);
|
|
1578
|
+
}
|
|
1579
|
+
if (0 === replace.size) return 0;
|
|
1580
|
+
let merged = 0;
|
|
1581
|
+
walk(json, (node, key, parent)=>{
|
|
1582
|
+
if (!key || !parent) return;
|
|
1583
|
+
const v = parent[key];
|
|
1584
|
+
if ('number' == typeof v && replace.has(v)) {
|
|
1585
|
+
parent[key] = replace.get(v);
|
|
1586
|
+
merged++;
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
return merged;
|
|
1590
|
+
}
|
|
1591
|
+
function fixMissingInd(json) {
|
|
1592
|
+
let fixed = 0;
|
|
1593
|
+
const patch = (layers)=>{
|
|
1594
|
+
if (!Array.isArray(layers)) return;
|
|
1595
|
+
for(let i = 0; i < layers.length; i++){
|
|
1596
|
+
const lyr = layers[i];
|
|
1597
|
+
if (lyr && (void 0 === lyr.ind || null === lyr.ind)) {
|
|
1598
|
+
lyr.ind = i + 1;
|
|
1599
|
+
fixed++;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
patch(json.layers);
|
|
1604
|
+
if (Array.isArray(json.assets)) for (const a of json.assets)patch(a.layers);
|
|
1605
|
+
return fixed;
|
|
1606
|
+
}
|
|
1607
|
+
function countLayersAndAssets(json) {
|
|
1608
|
+
let layers = 0;
|
|
1609
|
+
let assets = 0;
|
|
1610
|
+
if (Array.isArray(json.layers)) layers += json.layers.length;
|
|
1611
|
+
if (Array.isArray(json.assets)) {
|
|
1612
|
+
assets += json.assets.length;
|
|
1613
|
+
for (const asset of json.assets)if (Array.isArray(asset.layers)) layers += asset.layers.length;
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
layers,
|
|
1617
|
+
assets
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function minifyLottieJson(input, options = {}) {
|
|
1621
|
+
const opts = {
|
|
1622
|
+
...DEFAULT_OPTIONS,
|
|
1623
|
+
...options
|
|
1624
|
+
};
|
|
1625
|
+
const { onLog } = opts;
|
|
1626
|
+
const json = deepClone(input);
|
|
1627
|
+
const originalSize = JSON.stringify(json).length;
|
|
1628
|
+
const { layers: layersCount, assets: assetsCount } = countLayersAndAssets(json);
|
|
1629
|
+
let idsShortened = 0;
|
|
1630
|
+
let numbersPrecisionReduced = 0;
|
|
1631
|
+
let nearbyNumbersMerged = 0;
|
|
1632
|
+
let missingIndicesFixed = 0;
|
|
1633
|
+
let metadataKeysRemoved = 0;
|
|
1634
|
+
let globalPrecisionProcessed = 0;
|
|
1635
|
+
let namesCompressed = 0;
|
|
1636
|
+
let defaultValuesRemoved = 0;
|
|
1637
|
+
let hiddenFalseRemoved = 0;
|
|
1638
|
+
applyMarkAndRound(json, opts.markTiny, opts.roundThreshold);
|
|
1639
|
+
if (!opts.keepIds) idsShortened = shortenIdRef(json);
|
|
1640
|
+
numbersPrecisionReduced = applyNumericPrecision(json, opts.precisionTargetKeys, opts.frameKeyDigits, opts.normalKeyDigits, opts.colorQuantizePrecision);
|
|
1641
|
+
globalPrecisionProcessed = applyGlobalPrecision(json, opts.enableGlobalPrecision, opts.globalMaxDecimals);
|
|
1642
|
+
metadataKeysRemoved = removeMetadataKeys(json, opts.removeMetadataKeys, opts.keepMatchName, opts.customMetadataKeys);
|
|
1643
|
+
const defaultResult = removeDefaultValues(json, opts.removeDefaultValues, opts.customDefaultValues);
|
|
1644
|
+
defaultValuesRemoved = defaultResult.defaultValuesRemoved;
|
|
1645
|
+
hiddenFalseRemoved = defaultResult.hiddenFalseRemoved;
|
|
1646
|
+
if (opts.compressNames) namesCompressed = compressNames(json, opts.compressNames);
|
|
1647
|
+
if (opts.mergeNearbyNumbers) nearbyNumbersMerged = mergeNearbyNumbers(json, opts.mergeStep);
|
|
1648
|
+
missingIndicesFixed = fixMissingInd(json);
|
|
1649
|
+
const minifiedSize = JSON.stringify(json).length;
|
|
1650
|
+
const savedBytes = originalSize - minifiedSize;
|
|
1651
|
+
const compressionRatio = originalSize > 0 ? 1 - minifiedSize / originalSize : 0;
|
|
1652
|
+
const stats = {
|
|
1653
|
+
totalItems: layersCount + assetsCount,
|
|
1654
|
+
processedItems: layersCount + assetsCount,
|
|
1655
|
+
warnings: [],
|
|
1656
|
+
originalSize,
|
|
1657
|
+
minifiedSize,
|
|
1658
|
+
compressionRatio,
|
|
1659
|
+
savedBytes,
|
|
1660
|
+
layersProcessed: layersCount,
|
|
1661
|
+
assetsProcessed: assetsCount,
|
|
1662
|
+
idsShortened,
|
|
1663
|
+
numbersPrecisionReduced,
|
|
1664
|
+
attributesRemoved: metadataKeysRemoved,
|
|
1665
|
+
nearbyNumbersMerged,
|
|
1666
|
+
missingIndicesFixed,
|
|
1667
|
+
metadataKeysRemoved,
|
|
1668
|
+
globalPrecisionProcessed,
|
|
1669
|
+
namesCompressed,
|
|
1670
|
+
defaultValuesRemoved,
|
|
1671
|
+
hiddenFalseRemoved
|
|
1672
|
+
};
|
|
1673
|
+
return {
|
|
1674
|
+
data: json,
|
|
1675
|
+
stats
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
const minifyJsonPlugin = {
|
|
1679
|
+
name: 'minify-json',
|
|
1680
|
+
description: 'Optimize Lottie JSON structure: round frame rates, quantize colors, reduce precision, shorten IDs, remove redundant fields',
|
|
1681
|
+
version: '1.0.0',
|
|
1682
|
+
defaultOptions: {
|
|
1683
|
+
keepMatchName: true,
|
|
1684
|
+
keepIds: false,
|
|
1685
|
+
mergeNearbyNumbers: false,
|
|
1686
|
+
markTiny: false,
|
|
1687
|
+
precisionTargetKeys: [
|
|
1688
|
+
't',
|
|
1689
|
+
'sr',
|
|
1690
|
+
'to',
|
|
1691
|
+
'ti',
|
|
1692
|
+
'color',
|
|
1693
|
+
'ip',
|
|
1694
|
+
'op',
|
|
1695
|
+
'st',
|
|
1696
|
+
's',
|
|
1697
|
+
'e'
|
|
1698
|
+
],
|
|
1699
|
+
frameKeyDigits: 0,
|
|
1700
|
+
normalKeyDigits: 3,
|
|
1701
|
+
mergeStep: 0.001,
|
|
1702
|
+
colorQuantizePrecision: 255,
|
|
1703
|
+
roundThreshold: 0.01,
|
|
1704
|
+
removeMetadataKeys: true,
|
|
1705
|
+
enableGlobalPrecision: true,
|
|
1706
|
+
globalMaxDecimals: 3,
|
|
1707
|
+
compressNames: true,
|
|
1708
|
+
removeDefaultValues: true,
|
|
1709
|
+
customMetadataKeys: [],
|
|
1710
|
+
customDefaultValues: {}
|
|
1711
|
+
},
|
|
1712
|
+
async apply (data, options, context) {
|
|
1713
|
+
const sizeBefore = calculateSize(data);
|
|
1714
|
+
const pluginOptions = {
|
|
1715
|
+
...options,
|
|
1716
|
+
onLog: (msg)=>{
|
|
1717
|
+
if (context.logger) context.logger.info(`[minify-json] ${msg}`);
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
const result = minifyLottieJson(data, pluginOptions);
|
|
1721
|
+
const sizeAfter = calculateSize(result.data);
|
|
1722
|
+
const warnings = [];
|
|
1723
|
+
if (pluginOptions.mergeNearbyNumbers && result.stats.nearbyNumbersMerged > 0) warnings.push(`\u{6FC0}\u{8FDB}\u{4F18}\u{5316}\u{FF1A}\u{5408}\u{5E76}\u{4E86} ${result.stats.nearbyNumbersMerged} \u{4E2A}\u{76F8}\u{90BB}\u{6570}\u{503C}\u{FF0C}\u{53EF}\u{80FD}\u{5F15}\u{5165}\u{8F7B}\u{5FAE}\u{6570}\u{503C}\u{6270}\u{52A8}`);
|
|
1724
|
+
if (!pluginOptions.keepMatchName && result.stats.attributesRemoved > 0) warnings.push("\u5DF2\u5220\u9664 matchName \u5C5E\u6027\uFF0C\u67D0\u4E9B\u5DE5\u5177\u53EF\u80FD\u65E0\u6CD5\u6B63\u786E\u89E3\u6790");
|
|
1725
|
+
if (!pluginOptions.keepIds && result.stats.idsShortened > 0) warnings.push(`\u{5DF2}\u{77ED}\u{540D}\u{5316} ${result.stats.idsShortened} \u{4E2A} ID\u{FF0C}\u{786E}\u{4FDD}\u{4E0D}\u{4F9D}\u{8D56}\u{539F}\u{59CB} ID \u{503C}`);
|
|
1726
|
+
return {
|
|
1727
|
+
data: result.data,
|
|
1728
|
+
changed: result.stats.savedBytes > 0,
|
|
1729
|
+
stats: {
|
|
1730
|
+
itemsProcessed: result.stats.totalItems,
|
|
1731
|
+
itemsRemoved: result.stats.attributesRemoved,
|
|
1732
|
+
sizeBefore,
|
|
1733
|
+
sizeAfter
|
|
1734
|
+
},
|
|
1735
|
+
warnings
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
const plugins_plugins = [
|
|
1740
|
+
removeUnusedAssetsPlugin,
|
|
1741
|
+
simplifyPathsPlugin,
|
|
1742
|
+
compressImagesPlugin,
|
|
1743
|
+
minifyJsonPlugin
|
|
1744
|
+
];
|
|
1745
|
+
function getAvailablePlugins() {
|
|
1746
|
+
return [
|
|
1747
|
+
...plugins_plugins
|
|
1748
|
+
];
|
|
1749
|
+
}
|
|
1750
|
+
function validateQualityLevel(value, fieldName) {
|
|
1751
|
+
const errors = [];
|
|
1752
|
+
if (void 0 !== value) {
|
|
1753
|
+
const validLevels = [
|
|
1754
|
+
'lossless',
|
|
1755
|
+
'balanced',
|
|
1756
|
+
'smallest'
|
|
1757
|
+
];
|
|
1758
|
+
if (!validLevels.includes(value)) errors.push(`${fieldName} must be one of: ${validLevels.join(', ')}, got: ${value}`);
|
|
1759
|
+
}
|
|
1760
|
+
return errors;
|
|
1761
|
+
}
|
|
1762
|
+
function validateNumberRange(value, fieldName, min, max) {
|
|
1763
|
+
const errors = [];
|
|
1764
|
+
if (void 0 !== value) if ('number' != typeof value) errors.push(`${fieldName} must be a number, got: ${typeof value}`);
|
|
1765
|
+
else {
|
|
1766
|
+
if (void 0 !== min && value < min) errors.push(`${fieldName} must be >= ${min}, got: ${value}`);
|
|
1767
|
+
if (void 0 !== max && value > max) errors.push(`${fieldName} must be <= ${max}, got: ${value}`);
|
|
1768
|
+
}
|
|
1769
|
+
return errors;
|
|
1770
|
+
}
|
|
1771
|
+
function validateBoolean(value, fieldName) {
|
|
1772
|
+
const errors = [];
|
|
1773
|
+
if (void 0 !== value && 'boolean' != typeof value) errors.push(`${fieldName} must be a boolean, got: ${typeof value}`);
|
|
1774
|
+
return errors;
|
|
1775
|
+
}
|
|
1776
|
+
function validateCompressImagesOptions(options) {
|
|
1777
|
+
const errors = [];
|
|
1778
|
+
const warnings = [];
|
|
1779
|
+
const suggestions = [];
|
|
1780
|
+
errors.push(...validateBoolean(options.enableWebp, 'enableWebp'));
|
|
1781
|
+
errors.push(...validateBoolean(options.enableAvif, 'enableAvif'));
|
|
1782
|
+
errors.push(...validateBoolean(options.embed, 'embed'));
|
|
1783
|
+
errors.push(...validateQualityLevel(options.quality, 'quality'));
|
|
1784
|
+
errors.push(...validateNumberRange(options.longEdge, 'longEdge', 0));
|
|
1785
|
+
if (false === options.enableWebp && false === options.enableAvif && 'smallest' === options.quality) {
|
|
1786
|
+
warnings.push("\u8D28\u91CF\u8BBE\u7F6E\u4E3A smallest \u4F46\u7981\u7528\u4E86 WebP \u548C AVIF\uFF0C\u53EF\u80FD\u65E0\u6CD5\u8FBE\u5230\u6700\u4F73\u538B\u7F29\u6548\u679C");
|
|
1787
|
+
suggestions.push("\u5EFA\u8BAE\u542F\u7528 WebP \u6216 AVIF \u4EE5\u83B7\u5F97\u66F4\u597D\u7684\u538B\u7F29\u6548\u679C");
|
|
1788
|
+
}
|
|
1789
|
+
if ('lossless' === options.quality && (options.enableWebp || options.enableAvif)) suggestions.push("\u4F7F\u7528\u65E0\u635F\u8D28\u91CF\u65F6\uFF0C\u73B0\u4EE3\u683C\u5F0F (WebP/AVIF) \u7684\u4F18\u52BF\u8F83\u5C0F\uFF0C\u8003\u8651\u4F7F\u7528 balanced \u8D28\u91CF");
|
|
1790
|
+
if (options.longEdge && options.longEdge < 256) warnings.push(`longEdge \u{8BBE}\u{7F6E}\u{8FC7}\u{5C0F} (${options.longEdge}px)\u{FF0C}\u{53EF}\u{80FD}\u{5BFC}\u{81F4}\u{56FE}\u{50CF}\u{8D28}\u{91CF}\u{4E25}\u{91CD}\u{4E0B}\u{964D}`);
|
|
1791
|
+
return {
|
|
1792
|
+
valid: 0 === errors.length,
|
|
1793
|
+
errors,
|
|
1794
|
+
warnings,
|
|
1795
|
+
suggestions
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function validateSimplifyPathsOptions(options) {
|
|
1799
|
+
const errors = [];
|
|
1800
|
+
const warnings = [];
|
|
1801
|
+
const suggestions = [];
|
|
1802
|
+
errors.push(...validateNumberRange(options.tolerance, 'tolerance', 0));
|
|
1803
|
+
errors.push(...validateNumberRange(options.minPoints, 'minPoints', 3));
|
|
1804
|
+
errors.push(...validateNumberRange(options.cornerAngleDeg, 'cornerAngleDeg', 0, 180));
|
|
1805
|
+
errors.push(...validateNumberRange(options.samplesPerSegment, 'samplesPerSegment', 1));
|
|
1806
|
+
errors.push(...validateNumberRange(options.tension, 'tension', 0, 2));
|
|
1807
|
+
errors.push(...validateBoolean(options.relativeTolerance, 'relativeTolerance'));
|
|
1808
|
+
errors.push(...validateBoolean(options.preserveCorners, 'preserveCorners'));
|
|
1809
|
+
if (void 0 !== options.mode) {
|
|
1810
|
+
const validModes = [
|
|
1811
|
+
'static-only',
|
|
1812
|
+
'animated-consistent',
|
|
1813
|
+
'all'
|
|
1814
|
+
];
|
|
1815
|
+
if (!validModes.includes(options.mode)) errors.push(`mode must be one of: ${validModes.join(', ')}, got: ${options.mode}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (void 0 !== options.algorithm && 'rdp' !== options.algorithm) errors.push(`algorithm must be 'rdp', got: ${options.algorithm}`);
|
|
1818
|
+
if (options.tolerance && options.tolerance > 10) warnings.push(`tolerance \u{503C}\u{8F83}\u{5927} (${options.tolerance})\u{FF0C}\u{53EF}\u{80FD}\u{5BFC}\u{81F4}\u{8DEF}\u{5F84}\u{8FC7}\u{5EA6}\u{7B80}\u{5316}`);
|
|
1819
|
+
if (options.minPoints && options.minPoints < 3) errors.push("minPoints \u5FC5\u987B\u81F3\u5C11\u4E3A 3 \u624D\u80FD\u5F62\u6210\u6709\u6548\u8DEF\u5F84");
|
|
1820
|
+
return {
|
|
1821
|
+
valid: 0 === errors.length,
|
|
1822
|
+
errors,
|
|
1823
|
+
warnings,
|
|
1824
|
+
suggestions
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function validateRemoveUnusedAssetsOptions(options) {
|
|
1828
|
+
const errors = [];
|
|
1829
|
+
const warnings = [];
|
|
1830
|
+
const suggestions = [];
|
|
1831
|
+
errors.push(...validateBoolean(options.keepAssetsWithoutId, 'keepAssetsWithoutId'));
|
|
1832
|
+
errors.push(...validateBoolean(options.keepUnreferencedPrecomps, 'keepUnreferencedPrecomps'));
|
|
1833
|
+
if (void 0 !== options.preserveAssetIds) if (Array.isArray(options.preserveAssetIds)) {
|
|
1834
|
+
for (const id of options.preserveAssetIds)if ('string' != typeof id) {
|
|
1835
|
+
errors.push('All items in preserveAssetIds must be strings');
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
} else errors.push('preserveAssetIds must be an array');
|
|
1839
|
+
return {
|
|
1840
|
+
valid: 0 === errors.length,
|
|
1841
|
+
errors,
|
|
1842
|
+
warnings,
|
|
1843
|
+
suggestions
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
const pluginValidators = {
|
|
1847
|
+
'compress-images': validateCompressImagesOptions,
|
|
1848
|
+
'simplify-paths': validateSimplifyPathsOptions,
|
|
1849
|
+
'remove-unused-assets': validateRemoveUnusedAssetsOptions
|
|
1850
|
+
};
|
|
1851
|
+
function validatePluginConfig(pluginName, options) {
|
|
1852
|
+
const validator = pluginValidators[pluginName];
|
|
1853
|
+
if (!validator) return {
|
|
1854
|
+
valid: true,
|
|
1855
|
+
errors: [],
|
|
1856
|
+
warnings: [
|
|
1857
|
+
`No validator found for plugin: ${pluginName}`
|
|
1858
|
+
],
|
|
1859
|
+
suggestions: []
|
|
1860
|
+
};
|
|
1861
|
+
return validator(options);
|
|
1862
|
+
}
|
|
1863
|
+
function validateAllPluginConfigs(plugins, pluginOptions) {
|
|
1864
|
+
const results = {};
|
|
1865
|
+
for (const plugin of plugins){
|
|
1866
|
+
const options = {
|
|
1867
|
+
...plugin.defaultOptions || {},
|
|
1868
|
+
...pluginOptions[plugin.name] || {}
|
|
1869
|
+
};
|
|
1870
|
+
results[plugin.name] = validatePluginConfig(plugin.name, options);
|
|
1871
|
+
}
|
|
1872
|
+
return results;
|
|
1873
|
+
}
|
|
1874
|
+
function detectConfigConflicts(plugins, pluginOptions) {
|
|
1875
|
+
const conflicts = [];
|
|
1876
|
+
const compressImagesOptions = pluginOptions['compress-images'];
|
|
1877
|
+
const simplifyPathsOptions = pluginOptions['simplify-paths'];
|
|
1878
|
+
if (compressImagesOptions?.quality === 'lossless' && simplifyPathsOptions?.tolerance && simplifyPathsOptions.tolerance > 1) conflicts.push("配置冲突:compress-images 使用无损质量,但 simplify-paths 的容差较大,可能导致视觉质量不一致");
|
|
1879
|
+
if (compressImagesOptions?.enableAvif === true && compressImagesOptions?.longEdge === 0) conflicts.push("\u6027\u80FD\u8B66\u544A\uFF1A\u542F\u7528\u4E86 AVIF \u4E14\u672A\u9650\u5236\u56FE\u50CF\u5C3A\u5BF8\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5904\u7406\u65F6\u95F4\u8FC7\u957F");
|
|
1880
|
+
return conflicts;
|
|
1881
|
+
}
|
|
1882
|
+
class LottieOptimizer {
|
|
1883
|
+
constructor(logger){
|
|
1884
|
+
this.plugins = new Map();
|
|
1885
|
+
this.logger = logger || createLogger();
|
|
1886
|
+
this.loadPlugins();
|
|
1887
|
+
}
|
|
1888
|
+
loadPlugins() {
|
|
1889
|
+
const availablePlugins = getAvailablePlugins();
|
|
1890
|
+
for (const plugin of availablePlugins)this.plugins.set(plugin.name, plugin);
|
|
1891
|
+
}
|
|
1892
|
+
async optimize(data, options = {}, inputFileSize) {
|
|
1893
|
+
const startTime = Date.now();
|
|
1894
|
+
const originalSize = inputFileSize || calculateSize(data);
|
|
1895
|
+
this.logger.info(`Starting optimization of ${originalSize} bytes Lottie`);
|
|
1896
|
+
const effectiveOptions = options;
|
|
1897
|
+
const pluginsToApply = await this.resolvePlugins(effectiveOptions);
|
|
1898
|
+
if (effectiveOptions.debug?.showConfigSources) {
|
|
1899
|
+
const validationResults = validateAllPluginConfigs(pluginsToApply, effectiveOptions.pluginOptions || {});
|
|
1900
|
+
const conflicts = detectConfigConflicts(pluginsToApply, effectiveOptions.pluginOptions || {});
|
|
1901
|
+
for (const [pluginName, result] of Object.entries(validationResults)){
|
|
1902
|
+
if (!result.valid) this.logger.warn(`[${pluginName}] Configuration errors: ${result.errors.join(', ')}`);
|
|
1903
|
+
if (result.warnings.length > 0) this.logger.warn(`[${pluginName}] Warnings: ${result.warnings.join(', ')}`);
|
|
1904
|
+
if (result.suggestions.length > 0) this.logger.info(`[${pluginName}] Suggestions: ${result.suggestions.join(', ')}`);
|
|
1905
|
+
}
|
|
1906
|
+
if (conflicts.length > 0) {
|
|
1907
|
+
this.logger.warn('Configuration conflicts detected:');
|
|
1908
|
+
conflicts.forEach((conflict)=>this.logger.warn(` - ${conflict}`));
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const stats = {
|
|
1912
|
+
originalSize,
|
|
1913
|
+
optimizedSize: originalSize,
|
|
1914
|
+
compressionRatio: 0,
|
|
1915
|
+
pluginsApplied: [],
|
|
1916
|
+
processingTime: 0
|
|
1917
|
+
};
|
|
1918
|
+
let currentData = JSON.parse(JSON.stringify(data));
|
|
1919
|
+
const pluginReports = [];
|
|
1920
|
+
const warnings = [];
|
|
1921
|
+
const errors = [];
|
|
1922
|
+
for(let i = 0; i < pluginsToApply.length; i++){
|
|
1923
|
+
const plugin = pluginsToApply[i];
|
|
1924
|
+
try {
|
|
1925
|
+
this.logger.info(`Applying plugin: ${plugin.name}`);
|
|
1926
|
+
const pluginStartTime = Date.now();
|
|
1927
|
+
const sizeBefore = calculateSize(currentData);
|
|
1928
|
+
const context = {
|
|
1929
|
+
originalData: data,
|
|
1930
|
+
currentData,
|
|
1931
|
+
stats,
|
|
1932
|
+
logger: this.logger
|
|
1933
|
+
};
|
|
1934
|
+
const defaultOptions = plugin.defaultOptions || {};
|
|
1935
|
+
const presetOptions = effectiveOptions.pluginOptions?.[plugin.name] || {};
|
|
1936
|
+
const pluginOptions = {
|
|
1937
|
+
...defaultOptions,
|
|
1938
|
+
...presetOptions
|
|
1939
|
+
};
|
|
1940
|
+
if (effectiveOptions.debug?.showEffectiveConfig) {
|
|
1941
|
+
this.logger.info(`[${plugin.name}] Effective configuration:`);
|
|
1942
|
+
this.logger.info(JSON.stringify(pluginOptions, null, 2));
|
|
1943
|
+
}
|
|
1944
|
+
if (effectiveOptions.debug?.showConfigSources) {
|
|
1945
|
+
this.logger.info(`[${plugin.name}] Configuration sources:`);
|
|
1946
|
+
this.logger.info(` Default options: ${JSON.stringify(defaultOptions, null, 2)}`);
|
|
1947
|
+
this.logger.info(` Preset/User options: ${JSON.stringify(presetOptions, null, 2)}`);
|
|
1948
|
+
}
|
|
1949
|
+
const result = await plugin.apply(currentData, pluginOptions, context);
|
|
1950
|
+
if (result.changed) {
|
|
1951
|
+
currentData = result.data;
|
|
1952
|
+
stats.pluginsApplied.push(plugin.name);
|
|
1953
|
+
}
|
|
1954
|
+
if (plugin.validate) {
|
|
1955
|
+
const isValid = await plugin.validate(result);
|
|
1956
|
+
if (!isValid) {
|
|
1957
|
+
this.logger.warn(`Plugin ${plugin.name} validation failed`);
|
|
1958
|
+
warnings.push(`Plugin ${plugin.name} validation failed`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
const sizeAfter = calculateSize(currentData);
|
|
1962
|
+
const processingTime = Date.now() - pluginStartTime;
|
|
1963
|
+
pluginReports.push({
|
|
1964
|
+
pluginName: plugin.name,
|
|
1965
|
+
sizeBefore: result.stats.sizeBefore || sizeBefore,
|
|
1966
|
+
sizeAfter: result.stats.sizeAfter || sizeAfter,
|
|
1967
|
+
itemsProcessed: result.stats.itemsProcessed,
|
|
1968
|
+
itemsRemoved: result.stats.itemsRemoved,
|
|
1969
|
+
processingTime
|
|
1970
|
+
});
|
|
1971
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
1972
|
+
if (result.errors) errors.push(...result.errors);
|
|
1973
|
+
const reportedSizeBefore = result.stats.sizeBefore || sizeBefore;
|
|
1974
|
+
const reportedSizeAfter = result.stats.sizeAfter || sizeAfter;
|
|
1975
|
+
this.logger.debug(`Plugin ${plugin.name} completed: ${reportedSizeBefore} -> ${reportedSizeAfter} bytes`);
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
const errorMsg = `Plugin ${plugin.name} failed: ${error}`;
|
|
1978
|
+
this.logger.error(errorMsg);
|
|
1979
|
+
errors.push(errorMsg);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
const finalSize = calculateSize(currentData);
|
|
1983
|
+
stats.optimizedSize = finalSize;
|
|
1984
|
+
stats.compressionRatio = (originalSize - finalSize) / originalSize * 100;
|
|
1985
|
+
stats.processingTime = Date.now() - startTime;
|
|
1986
|
+
const summary = this.generateSummary(stats);
|
|
1987
|
+
this.logger.info(`Optimization complete: ${originalSize} -> ${finalSize} bytes (${stats.compressionRatio.toFixed(1)}% reduction)`);
|
|
1988
|
+
return {
|
|
1989
|
+
data: currentData,
|
|
1990
|
+
stats,
|
|
1991
|
+
report: {
|
|
1992
|
+
summary,
|
|
1993
|
+
details: pluginReports,
|
|
1994
|
+
warnings,
|
|
1995
|
+
errors
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
async resolvePlugins(effectiveOptions) {
|
|
2000
|
+
let pluginNames = [];
|
|
2001
|
+
if (effectiveOptions.plugins) if ('string' != typeof effectiveOptions.plugins[0]) return effectiveOptions.plugins;
|
|
2002
|
+
else pluginNames = effectiveOptions.plugins;
|
|
2003
|
+
else pluginNames = Array.from(this.plugins.keys());
|
|
2004
|
+
if (effectiveOptions.disable) {
|
|
2005
|
+
const disabled = Array.isArray(effectiveOptions.disable) ? effectiveOptions.disable : [
|
|
2006
|
+
effectiveOptions.disable
|
|
2007
|
+
];
|
|
2008
|
+
pluginNames = pluginNames.filter((name)=>!disabled.includes(name));
|
|
2009
|
+
}
|
|
2010
|
+
const resolvedPlugins = [];
|
|
2011
|
+
for (const name of pluginNames){
|
|
2012
|
+
const plugin = this.plugins.get(name);
|
|
2013
|
+
if (plugin) resolvedPlugins.push(plugin);
|
|
2014
|
+
else this.logger.warn(`Plugin "${name}" not found`);
|
|
2015
|
+
}
|
|
2016
|
+
return resolvedPlugins;
|
|
2017
|
+
}
|
|
2018
|
+
generateSummary(stats) {
|
|
2019
|
+
const reduction = stats.compressionRatio.toFixed(1);
|
|
2020
|
+
const time = (stats.processingTime / 1000).toFixed(2);
|
|
2021
|
+
return `Optimized ${stats.originalSize} -> ${stats.optimizedSize} bytes (${reduction}% reduction) in ${time}s using ${stats.pluginsApplied.length} plugins`;
|
|
2022
|
+
}
|
|
2023
|
+
getAvailablePlugins() {
|
|
2024
|
+
return Array.from(this.plugins.keys());
|
|
2025
|
+
}
|
|
2026
|
+
getPlugin(name) {
|
|
2027
|
+
return this.plugins.get(name);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
class LottieValidator {
|
|
2031
|
+
async validate(data) {
|
|
2032
|
+
const errors = [];
|
|
2033
|
+
const warnings = [];
|
|
2034
|
+
this.validateBasicStructure(data, errors, warnings);
|
|
2035
|
+
this.validateLayers(data, errors, warnings);
|
|
2036
|
+
this.validateAssets(data, errors, warnings);
|
|
2037
|
+
this.validateAnimation(data, errors, warnings);
|
|
2038
|
+
const metrics = this.calculateQualityMetrics(data);
|
|
2039
|
+
return {
|
|
2040
|
+
isValid: 0 === errors.length,
|
|
2041
|
+
errors,
|
|
2042
|
+
warnings,
|
|
2043
|
+
metrics
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
async compare(original, optimized) {
|
|
2047
|
+
const originalSize = JSON.stringify(original).length;
|
|
2048
|
+
const optimizedSize = JSON.stringify(optimized).length;
|
|
2049
|
+
const compressionRatio = (originalSize - optimizedSize) / originalSize;
|
|
2050
|
+
const sizeReduction = originalSize - optimizedSize;
|
|
2051
|
+
const originalResult = await this.validate(original);
|
|
2052
|
+
const optimizedResult = await this.validate(optimized);
|
|
2053
|
+
const qualityLoss = this.calculateQualityLoss(originalResult.metrics, optimizedResult.metrics);
|
|
2054
|
+
const timingDiff = this.calculateTimingDifference(original, optimized);
|
|
2055
|
+
const pathDiff = this.calculatePathDifference(original, optimized);
|
|
2056
|
+
const colorDiff = this.calculateColorDifference(original, optimized);
|
|
2057
|
+
const compatibilityScore = optimizedResult.isValid ? 1.0 : 0.5;
|
|
2058
|
+
return {
|
|
2059
|
+
compressionRatio,
|
|
2060
|
+
sizeReduction,
|
|
2061
|
+
qualityLoss,
|
|
2062
|
+
timingDifference: timingDiff,
|
|
2063
|
+
pathDifference: pathDiff,
|
|
2064
|
+
colorDifference: colorDiff,
|
|
2065
|
+
compatibilityScore
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
validateBasicStructure(data, errors, warnings) {
|
|
2069
|
+
if (!data.v) errors.push({
|
|
2070
|
+
code: 'MISSING_VERSION',
|
|
2071
|
+
message: 'Missing version field',
|
|
2072
|
+
severity: 'critical'
|
|
2073
|
+
});
|
|
2074
|
+
if (!data.fr || data.fr <= 0) errors.push({
|
|
2075
|
+
code: 'INVALID_FRAMERATE',
|
|
2076
|
+
message: 'Invalid or missing frame rate',
|
|
2077
|
+
severity: 'high'
|
|
2078
|
+
});
|
|
2079
|
+
if (void 0 === data.ip || void 0 === data.op || data.op <= data.ip) errors.push({
|
|
2080
|
+
code: 'INVALID_DURATION',
|
|
2081
|
+
message: 'Invalid animation duration',
|
|
2082
|
+
severity: 'high'
|
|
2083
|
+
});
|
|
2084
|
+
if (!data.w || !data.h || data.w <= 0 || data.h <= 0) errors.push({
|
|
2085
|
+
code: 'INVALID_DIMENSIONS',
|
|
2086
|
+
message: 'Invalid canvas dimensions',
|
|
2087
|
+
severity: 'medium'
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
validateLayers(data, errors, warnings) {
|
|
2091
|
+
if (!data.layers || !Array.isArray(data.layers)) return void errors.push({
|
|
2092
|
+
code: 'MISSING_LAYERS',
|
|
2093
|
+
message: 'No layers found',
|
|
2094
|
+
severity: 'critical'
|
|
2095
|
+
});
|
|
2096
|
+
data.layers.forEach((layer, index)=>{
|
|
2097
|
+
const layerPath = `layers[${index}]`;
|
|
2098
|
+
if (void 0 === layer.ty) errors.push({
|
|
2099
|
+
code: 'MISSING_LAYER_TYPE',
|
|
2100
|
+
message: 'Layer missing type',
|
|
2101
|
+
severity: 'high',
|
|
2102
|
+
path: layerPath
|
|
2103
|
+
});
|
|
2104
|
+
if (void 0 !== layer.ip && void 0 !== layer.op && layer.op <= layer.ip) warnings.push({
|
|
2105
|
+
code: 'INVALID_LAYER_DURATION',
|
|
2106
|
+
message: 'Layer has invalid duration',
|
|
2107
|
+
path: layerPath
|
|
2108
|
+
});
|
|
2109
|
+
if (2 === layer.ty && layer.refId) {
|
|
2110
|
+
const asset = data.assets?.find((a)=>a.id === layer.refId);
|
|
2111
|
+
if (!asset) errors.push({
|
|
2112
|
+
code: 'MISSING_ASSET_REFERENCE',
|
|
2113
|
+
message: `Layer references missing asset: ${layer.refId}`,
|
|
2114
|
+
severity: 'high',
|
|
2115
|
+
path: layerPath
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
validateAssets(data, errors, warnings) {
|
|
2121
|
+
if (!data.assets) return;
|
|
2122
|
+
data.assets.forEach((asset, index)=>{
|
|
2123
|
+
const assetPath = `assets[${index}]`;
|
|
2124
|
+
if (!asset.id) errors.push({
|
|
2125
|
+
code: 'MISSING_ASSET_ID',
|
|
2126
|
+
message: 'Asset missing ID',
|
|
2127
|
+
severity: 'high',
|
|
2128
|
+
path: assetPath
|
|
2129
|
+
});
|
|
2130
|
+
if (asset.p) {
|
|
2131
|
+
if (1 === asset.e) {
|
|
2132
|
+
if (!asset.p.startsWith('data:')) warnings.push({
|
|
2133
|
+
code: 'INVALID_EMBEDDED_DATA',
|
|
2134
|
+
message: 'Embedded asset has invalid data format',
|
|
2135
|
+
path: assetPath
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
validateAnimation(data, errors, warnings) {
|
|
2142
|
+
const duration = data.op - data.ip;
|
|
2143
|
+
if (duration <= 0) errors.push({
|
|
2144
|
+
code: 'ZERO_DURATION',
|
|
2145
|
+
message: 'Animation has zero or negative duration',
|
|
2146
|
+
severity: 'high'
|
|
2147
|
+
});
|
|
2148
|
+
if (data.fr && (data.fr < 12 || data.fr > 120)) warnings.push({
|
|
2149
|
+
code: 'UNUSUAL_FRAMERATE',
|
|
2150
|
+
message: `Unusual frame rate: ${data.fr}fps`
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
calculateQualityMetrics(data) {
|
|
2154
|
+
const structuralIntegrity = this.calculateStructuralIntegrity(data);
|
|
2155
|
+
const animationContinuity = this.calculateAnimationContinuity(data);
|
|
2156
|
+
const dataConsistency = this.calculateDataConsistency(data);
|
|
2157
|
+
const performanceScore = this.calculatePerformanceScore(data);
|
|
2158
|
+
return {
|
|
2159
|
+
structuralIntegrity,
|
|
2160
|
+
animationContinuity,
|
|
2161
|
+
dataConsistency,
|
|
2162
|
+
performanceScore,
|
|
2163
|
+
frameCount: data.op - data.ip,
|
|
2164
|
+
layerCount: data.layers?.length || 0,
|
|
2165
|
+
assetCount: data.assets?.length || 0,
|
|
2166
|
+
totalKeyframes: this.countTotalKeyframes(data),
|
|
2167
|
+
averageComplexity: this.calculateAverageComplexity(data)
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
calculateStructuralIntegrity(data) {
|
|
2171
|
+
let score = 1.0;
|
|
2172
|
+
if (!data.layers || 0 === data.layers.length) score -= 0.5;
|
|
2173
|
+
if (!data.fr || data.fr <= 0) score -= 0.3;
|
|
2174
|
+
if (!data.v) score -= 0.2;
|
|
2175
|
+
return Math.max(0, score);
|
|
2176
|
+
}
|
|
2177
|
+
calculateAnimationContinuity(data) {
|
|
2178
|
+
let continuityScore = 1.0;
|
|
2179
|
+
if (data.layers) {
|
|
2180
|
+
for (const layer of data.layers)if (layer.ks) continuityScore *= this.checkTransformContinuity(layer.ks);
|
|
2181
|
+
}
|
|
2182
|
+
return continuityScore;
|
|
2183
|
+
}
|
|
2184
|
+
calculateDataConsistency(data) {
|
|
2185
|
+
let consistencyScore = 1.0;
|
|
2186
|
+
if (data.layers && data.assets) {
|
|
2187
|
+
for (const layer of data.layers)if (layer.refId) {
|
|
2188
|
+
const asset = data.assets.find((a)=>a.id === layer.refId);
|
|
2189
|
+
if (!asset) consistencyScore -= 0.1;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return Math.max(0, consistencyScore);
|
|
2193
|
+
}
|
|
2194
|
+
calculatePerformanceScore(data) {
|
|
2195
|
+
let score = 1.0;
|
|
2196
|
+
const layerCount = data.layers?.length || 0;
|
|
2197
|
+
const assetCount = data.assets?.length || 0;
|
|
2198
|
+
const totalKeyframes = this.countTotalKeyframes(data);
|
|
2199
|
+
if (layerCount > 50) score -= 0.2;
|
|
2200
|
+
if (assetCount > 20) score -= 0.2;
|
|
2201
|
+
if (totalKeyframes > 1000) score -= 0.3;
|
|
2202
|
+
return Math.max(0, score);
|
|
2203
|
+
}
|
|
2204
|
+
countTotalKeyframes(data) {
|
|
2205
|
+
let total = 0;
|
|
2206
|
+
if (data.layers) for (const layer of data.layers)total += this.countLayerKeyframes(layer);
|
|
2207
|
+
return total;
|
|
2208
|
+
}
|
|
2209
|
+
countLayerKeyframes(layer) {
|
|
2210
|
+
let count = 0;
|
|
2211
|
+
if (layer.ks) [
|
|
2212
|
+
'p',
|
|
2213
|
+
's',
|
|
2214
|
+
'r',
|
|
2215
|
+
'a',
|
|
2216
|
+
'o'
|
|
2217
|
+
].forEach((prop)=>{
|
|
2218
|
+
if (layer.ks[prop] && 1 === layer.ks[prop].a && Array.isArray(layer.ks[prop].k)) count += layer.ks[prop].k.length;
|
|
2219
|
+
});
|
|
2220
|
+
return count;
|
|
2221
|
+
}
|
|
2222
|
+
calculateAverageComplexity(data) {
|
|
2223
|
+
const layerCount = data.layers?.length || 1;
|
|
2224
|
+
const totalKeyframes = this.countTotalKeyframes(data);
|
|
2225
|
+
return totalKeyframes / layerCount;
|
|
2226
|
+
}
|
|
2227
|
+
checkTransformContinuity(transform) {
|
|
2228
|
+
return 1.0;
|
|
2229
|
+
}
|
|
2230
|
+
calculateQualityLoss(originalMetrics, optimizedMetrics) {
|
|
2231
|
+
const structuralLoss = Math.max(0, originalMetrics.structuralIntegrity - optimizedMetrics.structuralIntegrity);
|
|
2232
|
+
const animationLoss = Math.max(0, originalMetrics.animationContinuity - optimizedMetrics.animationContinuity);
|
|
2233
|
+
const dataLoss = Math.max(0, originalMetrics.dataConsistency - optimizedMetrics.dataConsistency);
|
|
2234
|
+
const performanceLoss = Math.max(0, originalMetrics.performanceScore - optimizedMetrics.performanceScore);
|
|
2235
|
+
return 0.3 * structuralLoss + 0.4 * animationLoss + 0.2 * dataLoss + 0.1 * performanceLoss;
|
|
2236
|
+
}
|
|
2237
|
+
calculateTimingDifference(original, optimized) {
|
|
2238
|
+
const originalDuration = original.op - original.ip;
|
|
2239
|
+
const optimizedDuration = optimized.op - optimized.ip;
|
|
2240
|
+
return Math.abs(originalDuration - optimizedDuration) / originalDuration;
|
|
2241
|
+
}
|
|
2242
|
+
calculatePathDifference(original, optimized) {
|
|
2243
|
+
return 0.0;
|
|
2244
|
+
}
|
|
2245
|
+
calculateColorDifference(original, optimized) {
|
|
2246
|
+
return 0.0;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const validation_validator = new LottieValidator();
|
|
2250
|
+
class VisualValidator {
|
|
2251
|
+
async validatePlayback(data) {
|
|
2252
|
+
const errors = [];
|
|
2253
|
+
const startTime = Date.now();
|
|
2254
|
+
try {
|
|
2255
|
+
const canRender = await this.simulateRendering(data, errors);
|
|
2256
|
+
const performanceMetrics = this.calculateRenderingMetrics(data, Date.now() - startTime);
|
|
2257
|
+
return {
|
|
2258
|
+
canRender,
|
|
2259
|
+
renderingErrors: errors,
|
|
2260
|
+
performanceMetrics,
|
|
2261
|
+
visualDifference: 0
|
|
2262
|
+
};
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
errors.push({
|
|
2265
|
+
code: 'VALIDATION_FAILED',
|
|
2266
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2267
|
+
timestamp: Date.now()
|
|
2268
|
+
});
|
|
2269
|
+
return {
|
|
2270
|
+
canRender: false,
|
|
2271
|
+
renderingErrors: errors,
|
|
2272
|
+
performanceMetrics: this.getDefaultMetrics(),
|
|
2273
|
+
visualDifference: 1.0
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
async compareVisual(original, optimized) {
|
|
2278
|
+
const originalResult = await this.validatePlayback(original);
|
|
2279
|
+
const optimizedResult = await this.validatePlayback(optimized);
|
|
2280
|
+
const visualDifference = this.calculateVisualDifference(original, optimized);
|
|
2281
|
+
return {
|
|
2282
|
+
canRender: optimizedResult.canRender,
|
|
2283
|
+
renderingErrors: [
|
|
2284
|
+
...originalResult.renderingErrors,
|
|
2285
|
+
...optimizedResult.renderingErrors
|
|
2286
|
+
],
|
|
2287
|
+
performanceMetrics: optimizedResult.performanceMetrics,
|
|
2288
|
+
visualDifference
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
async performPlaybackTest(data, options = {}) {
|
|
2292
|
+
const errors = [];
|
|
2293
|
+
const startTime = Date.now();
|
|
2294
|
+
try {
|
|
2295
|
+
const testDuration = options.duration || (data.op - data.ip) / data.fr * 1000;
|
|
2296
|
+
const targetFPS = options.framerate || data.fr;
|
|
2297
|
+
const success = await this.simulatePlayback(data, testDuration, targetFPS, errors);
|
|
2298
|
+
const actualDuration = Date.now() - startTime;
|
|
2299
|
+
const metrics = this.calculateRenderingMetrics(data, actualDuration);
|
|
2300
|
+
return {
|
|
2301
|
+
duration: actualDuration,
|
|
2302
|
+
success,
|
|
2303
|
+
errors,
|
|
2304
|
+
metrics
|
|
2305
|
+
};
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
2308
|
+
return {
|
|
2309
|
+
duration: Date.now() - startTime,
|
|
2310
|
+
success: false,
|
|
2311
|
+
errors,
|
|
2312
|
+
metrics: this.getDefaultMetrics()
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
async simulateRendering(data, errors) {
|
|
2317
|
+
if (!this.checkBasicRenderRequirements(data, errors)) return false;
|
|
2318
|
+
if (!this.checkLayerRendering(data, errors)) return false;
|
|
2319
|
+
if (!this.checkAssetAvailability(data, errors)) return false;
|
|
2320
|
+
if (!this.checkAnimationTiming(data, errors)) return false;
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
checkBasicRenderRequirements(data, errors) {
|
|
2324
|
+
let canRender = true;
|
|
2325
|
+
if (!data.w || !data.h || data.w <= 0 || data.h <= 0) {
|
|
2326
|
+
errors.push({
|
|
2327
|
+
code: 'INVALID_CANVAS_SIZE',
|
|
2328
|
+
message: 'Invalid canvas dimensions for rendering',
|
|
2329
|
+
timestamp: Date.now()
|
|
2330
|
+
});
|
|
2331
|
+
canRender = false;
|
|
2332
|
+
}
|
|
2333
|
+
if (!data.fr || data.fr <= 0) {
|
|
2334
|
+
errors.push({
|
|
2335
|
+
code: 'INVALID_FRAMERATE',
|
|
2336
|
+
message: 'Invalid frame rate for rendering',
|
|
2337
|
+
timestamp: Date.now()
|
|
2338
|
+
});
|
|
2339
|
+
canRender = false;
|
|
2340
|
+
}
|
|
2341
|
+
if (data.op <= data.ip) {
|
|
2342
|
+
errors.push({
|
|
2343
|
+
code: 'INVALID_ANIMATION_RANGE',
|
|
2344
|
+
message: 'Invalid animation time range',
|
|
2345
|
+
timestamp: Date.now()
|
|
2346
|
+
});
|
|
2347
|
+
canRender = false;
|
|
2348
|
+
}
|
|
2349
|
+
return canRender;
|
|
2350
|
+
}
|
|
2351
|
+
checkLayerRendering(data, errors) {
|
|
2352
|
+
if (!data.layers || 0 === data.layers.length) {
|
|
2353
|
+
errors.push({
|
|
2354
|
+
code: 'NO_RENDERABLE_LAYERS',
|
|
2355
|
+
message: 'No layers available for rendering',
|
|
2356
|
+
timestamp: Date.now()
|
|
2357
|
+
});
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
let hasRenderableLayer = false;
|
|
2361
|
+
for(let i = 0; i < data.layers.length; i++){
|
|
2362
|
+
const layer = data.layers[i];
|
|
2363
|
+
if (this.isSupportedLayerType(layer.ty)) hasRenderableLayer = true;
|
|
2364
|
+
else errors.push({
|
|
2365
|
+
code: 'UNSUPPORTED_LAYER_TYPE',
|
|
2366
|
+
message: `Unsupported layer type: ${layer.ty}`,
|
|
2367
|
+
timestamp: Date.now()
|
|
2368
|
+
});
|
|
2369
|
+
if (void 0 !== layer.ip && void 0 !== layer.op && layer.op <= layer.ip) errors.push({
|
|
2370
|
+
code: 'INVALID_LAYER_TIME_RANGE',
|
|
2371
|
+
message: `Layer ${i} has invalid time range`,
|
|
2372
|
+
timestamp: Date.now()
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
return hasRenderableLayer;
|
|
2376
|
+
}
|
|
2377
|
+
isSupportedLayerType(type) {
|
|
2378
|
+
const supportedTypes = [
|
|
2379
|
+
0,
|
|
2380
|
+
1,
|
|
2381
|
+
2,
|
|
2382
|
+
3,
|
|
2383
|
+
4,
|
|
2384
|
+
5
|
|
2385
|
+
];
|
|
2386
|
+
return supportedTypes.includes(type);
|
|
2387
|
+
}
|
|
2388
|
+
checkAssetAvailability(data, errors) {
|
|
2389
|
+
if (!data.assets) return true;
|
|
2390
|
+
let allAssetsAvailable = true;
|
|
2391
|
+
for(let i = 0; i < data.assets.length; i++){
|
|
2392
|
+
const asset = data.assets[i];
|
|
2393
|
+
if (asset.p) {
|
|
2394
|
+
if (1 === asset.e) {
|
|
2395
|
+
if (!asset.p.startsWith('data:')) {
|
|
2396
|
+
errors.push({
|
|
2397
|
+
code: 'INVALID_EMBEDDED_ASSET',
|
|
2398
|
+
message: `Invalid embedded asset data: ${asset.id}`,
|
|
2399
|
+
timestamp: Date.now()
|
|
2400
|
+
});
|
|
2401
|
+
allAssetsAvailable = false;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
return allAssetsAvailable;
|
|
2407
|
+
}
|
|
2408
|
+
checkAnimationTiming(data, errors) {
|
|
2409
|
+
const duration = (data.op - data.ip) / data.fr;
|
|
2410
|
+
if (duration <= 0) {
|
|
2411
|
+
errors.push({
|
|
2412
|
+
code: 'ZERO_ANIMATION_DURATION',
|
|
2413
|
+
message: 'Animation has zero duration',
|
|
2414
|
+
timestamp: Date.now()
|
|
2415
|
+
});
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
if (duration > 300) errors.push({
|
|
2419
|
+
code: 'EXCESSIVE_DURATION',
|
|
2420
|
+
message: `Animation duration too long: ${duration}s`,
|
|
2421
|
+
timestamp: Date.now()
|
|
2422
|
+
});
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
async simulatePlayback(data, duration, targetFPS, errors) {
|
|
2426
|
+
const totalFrames = Math.ceil(duration * targetFPS / 1000);
|
|
2427
|
+
const frameInterval = 1000 / targetFPS;
|
|
2428
|
+
try {
|
|
2429
|
+
for(let frame = 0; frame < totalFrames; frame++){
|
|
2430
|
+
const currentTime = data.ip + frame / targetFPS * data.fr;
|
|
2431
|
+
if (!this.canRenderFrame(data, currentTime)) errors.push(`Frame ${frame} cannot be rendered`);
|
|
2432
|
+
await this.simulateFrameDelay(frameInterval);
|
|
2433
|
+
}
|
|
2434
|
+
return 0 === errors.length;
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
errors.push(`Playback simulation failed: ${error}`);
|
|
2437
|
+
return false;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
canRenderFrame(data, time) {
|
|
2441
|
+
if (!data.layers) return false;
|
|
2442
|
+
for (const layer of data.layers){
|
|
2443
|
+
const layerStart = layer.ip || data.ip;
|
|
2444
|
+
const layerEnd = layer.op || data.op;
|
|
2445
|
+
if (time >= layerStart && time <= layerEnd) return true;
|
|
2446
|
+
}
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
async simulateFrameDelay(interval) {
|
|
2450
|
+
const renderTime = Math.random() * interval * 0.1;
|
|
2451
|
+
await new Promise((resolve)=>setTimeout(resolve, renderTime));
|
|
2452
|
+
}
|
|
2453
|
+
calculateRenderingMetrics(data, actualDuration) {
|
|
2454
|
+
const totalFrames = data.op - data.ip;
|
|
2455
|
+
const expectedDuration = totalFrames / data.fr * 1000;
|
|
2456
|
+
const averageFPS = totalFrames / (actualDuration / 1000);
|
|
2457
|
+
const layerCount = data.layers?.length || 0;
|
|
2458
|
+
const assetCount = data.assets?.length || 0;
|
|
2459
|
+
const memoryUsage = (0.5 * layerCount + 2 * assetCount) * (data.w * data.h) / 1048576;
|
|
2460
|
+
const droppedFrames = Math.max(0, Math.floor((actualDuration - expectedDuration) / (1000 / data.fr)));
|
|
2461
|
+
return {
|
|
2462
|
+
averageFPS,
|
|
2463
|
+
memoryUsage,
|
|
2464
|
+
renderTime: actualDuration,
|
|
2465
|
+
droppedFrames,
|
|
2466
|
+
totalFrames
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
getDefaultMetrics() {
|
|
2470
|
+
return {
|
|
2471
|
+
averageFPS: 0,
|
|
2472
|
+
memoryUsage: 0,
|
|
2473
|
+
renderTime: 0,
|
|
2474
|
+
droppedFrames: 0,
|
|
2475
|
+
totalFrames: 0
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
calculateVisualDifference(original, optimized) {
|
|
2479
|
+
let difference = 0;
|
|
2480
|
+
if (original.w !== optimized.w || original.h !== optimized.h) difference += 0.1;
|
|
2481
|
+
if (Math.abs(original.fr - optimized.fr) > 0.1) difference += 0.1;
|
|
2482
|
+
const originalLayers = original.layers?.length || 0;
|
|
2483
|
+
const optimizedLayers = optimized.layers?.length || 0;
|
|
2484
|
+
const layerDifference = Math.abs(originalLayers - optimizedLayers) / Math.max(originalLayers, 1);
|
|
2485
|
+
difference += 0.3 * layerDifference;
|
|
2486
|
+
const originalAssets = original.assets?.length || 0;
|
|
2487
|
+
const optimizedAssets = optimized.assets?.length || 0;
|
|
2488
|
+
const assetDifference = Math.abs(originalAssets - optimizedAssets) / Math.max(originalAssets, 1);
|
|
2489
|
+
difference += 0.2 * assetDifference;
|
|
2490
|
+
const originalDuration = original.op - original.ip;
|
|
2491
|
+
const optimizedDuration = optimized.op - optimized.ip;
|
|
2492
|
+
const durationDifference = Math.abs(originalDuration - optimizedDuration) / originalDuration;
|
|
2493
|
+
difference += 0.3 * durationDifference;
|
|
2494
|
+
return Math.min(1.0, difference);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
const visualValidator = new VisualValidator();
|
|
2498
|
+
async function optimize(data, options = {}, inputFileSize) {
|
|
2499
|
+
const optimizer = new LottieOptimizer();
|
|
2500
|
+
return optimizer.optimize(data, options, inputFileSize);
|
|
2501
|
+
}
|
|
2502
|
+
class ComparisonTool {
|
|
2503
|
+
async runComparisonTest(lottieData, configurations = [
|
|
2504
|
+
{
|
|
2505
|
+
name: 'basic',
|
|
2506
|
+
options: {
|
|
2507
|
+
plugins: [
|
|
2508
|
+
'remove-unused-assets'
|
|
2509
|
+
]
|
|
2510
|
+
}
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
name: 'balanced',
|
|
2514
|
+
options: {
|
|
2515
|
+
plugins: [
|
|
2516
|
+
'remove-unused-assets',
|
|
2517
|
+
'simplify-paths'
|
|
2518
|
+
]
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
name: 'complete',
|
|
2523
|
+
options: {}
|
|
2524
|
+
}
|
|
2525
|
+
]) {
|
|
2526
|
+
const results = [];
|
|
2527
|
+
const originalValidation = await validation_validator.validate(lottieData);
|
|
2528
|
+
const originalVisual = await visualValidator.validatePlayback(lottieData);
|
|
2529
|
+
const originalSize = JSON.stringify(lottieData).length;
|
|
2530
|
+
for (const config of configurations)try {
|
|
2531
|
+
console.log(`\u{1F9EA} Testing configuration: ${config.name}`);
|
|
2532
|
+
const optimizeResult = await optimize(lottieData, config.options);
|
|
2533
|
+
const optimizedValidation = await validation_validator.validate(optimizeResult.data);
|
|
2534
|
+
const optimizedVisual = await visualValidator.validatePlayback(optimizeResult.data);
|
|
2535
|
+
const optimizedSize = JSON.stringify(optimizeResult.data).length;
|
|
2536
|
+
const comparison = await validation_validator.compare(lottieData, optimizeResult.data);
|
|
2537
|
+
const recommendation = this.calculateRecommendation(comparison, originalValidation, optimizedValidation);
|
|
2538
|
+
const summary = this.generateSummary(config.name, comparison, recommendation);
|
|
2539
|
+
results.push({
|
|
2540
|
+
preset: config.name,
|
|
2541
|
+
original: {
|
|
2542
|
+
size: originalSize,
|
|
2543
|
+
validationResult: originalValidation,
|
|
2544
|
+
visualResult: originalVisual
|
|
2545
|
+
},
|
|
2546
|
+
optimized: {
|
|
2547
|
+
size: optimizedSize,
|
|
2548
|
+
validationResult: optimizedValidation,
|
|
2549
|
+
visualResult: optimizedVisual
|
|
2550
|
+
},
|
|
2551
|
+
comparison,
|
|
2552
|
+
recommendation,
|
|
2553
|
+
summary
|
|
2554
|
+
});
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
console.error(`\u{274C} Failed to test configuration ${config.name}:`, error);
|
|
2557
|
+
results.push({
|
|
2558
|
+
preset: config.name,
|
|
2559
|
+
original: {
|
|
2560
|
+
size: originalSize,
|
|
2561
|
+
validationResult: originalValidation,
|
|
2562
|
+
visualResult: originalVisual
|
|
2563
|
+
},
|
|
2564
|
+
optimized: {
|
|
2565
|
+
size: originalSize,
|
|
2566
|
+
validationResult: {
|
|
2567
|
+
isValid: false,
|
|
2568
|
+
errors: [],
|
|
2569
|
+
warnings: [],
|
|
2570
|
+
metrics: {}
|
|
2571
|
+
},
|
|
2572
|
+
visualResult: {
|
|
2573
|
+
canRender: false,
|
|
2574
|
+
renderingErrors: [],
|
|
2575
|
+
performanceMetrics: {},
|
|
2576
|
+
visualDifference: 1
|
|
2577
|
+
}
|
|
2578
|
+
},
|
|
2579
|
+
comparison: {
|
|
2580
|
+
compressionRatio: 0,
|
|
2581
|
+
sizeReduction: 0,
|
|
2582
|
+
qualityLoss: 1,
|
|
2583
|
+
timingDifference: 1,
|
|
2584
|
+
pathDifference: 1,
|
|
2585
|
+
colorDifference: 1,
|
|
2586
|
+
compatibilityScore: 0
|
|
2587
|
+
},
|
|
2588
|
+
recommendation: 'failed',
|
|
2589
|
+
summary: `Optimization failed: ${error}`
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
return results;
|
|
2593
|
+
}
|
|
2594
|
+
async runTestSuite(suite) {
|
|
2595
|
+
console.log(`
|
|
2596
|
+
\u{1F3AF} Running test suite: ${suite.name}`);
|
|
2597
|
+
console.log(`\u{1F4DD} ${suite.description}\n`);
|
|
2598
|
+
const results = new Map();
|
|
2599
|
+
for (const testCase of suite.testCases){
|
|
2600
|
+
console.log(`
|
|
2601
|
+
\u{1F4CB} Test Case: ${testCase.name}`);
|
|
2602
|
+
try {
|
|
2603
|
+
const testResults = await this.runComparisonTest(testCase.lottieData);
|
|
2604
|
+
results.set(testCase.name, testResults);
|
|
2605
|
+
if (testCase.expectedResults) this.validateExpectedResults(testResults, testCase.expectedResults);
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
console.error(`\u{274C} Test case ${testCase.name} failed:`, error);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
return results;
|
|
2611
|
+
}
|
|
2612
|
+
generateReport(results) {
|
|
2613
|
+
const report = [];
|
|
2614
|
+
report.push('# Lottie Optimization Comparison Report\n');
|
|
2615
|
+
report.push(`Generated: ${new Date().toISOString()}\n`);
|
|
2616
|
+
report.push('## Summary\n');
|
|
2617
|
+
report.push('| Preset | Compression | Quality Loss | Recommendation | Status |');
|
|
2618
|
+
report.push('|--------|-------------|--------------|----------------|---------|');
|
|
2619
|
+
for (const result of results){
|
|
2620
|
+
const compression = `${(100 * result.comparison.compressionRatio).toFixed(1)}%`;
|
|
2621
|
+
const qualityLoss = `${(100 * result.comparison.qualityLoss).toFixed(1)}%`;
|
|
2622
|
+
const status = result.optimized.validationResult.isValid ? "\u2705" : "\u274C";
|
|
2623
|
+
report.push(`| ${result.preset} | ${compression} | ${qualityLoss} | ${result.recommendation} | ${status} |`);
|
|
2624
|
+
}
|
|
2625
|
+
report.push('\n');
|
|
2626
|
+
report.push('## Detailed Results\n');
|
|
2627
|
+
for (const result of results){
|
|
2628
|
+
report.push(`### ${result.preset.toUpperCase()} Preset\n`);
|
|
2629
|
+
report.push(`**Summary**: ${result.summary}\n`);
|
|
2630
|
+
report.push('**Metrics**:');
|
|
2631
|
+
report.push(`- Original Size: ${result.original.size.toLocaleString()} bytes`);
|
|
2632
|
+
report.push(`- Optimized Size: ${result.optimized.size.toLocaleString()} bytes`);
|
|
2633
|
+
report.push(`- Size Reduction: ${result.comparison.sizeReduction.toLocaleString()} bytes (${(100 * result.comparison.compressionRatio).toFixed(1)}%)`);
|
|
2634
|
+
report.push(`- Quality Loss: ${(100 * result.comparison.qualityLoss).toFixed(1)}%`);
|
|
2635
|
+
report.push(`- Compatibility Score: ${(100 * result.comparison.compatibilityScore).toFixed(1)}%`);
|
|
2636
|
+
report.push('\n**Validation Results**:');
|
|
2637
|
+
report.push(`- Original: ${result.original.validationResult.isValid ? 'Valid' : 'Invalid'} (${result.original.validationResult.errors.length} errors, ${result.original.validationResult.warnings.length} warnings)`);
|
|
2638
|
+
report.push(`- Optimized: ${result.optimized.validationResult.isValid ? 'Valid' : 'Invalid'} (${result.optimized.validationResult.errors.length} errors, ${result.optimized.validationResult.warnings.length} warnings)`);
|
|
2639
|
+
report.push('\n**Playback Test**:');
|
|
2640
|
+
report.push(`- Original: ${result.original.visualResult.canRender ? 'Can render' : 'Cannot render'}`);
|
|
2641
|
+
report.push(`- Optimized: ${result.optimized.visualResult.canRender ? 'Can render' : 'Cannot render'}`);
|
|
2642
|
+
report.push(`- Visual Difference: ${(100 * result.optimized.visualResult.visualDifference).toFixed(1)}%`);
|
|
2643
|
+
report.push('\n---\n');
|
|
2644
|
+
}
|
|
2645
|
+
report.push('## Recommendations\n');
|
|
2646
|
+
const bestResult = this.findBestResult(results);
|
|
2647
|
+
if (bestResult) {
|
|
2648
|
+
report.push(`\u{1F3C6} **Best Overall**: ${bestResult.preset} preset`);
|
|
2649
|
+
report.push(`- Compression: ${(100 * bestResult.comparison.compressionRatio).toFixed(1)}%`);
|
|
2650
|
+
report.push(`- Quality preserved: ${(100 - 100 * bestResult.comparison.qualityLoss).toFixed(1)}%`);
|
|
2651
|
+
report.push(`- ${bestResult.summary}`);
|
|
2652
|
+
}
|
|
2653
|
+
return report.join('\n');
|
|
2654
|
+
}
|
|
2655
|
+
calculateRecommendation(comparison, originalValidation, optimizedValidation) {
|
|
2656
|
+
if (!optimizedValidation.isValid) return 'failed';
|
|
2657
|
+
let score = 0;
|
|
2658
|
+
const compressionScore = Math.min(40, 100 * comparison.compressionRatio);
|
|
2659
|
+
score += compressionScore;
|
|
2660
|
+
const qualityScore = Math.max(0, 40 - 40 * comparison.qualityLoss);
|
|
2661
|
+
score += qualityScore;
|
|
2662
|
+
const compatibilityScore = 20 * comparison.compatibilityScore;
|
|
2663
|
+
score += compatibilityScore;
|
|
2664
|
+
if (score >= 80) return 'excellent';
|
|
2665
|
+
if (score >= 65) return 'good';
|
|
2666
|
+
if (score >= 50) return 'acceptable';
|
|
2667
|
+
if (score >= 30) return 'poor';
|
|
2668
|
+
return 'failed';
|
|
2669
|
+
}
|
|
2670
|
+
generateSummary(preset, comparison, recommendation) {
|
|
2671
|
+
const compression = (100 * comparison.compressionRatio).toFixed(1);
|
|
2672
|
+
const qualityLoss = (100 * comparison.qualityLoss).toFixed(1);
|
|
2673
|
+
switch(recommendation){
|
|
2674
|
+
case 'excellent':
|
|
2675
|
+
return `Excellent optimization: ${compression}% compression with only ${qualityLoss}% quality loss. Highly recommended.`;
|
|
2676
|
+
case 'good':
|
|
2677
|
+
return `Good optimization: ${compression}% compression with ${qualityLoss}% quality loss. Recommended for most use cases.`;
|
|
2678
|
+
case 'acceptable':
|
|
2679
|
+
return `Acceptable optimization: ${compression}% compression with ${qualityLoss}% quality loss. Consider if file size is critical.`;
|
|
2680
|
+
case 'poor':
|
|
2681
|
+
return `Poor optimization: Only ${compression}% compression with ${qualityLoss}% quality loss. Not recommended.`;
|
|
2682
|
+
case 'failed':
|
|
2683
|
+
return "Optimization failed or resulted in invalid file. Use with caution or try a different preset.";
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
validateExpectedResults(results, expected) {
|
|
2687
|
+
for (const result of results){
|
|
2688
|
+
if (expected.minCompressionRatio && result.comparison.compressionRatio < expected.minCompressionRatio) console.warn(`\u{26A0}\u{FE0F} ${result.preset}: Compression ratio ${(100 * result.comparison.compressionRatio).toFixed(1)}% below expected ${(100 * expected.minCompressionRatio).toFixed(1)}%`);
|
|
2689
|
+
if (expected.maxQualityLoss && result.comparison.qualityLoss > expected.maxQualityLoss) console.warn(`\u{26A0}\u{FE0F} ${result.preset}: Quality loss ${(100 * result.comparison.qualityLoss).toFixed(1)}% exceeds maximum ${(100 * expected.maxQualityLoss).toFixed(1)}%`);
|
|
2690
|
+
if (expected.mustPlayback && !result.optimized.visualResult.canRender) console.error(`\u{274C} ${result.preset}: File cannot playback but playback is required`);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
findBestResult(results) {
|
|
2694
|
+
if (0 === results.length) return null;
|
|
2695
|
+
const validResults = results.filter((r)=>r.optimized.validationResult.isValid);
|
|
2696
|
+
if (0 === validResults.length) return null;
|
|
2697
|
+
const recommendationOrder = [
|
|
2698
|
+
'excellent',
|
|
2699
|
+
'good',
|
|
2700
|
+
'acceptable',
|
|
2701
|
+
'poor',
|
|
2702
|
+
'failed'
|
|
2703
|
+
];
|
|
2704
|
+
for (const level of recommendationOrder){
|
|
2705
|
+
const candidates = validResults.filter((r)=>r.recommendation === level);
|
|
2706
|
+
if (candidates.length > 0) return candidates.reduce((best, current)=>current.comparison.compressionRatio > best.comparison.compressionRatio ? current : best);
|
|
2707
|
+
}
|
|
2708
|
+
return validResults[0];
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
const comparisonTool = new ComparisonTool();
|
|
2712
|
+
function createValidateCommand() {
|
|
2713
|
+
const validateCmd = new Command('validate');
|
|
2714
|
+
validateCmd.description('Validate and compare Lottie files').argument('<input>', 'Input Lottie file path').option('-o, --output <path>', 'Output directory for reports').option('-c, --configurations <configs...>', 'Plugin configurations to test', [
|
|
2715
|
+
'basic',
|
|
2716
|
+
'balanced',
|
|
2717
|
+
'complete'
|
|
2718
|
+
]).option('-r, --report', 'Generate detailed report', false).option('--visual', 'Include visual validation', false).option('--playback', 'Test playback capability', false).action(async (input, options)=>{
|
|
2719
|
+
try {
|
|
2720
|
+
console.log(chalk.blue("\uD83D\uDD0D Lottie Quality Validation\n"));
|
|
2721
|
+
const inputPath = external_path_resolve(input);
|
|
2722
|
+
const fileContent = await promises.readFile(inputPath, 'utf-8');
|
|
2723
|
+
const lottieData = JSON.parse(fileContent);
|
|
2724
|
+
const originalSize = fileContent.length;
|
|
2725
|
+
console.log(`\u{1F4C1} Input file: ${chalk.cyan(inputPath)}`);
|
|
2726
|
+
console.log(`\u{1F4CA} Original size: ${chalk.yellow(originalSize.toLocaleString())} bytes\n`);
|
|
2727
|
+
console.log(chalk.blue("\uD83D\uDCCB Basic Validation:"));
|
|
2728
|
+
const validationResult = await validation_validator.validate(lottieData);
|
|
2729
|
+
if (validationResult.isValid) console.log(chalk.green("\u2705 File structure is valid"));
|
|
2730
|
+
else {
|
|
2731
|
+
console.log(chalk.red("\u274C File structure has issues:"));
|
|
2732
|
+
validationResult.errors.forEach((error)=>{
|
|
2733
|
+
console.log(` ${chalk.red("\u2022")} ${error.message} (${error.severity})`);
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
if (validationResult.warnings.length > 0) {
|
|
2737
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F Warnings:"));
|
|
2738
|
+
validationResult.warnings.forEach((warning)=>{
|
|
2739
|
+
console.log(` ${chalk.yellow("\u2022")} ${warning.message}`);
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
console.log(chalk.blue("\n\uD83D\uDCC8 Quality Metrics:"));
|
|
2743
|
+
const metrics = validationResult.metrics;
|
|
2744
|
+
console.log(` Structural Integrity: ${chalk.cyan((100 * metrics.structuralIntegrity).toFixed(1))}%`);
|
|
2745
|
+
console.log(` Animation Continuity: ${chalk.cyan((100 * metrics.animationContinuity).toFixed(1))}%`);
|
|
2746
|
+
console.log(` Data Consistency: ${chalk.cyan((100 * metrics.dataConsistency).toFixed(1))}%`);
|
|
2747
|
+
console.log(` Performance Score: ${chalk.cyan((100 * metrics.performanceScore).toFixed(1))}%`);
|
|
2748
|
+
console.log(` Layers: ${chalk.cyan(metrics.layerCount)}, Assets: ${chalk.cyan(metrics.assetCount)}, Keyframes: ${chalk.cyan(metrics.totalKeyframes)}`);
|
|
2749
|
+
if (options.visual || options.playback) {
|
|
2750
|
+
console.log(chalk.blue("\n\uD83C\uDFAC Visual Validation:"));
|
|
2751
|
+
const visualResult = await visualValidator.validatePlayback(lottieData);
|
|
2752
|
+
if (visualResult.canRender) console.log(chalk.green("\u2705 Animation can render"));
|
|
2753
|
+
else {
|
|
2754
|
+
console.log(chalk.red("\u274C Animation cannot render:"));
|
|
2755
|
+
visualResult.renderingErrors.forEach((error)=>{
|
|
2756
|
+
console.log(` ${chalk.red("\u2022")} ${error.message}`);
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
const perfMetrics = visualResult.performanceMetrics;
|
|
2760
|
+
console.log(` Estimated FPS: ${chalk.cyan(perfMetrics.averageFPS.toFixed(1))}`);
|
|
2761
|
+
console.log(` Memory Usage: ${chalk.cyan(perfMetrics.memoryUsage.toFixed(1))} MB`);
|
|
2762
|
+
console.log(` Render Time: ${chalk.cyan(perfMetrics.renderTime)} ms`);
|
|
2763
|
+
}
|
|
2764
|
+
if (options.configurations.length > 0) {
|
|
2765
|
+
console.log(chalk.blue("\n\uD83E\uDDEA Compression Comparison:"));
|
|
2766
|
+
const configMap = {
|
|
2767
|
+
basic: {
|
|
2768
|
+
plugins: [
|
|
2769
|
+
'remove-unused-assets'
|
|
2770
|
+
]
|
|
2771
|
+
},
|
|
2772
|
+
balanced: {
|
|
2773
|
+
plugins: [
|
|
2774
|
+
'remove-unused-assets',
|
|
2775
|
+
'simplify-paths'
|
|
2776
|
+
]
|
|
2777
|
+
},
|
|
2778
|
+
complete: {}
|
|
2779
|
+
};
|
|
2780
|
+
const testConfigs = options.configurations.map((name)=>({
|
|
2781
|
+
name,
|
|
2782
|
+
options: configMap[name] || {}
|
|
2783
|
+
}));
|
|
2784
|
+
const comparisonResults = await comparisonTool.runComparisonTest(lottieData, testConfigs);
|
|
2785
|
+
console.log('\n' + chalk.bold('Configuration Comparison Results:'));
|
|
2786
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2787
|
+
console.log("\u2502 Config \u2502 Compression \u2502 Quality \u2502 Playback \u2502 Rating \u2502");
|
|
2788
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2789
|
+
for (const result of comparisonResults){
|
|
2790
|
+
const config = result.preset.padEnd(11);
|
|
2791
|
+
const compression = `${(100 * result.comparison.compressionRatio).toFixed(1)}%`.padStart(11);
|
|
2792
|
+
const quality = `${(100 - 100 * result.comparison.qualityLoss).toFixed(1)}%`.padStart(11);
|
|
2793
|
+
const playback = result.optimized.visualResult.canRender ? 'Yes'.padStart(12) : 'No'.padStart(12);
|
|
2794
|
+
let rating = '';
|
|
2795
|
+
let ratingColor = chalk.gray;
|
|
2796
|
+
switch(result.recommendation){
|
|
2797
|
+
case 'excellent':
|
|
2798
|
+
rating = 'Excellent';
|
|
2799
|
+
ratingColor = chalk.green;
|
|
2800
|
+
break;
|
|
2801
|
+
case 'good':
|
|
2802
|
+
rating = 'Good';
|
|
2803
|
+
ratingColor = chalk.cyan;
|
|
2804
|
+
break;
|
|
2805
|
+
case 'acceptable':
|
|
2806
|
+
rating = 'Acceptable';
|
|
2807
|
+
ratingColor = chalk.yellow;
|
|
2808
|
+
break;
|
|
2809
|
+
case 'poor':
|
|
2810
|
+
rating = 'Poor';
|
|
2811
|
+
ratingColor = chalk.red;
|
|
2812
|
+
break;
|
|
2813
|
+
case 'failed':
|
|
2814
|
+
rating = 'Failed';
|
|
2815
|
+
ratingColor = chalk.red;
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
console.log(`\u{2502} ${config} \u{2502} ${compression} \u{2502} ${quality} \u{2502} ${playback} \u{2502} ${ratingColor(rating.padEnd(10))} \u{2502}`);
|
|
2819
|
+
}
|
|
2820
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2821
|
+
const validResults = comparisonResults.filter((r)=>r.optimized.validationResult.isValid);
|
|
2822
|
+
if (validResults.length > 0) {
|
|
2823
|
+
const bestResult = validResults.reduce((best, current)=>{
|
|
2824
|
+
const bestScore = 0.6 * best.comparison.compressionRatio + (1 - best.comparison.qualityLoss) * 0.4;
|
|
2825
|
+
const currentScore = 0.6 * current.comparison.compressionRatio + (1 - current.comparison.qualityLoss) * 0.4;
|
|
2826
|
+
return currentScore > bestScore ? current : best;
|
|
2827
|
+
});
|
|
2828
|
+
console.log(chalk.green(`
|
|
2829
|
+
\u{1F3C6} Recommended: ${bestResult.preset.toUpperCase()} configuration`));
|
|
2830
|
+
console.log(` ${bestResult.summary}`);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
const shouldGenerateReport = options.report || process.argv.includes('--report') || process.argv.includes('-r');
|
|
2834
|
+
if (shouldGenerateReport) {
|
|
2835
|
+
const outputDir = options.output || dirname(inputPath);
|
|
2836
|
+
const reportPath = join(outputDir, `${basename(inputPath, '.json')}-validation-report.html`);
|
|
2837
|
+
let comparisonResults = [];
|
|
2838
|
+
if (options.configurations.length > 0) {
|
|
2839
|
+
console.log(chalk.blue("\n\uD83D\uDCCA Generating detailed report..."));
|
|
2840
|
+
const configMap = {
|
|
2841
|
+
basic: {
|
|
2842
|
+
plugins: [
|
|
2843
|
+
'remove-unused-assets'
|
|
2844
|
+
]
|
|
2845
|
+
},
|
|
2846
|
+
balanced: {
|
|
2847
|
+
plugins: [
|
|
2848
|
+
'remove-unused-assets',
|
|
2849
|
+
'simplify-paths'
|
|
2850
|
+
]
|
|
2851
|
+
},
|
|
2852
|
+
complete: {}
|
|
2853
|
+
};
|
|
2854
|
+
const testConfigs = options.configurations.map((name)=>({
|
|
2855
|
+
name,
|
|
2856
|
+
options: configMap[name] || {}
|
|
2857
|
+
}));
|
|
2858
|
+
comparisonResults = await comparisonTool.runComparisonTest(lottieData, testConfigs);
|
|
2859
|
+
}
|
|
2860
|
+
const htmlReport = await generateHTMLReport(lottieData, validationResult, comparisonResults);
|
|
2861
|
+
await promises.writeFile(reportPath, htmlReport);
|
|
2862
|
+
console.log(chalk.green(`
|
|
2863
|
+
\u{1F4BE} Detailed HTML report saved: ${reportPath}`));
|
|
2864
|
+
}
|
|
2865
|
+
if (!validationResult.isValid) process.exit(1);
|
|
2866
|
+
console.log(chalk.green("\n\u2705 Validation completed successfully!"));
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
console.error(chalk.red("\u274C Validation failed:"), error);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
return validateCmd;
|
|
2873
|
+
}
|
|
2874
|
+
async function generateHTMLReport(lottieData, validationResult, comparisonResults) {
|
|
2875
|
+
const timestamp = new Date().toISOString();
|
|
2876
|
+
const html = `
|
|
2877
|
+
<!DOCTYPE html>
|
|
2878
|
+
<html lang="en">
|
|
2879
|
+
<head>
|
|
2880
|
+
<meta charset="UTF-8">
|
|
2881
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2882
|
+
<title>Lottie Validation Report</title>
|
|
2883
|
+
<style>
|
|
2884
|
+
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
|
2885
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
2886
|
+
h1 { color: #333; border-bottom: 3px solid #007acc; padding-bottom: 10px; }
|
|
2887
|
+
h2 { color: #555; margin-top: 30px; }
|
|
2888
|
+
.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }
|
|
2889
|
+
.metric-card { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #007acc; }
|
|
2890
|
+
.metric-label { font-weight: bold; color: #666; font-size: 0.9em; }
|
|
2891
|
+
.metric-value { font-size: 1.4em; color: #333; margin-top: 5px; }
|
|
2892
|
+
.status-good { color: #28a745; }
|
|
2893
|
+
.status-warning { color: #ffc107; }
|
|
2894
|
+
.status-error { color: #dc3545; }
|
|
2895
|
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
2896
|
+
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
2897
|
+
th { background-color: #f8f9fa; font-weight: bold; }
|
|
2898
|
+
.recommendation { padding: 15px; border-radius: 8px; margin: 10px 0; }
|
|
2899
|
+
.rec-excellent { background: #d4edda; border-left: 4px solid #28a745; }
|
|
2900
|
+
.rec-good { background: #d1ecf1; border-left: 4px solid #17a2b8; }
|
|
2901
|
+
.rec-acceptable { background: #fff3cd; border-left: 4px solid #ffc107; }
|
|
2902
|
+
.rec-poor { background: #f8d7da; border-left: 4px solid #dc3545; }
|
|
2903
|
+
.timestamp { color: #666; font-size: 0.9em; margin-bottom: 20px; }
|
|
2904
|
+
</style>
|
|
2905
|
+
</head>
|
|
2906
|
+
<body>
|
|
2907
|
+
<div class="container">
|
|
2908
|
+
<h1>\u{1F50D} Lottie Validation Report</h1>
|
|
2909
|
+
<div class="timestamp">Generated: ${timestamp}</div>
|
|
2910
|
+
|
|
2911
|
+
<h2>\u{1F4CA} Basic Information</h2>
|
|
2912
|
+
<div class="metric-grid">
|
|
2913
|
+
<div class="metric-card">
|
|
2914
|
+
<div class="metric-label">File Size</div>
|
|
2915
|
+
<div class="metric-value">${JSON.stringify(lottieData).length.toLocaleString()} bytes</div>
|
|
2916
|
+
</div>
|
|
2917
|
+
<div class="metric-card">
|
|
2918
|
+
<div class="metric-label">Layers</div>
|
|
2919
|
+
<div class="metric-value">${validationResult.metrics.layerCount}</div>
|
|
2920
|
+
</div>
|
|
2921
|
+
<div class="metric-card">
|
|
2922
|
+
<div class="metric-label">Assets</div>
|
|
2923
|
+
<div class="metric-value">${validationResult.metrics.assetCount}</div>
|
|
2924
|
+
</div>
|
|
2925
|
+
<div class="metric-card">
|
|
2926
|
+
<div class="metric-label">Keyframes</div>
|
|
2927
|
+
<div class="metric-value">${validationResult.metrics.totalKeyframes}</div>
|
|
2928
|
+
</div>
|
|
2929
|
+
</div>
|
|
2930
|
+
|
|
2931
|
+
<h2>\u{2705} Validation Status</h2>
|
|
2932
|
+
<div class="metric-card">
|
|
2933
|
+
<div class="metric-label">Overall Status</div>
|
|
2934
|
+
<div class="metric-value ${validationResult.isValid ? 'status-good' : 'status-error'}">
|
|
2935
|
+
${validationResult.isValid ? "\u2705 Valid" : "\u274C Invalid"}
|
|
2936
|
+
</div>
|
|
2937
|
+
</div>
|
|
2938
|
+
|
|
2939
|
+
${validationResult.errors.length > 0 ? `
|
|
2940
|
+
<h3>\u{274C} Errors</h3>
|
|
2941
|
+
<ul>
|
|
2942
|
+
${validationResult.errors.map((error)=>`<li class="status-error">${error.message} (${error.severity})</li>`).join('')}
|
|
2943
|
+
</ul>
|
|
2944
|
+
` : ''}
|
|
2945
|
+
|
|
2946
|
+
${validationResult.warnings.length > 0 ? `
|
|
2947
|
+
<h3>\u{26A0}\u{FE0F} Warnings</h3>
|
|
2948
|
+
<ul>
|
|
2949
|
+
${validationResult.warnings.map((warning)=>`<li class="status-warning">${warning.message}</li>`).join('')}
|
|
2950
|
+
</ul>
|
|
2951
|
+
` : ''}
|
|
2952
|
+
|
|
2953
|
+
<h2>\u{1F4C8} Quality Metrics</h2>
|
|
2954
|
+
<div class="metric-grid">
|
|
2955
|
+
<div class="metric-card">
|
|
2956
|
+
<div class="metric-label">Structural Integrity</div>
|
|
2957
|
+
<div class="metric-value">${(100 * validationResult.metrics.structuralIntegrity).toFixed(1)}%</div>
|
|
2958
|
+
</div>
|
|
2959
|
+
<div class="metric-card">
|
|
2960
|
+
<div class="metric-label">Animation Continuity</div>
|
|
2961
|
+
<div class="metric-value">${(100 * validationResult.metrics.animationContinuity).toFixed(1)}%</div>
|
|
2962
|
+
</div>
|
|
2963
|
+
<div class="metric-card">
|
|
2964
|
+
<div class="metric-label">Data Consistency</div>
|
|
2965
|
+
<div class="metric-value">${(100 * validationResult.metrics.dataConsistency).toFixed(1)}%</div>
|
|
2966
|
+
</div>
|
|
2967
|
+
<div class="metric-card">
|
|
2968
|
+
<div class="metric-label">Performance Score</div>
|
|
2969
|
+
<div class="metric-value">${(100 * validationResult.metrics.performanceScore).toFixed(1)}%</div>
|
|
2970
|
+
</div>
|
|
2971
|
+
</div>
|
|
2972
|
+
|
|
2973
|
+
${comparisonResults.length > 0 ? `
|
|
2974
|
+
<h2>\u{1F9EA} Compression Comparison</h2>
|
|
2975
|
+
<table>
|
|
2976
|
+
<thead>
|
|
2977
|
+
<tr>
|
|
2978
|
+
<th>Configuration</th>
|
|
2979
|
+
<th>Compression</th>
|
|
2980
|
+
<th>Quality Preserved</th>
|
|
2981
|
+
<th>Recommendation</th>
|
|
2982
|
+
<th>Summary</th>
|
|
2983
|
+
</tr>
|
|
2984
|
+
</thead>
|
|
2985
|
+
<tbody>
|
|
2986
|
+
${comparisonResults.map((result)=>`
|
|
2987
|
+
<tr>
|
|
2988
|
+
<td><strong>${result.preset}</strong></td>
|
|
2989
|
+
<td>${(100 * result.comparison.compressionRatio).toFixed(1)}%</td>
|
|
2990
|
+
<td>${(100 - 100 * result.comparison.qualityLoss).toFixed(1)}%</td>
|
|
2991
|
+
<td class="rec-${result.recommendation}">${result.recommendation}</td>
|
|
2992
|
+
<td>${result.summary}</td>
|
|
2993
|
+
</tr>
|
|
2994
|
+
`).join('')}
|
|
2995
|
+
</tbody>
|
|
2996
|
+
</table>
|
|
2997
|
+
` : ''}
|
|
2998
|
+
|
|
2999
|
+
<h2>\u{1F3C6} Recommendations</h2>
|
|
3000
|
+
${comparisonResults.length > 0 ? (()=>{
|
|
3001
|
+
const validResults = comparisonResults.filter((r)=>r.optimized.validationResult.isValid);
|
|
3002
|
+
if (validResults.length > 0) {
|
|
3003
|
+
const bestResult = validResults.reduce((best, current)=>{
|
|
3004
|
+
const bestScore = 0.6 * best.comparison.compressionRatio + (1 - best.comparison.qualityLoss) * 0.4;
|
|
3005
|
+
const currentScore = 0.6 * current.comparison.compressionRatio + (1 - current.comparison.qualityLoss) * 0.4;
|
|
3006
|
+
return currentScore > bestScore ? current : best;
|
|
3007
|
+
});
|
|
3008
|
+
return `
|
|
3009
|
+
<div class="recommendation rec-${bestResult.recommendation}">
|
|
3010
|
+
<strong>Best Overall: ${bestResult.preset.toUpperCase()} configuration</strong><br>
|
|
3011
|
+
${bestResult.summary}
|
|
3012
|
+
</div>
|
|
3013
|
+
`;
|
|
3014
|
+
}
|
|
3015
|
+
return '<div class="recommendation rec-poor">No valid optimization results found.</div>';
|
|
3016
|
+
})() : '<div class="recommendation">No compression comparison performed.</div>'}
|
|
3017
|
+
</div>
|
|
3018
|
+
</body>
|
|
3019
|
+
</html>
|
|
3020
|
+
`.trim();
|
|
3021
|
+
return html;
|
|
3022
|
+
}
|
|
3023
|
+
const program = new Command();
|
|
3024
|
+
function parseNestedOption(target, path, value) {
|
|
3025
|
+
const keys = path.split('.');
|
|
3026
|
+
let current = target;
|
|
3027
|
+
for(let i = 0; i < keys.length - 1; i++){
|
|
3028
|
+
const key = keys[i];
|
|
3029
|
+
if (!(key in current)) current[key] = {};
|
|
3030
|
+
current = current[key];
|
|
3031
|
+
}
|
|
3032
|
+
const finalKey = keys[keys.length - 1];
|
|
3033
|
+
current[finalKey] = convertValue(value);
|
|
3034
|
+
}
|
|
3035
|
+
function convertValue(value) {
|
|
3036
|
+
if ('true' === value.toLowerCase()) return true;
|
|
3037
|
+
if ('false' === value.toLowerCase()) return false;
|
|
3038
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
3039
|
+
if (/^-?\d*\.\d+$/.test(value)) return parseFloat(value);
|
|
3040
|
+
return value;
|
|
3041
|
+
}
|
|
3042
|
+
program.name('lottie-opt').description('A plugin-based Lottie JSON optimizer').version('1.0.0');
|
|
3043
|
+
program.addCommand(createValidateCommand());
|
|
3044
|
+
program.argument('[input]', 'Input Lottie JSON file').argument('[output]', 'Output file (defaults to input file with .opt.json suffix)').option('--plugins <plugins>', 'Comma-separated list of plugins to use').option('--disable <plugins>', 'Comma-separated list of plugins to disable').option('--plugin-config <config>', 'Plugin configuration in format plugin.option=value,plugin2.option=value').option('-c, --config <path>', 'Path to configuration file').option('-v, --verbose', 'Enable verbose logging').option('-r, --report', 'Generate detailed optimization report').option('--dry-run', 'Analyze without saving output').option('--show-config', 'Show effective plugin configurations').option('--show-config-sources', 'Show configuration sources (default vs preset/user)').option('--list-plugins', 'List available plugins').action(async (input, output, options)=>{
|
|
3045
|
+
try {
|
|
3046
|
+
if (options.listPlugins) return void await listPlugins();
|
|
3047
|
+
if (!input) {
|
|
3048
|
+
console.error(chalk.red('Error: Input file is required when not using --list options'));
|
|
3049
|
+
process.exit(1);
|
|
3050
|
+
}
|
|
3051
|
+
if (!existsSync(input)) {
|
|
3052
|
+
console.error(chalk.red(`Error: Input file "${input}" not found`));
|
|
3053
|
+
process.exit(1);
|
|
3054
|
+
}
|
|
3055
|
+
if (!output) {
|
|
3056
|
+
const ext = input.endsWith('.json') ? '.opt.json' : '.opt.json';
|
|
3057
|
+
output = input.replace(/\.[^/.]+$/, '') + ext;
|
|
3058
|
+
}
|
|
3059
|
+
const logger = createLogger(options.verbose);
|
|
3060
|
+
const optimizer = new LottieOptimizer(logger);
|
|
3061
|
+
console.log('Loading Lottie file...');
|
|
3062
|
+
let lottieData;
|
|
3063
|
+
let originalFileSize;
|
|
3064
|
+
try {
|
|
3065
|
+
const fileContent = readFileSync(external_path_resolve(input), 'utf-8');
|
|
3066
|
+
originalFileSize = Buffer.byteLength(fileContent, 'utf8');
|
|
3067
|
+
lottieData = JSON.parse(fileContent);
|
|
3068
|
+
console.log(`Loaded ${formatSize(originalFileSize)}`);
|
|
3069
|
+
} catch (error) {
|
|
3070
|
+
console.error(`Failed to load file: ${error}`);
|
|
3071
|
+
process.exit(1);
|
|
3072
|
+
}
|
|
3073
|
+
const optimizeOptions = await buildOptimizeOptions(options, input);
|
|
3074
|
+
console.log('Starting optimization...');
|
|
3075
|
+
const result = await optimizer.optimize(lottieData, optimizeOptions, originalFileSize);
|
|
3076
|
+
console.log('Optimization complete!');
|
|
3077
|
+
let finalSize;
|
|
3078
|
+
if (options.dryRun) finalSize = Buffer.byteLength(JSON.stringify(result.data), 'utf8');
|
|
3079
|
+
else {
|
|
3080
|
+
const outputData = JSON.stringify(result.data);
|
|
3081
|
+
finalSize = Buffer.byteLength(outputData, 'utf8');
|
|
3082
|
+
writeFileSync(external_path_resolve(output), outputData);
|
|
3083
|
+
console.log(chalk.green(`\u{2713} Saved to ${output}`));
|
|
3084
|
+
}
|
|
3085
|
+
displayResults(result, options, originalFileSize, finalSize);
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
async function buildOptimizeOptions(options, inputPath) {
|
|
3092
|
+
let optimizeOptions = {};
|
|
3093
|
+
if (options.config) {
|
|
3094
|
+
if (!existsSync(options.config)) throw new Error(`Config file not found: ${options.config}`);
|
|
3095
|
+
const configContent = readFileSync(options.config, 'utf-8');
|
|
3096
|
+
optimizeOptions = JSON.parse(configContent);
|
|
3097
|
+
}
|
|
3098
|
+
if (options.plugins) optimizeOptions.plugins = options.plugins.split(',').map((p)=>p.trim());
|
|
3099
|
+
if (options.disable) optimizeOptions.disable = options.disable.split(',').map((p)=>p.trim());
|
|
3100
|
+
if (options.pluginConfig) {
|
|
3101
|
+
optimizeOptions.pluginOptions = optimizeOptions.pluginOptions || {};
|
|
3102
|
+
const configs = options.pluginConfig.split(',');
|
|
3103
|
+
for (const config of configs){
|
|
3104
|
+
const [pluginOption, value] = config.split('=');
|
|
3105
|
+
if (pluginOption && void 0 !== value) parseNestedOption(optimizeOptions.pluginOptions, pluginOption.trim(), value.trim());
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
if (options.showConfig || options.showConfigSources) optimizeOptions.debug = {
|
|
3109
|
+
showEffectiveConfig: options.showConfig || false,
|
|
3110
|
+
showConfigSources: options.showConfigSources || false
|
|
3111
|
+
};
|
|
3112
|
+
return optimizeOptions;
|
|
3113
|
+
}
|
|
3114
|
+
function displayResults(result, options, actualOriginalSize, actualFinalSize) {
|
|
3115
|
+
const { stats, report } = result;
|
|
3116
|
+
const originalSize = actualOriginalSize || stats.originalSize;
|
|
3117
|
+
const finalSize = actualFinalSize || stats.optimizedSize;
|
|
3118
|
+
const actualReduction = (originalSize - finalSize) / originalSize * 100;
|
|
3119
|
+
console.log('\n' + chalk.bold('Optimization Results:'));
|
|
3120
|
+
console.log(` Original size: ${chalk.cyan(formatSize(originalSize))}`);
|
|
3121
|
+
console.log(` Optimized size: ${chalk.green(formatSize(finalSize))}`);
|
|
3122
|
+
console.log(` Reduction: ${chalk.yellow(actualReduction.toFixed(1) + '%')}`);
|
|
3123
|
+
console.log(` Processing time: ${chalk.gray(stats.processingTime + 'ms')}`);
|
|
3124
|
+
console.log(` Plugins applied: ${chalk.blue(stats.pluginsApplied.length)}`);
|
|
3125
|
+
if (options.report) {
|
|
3126
|
+
console.log('\n' + chalk.bold('Plugin Details:'));
|
|
3127
|
+
for (const detail of report.details){
|
|
3128
|
+
const reduction = detail.sizeBefore > 0 ? ((detail.sizeBefore - detail.sizeAfter) / detail.sizeBefore * 100).toFixed(1) : '0.0';
|
|
3129
|
+
console.log(` ${detail.pluginName}: ${formatSize(detail.sizeBefore)} \u{2192} ${formatSize(detail.sizeAfter)} (${reduction}%)`);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (report.warnings.length > 0) {
|
|
3133
|
+
console.log('\n' + chalk.yellow('Warnings:'));
|
|
3134
|
+
report.warnings.forEach((warning)=>{
|
|
3135
|
+
console.log(` \u{26A0} ${warning}`);
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
if (report.errors.length > 0) {
|
|
3139
|
+
console.log('\n' + chalk.red('Errors:'));
|
|
3140
|
+
report.errors.forEach((error)=>{
|
|
3141
|
+
console.log(` \u{2717} ${error}`);
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
async function listPlugins() {
|
|
3146
|
+
const optimizer = new LottieOptimizer();
|
|
3147
|
+
const plugins = optimizer.getAvailablePlugins();
|
|
3148
|
+
console.log(chalk.bold('\nAvailable Plugins:'));
|
|
3149
|
+
for (const name of plugins){
|
|
3150
|
+
const plugin = optimizer.getPlugin(name);
|
|
3151
|
+
if (plugin) console.log(` ${chalk.green(name)} - ${plugin.description}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
process.on('uncaughtException', (error)=>{
|
|
3155
|
+
console.error(chalk.red(`Uncaught error: ${error.message}`));
|
|
3156
|
+
process.exit(1);
|
|
3157
|
+
});
|
|
3158
|
+
process.on('unhandledRejection', (reason)=>{
|
|
3159
|
+
console.error(chalk.red(`Unhandled rejection: ${reason}`));
|
|
3160
|
+
process.exit(1);
|
|
3161
|
+
});
|
|
3162
|
+
program.parse();
|