sanitizeCrashingLayers function
Removes crashing/dead layers, assets, masks, and shape content from
doc in place, and prunes assets left unreferenced by that removal.
Implementation
SanitizeResult sanitizeCrashingLayers(Map<String, dynamic> doc) {
// Mutated in place throughout (rather than rebuilt and reassigned):
// callers may pass a document with a more narrowly-typed nested list than
// plain `List<dynamic>` (any `jsonDecode` output is fine, but a
// hand-built Map literal, e.g. in tests, can infer a stricter generic
// type), and assigning a freshly built `List<...>` back into e.g.
// `doc['assets']` would then fail Dart's runtime covariant-generic check.
final assets = (doc['assets'] as List?) ?? [];
// Asset ids usable enough for `lottie`'s own parser to read without
// crashing: a String, or a num (its JSON reader coerces a numeric id to
// a string with no error) -- anything else means the `late String id`
// local in its asset parser is never assigned.
final validAssetIds = <Object>{
for (final a in assets)
if (a is Map && (a['id'] is String || a['id'] is num)) a['id'] as Object,
};
// Only String ids are usable as a `refId` match, matching this file's
// own `_collectRefIds`/`_reachableAssetIds` strictness elsewhere: a
// numeric asset id can never actually be reached, since `refId` is always
// read as a string.
final validStringAssetIds = validAssetIds.whereType<String>().toSet();
var audioRemoved = 0;
var precompBadRefRemoved = 0;
var textMissingDataRemoved = 0;
void stripCrashingLayers(List<dynamic> layers) {
layers.removeWhere((l) {
if (l is! Map) return false;
if (l['ty'] == audioLayerType) {
audioRemoved++;
return true;
}
if (l['ty'] == _precompLayerType) {
final refId = l['refId'];
if (refId is! String || !validStringAssetIds.contains(refId)) {
precompBadRefRemoved++;
return true;
}
}
if (l['ty'] == _textLayerType) {
final t = l['t'];
if (t is! Map || !t.containsKey('d')) {
textMissingDataRemoved++;
return true;
}
}
return false;
});
}
_forEachLayerList(doc, assets, stripCrashingLayers);
var assetsMissingIdRemoved = 0;
assets.removeWhere((a) {
if (a is Map && a['id'] is! String && a['id'] is! num) {
assetsMissingIdRemoved++;
return true;
}
return false;
});
// Only remove an asset once no layer refId's it anymore — even if it's
// empty: a preComp layer may intentionally point to an empty precomp (an
// AE-exported placeholder). Wrongly removing a still-referenced asset
// leaves a dangling `refId`, causing `composition.getPrecomps(refId)!` to
// crash while building the render tree — later, and quite different from
// the audio layer's null-check error, so it doesn't surface at parse
// time but only when actually rendering.
final reachable = _reachableAssetIds(_layersOf(doc), assets);
var emptyPrecompsRemoved = 0;
var unreferencedRemoved = 0;
assets.removeWhere((a) {
if (a is! Map || a['id'] is! String || reachable.contains(a['id'])) {
return false;
}
if (a['layers'] is List && (a['layers'] as List).isEmpty) {
emptyPrecompsRemoved++;
} else {
unreferencedRemoved++;
}
return true;
});
var maskEntriesRemoved = 0;
_forEachLayerList(doc, assets, (layers) {
for (final l in layers) {
if (l is Map && l['masksProperties'] is List) {
final masks = l['masksProperties'] as List;
final before = masks.length;
masks.removeWhere(
(m) =>
!(m is Map &&
m.containsKey('mode') &&
m.containsKey('pt') &&
m.containsKey('o')),
);
maskEntriesRemoved += before - masks.length;
}
}
});
var malformedShapeContentRemoved = 0;
var invalidStrokeCapsOrJoinsFixed = 0;
void pruneShapeContent(List<dynamic> items) {
items.removeWhere((item) {
if (item is! Map) return false;
// Groups are the only nesting shape in the schema; recurse into
// their own items before deciding anything about the group itself.
if (item['ty'] == 'gr' && item['it'] is List) {
pruneShapeContent(item['it'] as List);
}
if (!_hasRequiredShapeContentFields(item)) {
malformedShapeContentRemoved++;
return true;
}
if (_clearInvalidCapOrJoin(item)) {
invalidStrokeCapsOrJoinsFixed++;
}
return false;
});
}
_forEachLayerList(doc, assets, (layers) {
for (final l in layers) {
if (l is Map && l['shapes'] is List) {
pruneShapeContent(l['shapes'] as List);
}
}
});
final missingTransform = <String>[];
void scanMissingTransform(List<dynamic> layers, String where) {
for (final l in layers) {
if (l is Map && !l.containsKey('ks')) {
missingTransform.add('$where: ty=${l['ty']} nm=${l['nm']}');
}
}
}
scanMissingTransform(_layersOf(doc), 'root');
for (final asset in assets) {
if (asset is Map && asset['layers'] is List) {
scanMissingTransform(asset['layers'] as List, 'asset ${asset['id']}');
}
}
final emptyKeyframes = <String>[];
_forEachLayerList(doc, assets, (layers) {
for (final l in layers) {
if (l is Map) {
_scanEmptyKeyframes(
l,
'',
'ty=${l['ty']} nm=${l['nm']}',
emptyKeyframes,
);
}
}
});
return SanitizeResult(
audioLayersRemoved: audioRemoved,
emptyPrecompsRemoved: emptyPrecompsRemoved,
unreferencedAssetsRemoved: unreferencedRemoved,
layersMissingTransform: missingTransform,
precompLayersWithBadRefRemoved: precompBadRefRemoved,
textLayersMissingDataRemoved: textMissingDataRemoved,
assetsMissingIdRemoved: assetsMissingIdRemoved,
maskEntriesRemoved: maskEntriesRemoved,
malformedShapeContentRemoved: malformedShapeContentRemoved,
invalidStrokeCapsOrJoinsFixed: invalidStrokeCapsOrJoinsFixed,
propertiesWithEmptyKeyframes: emptyKeyframes,
);
}