sanitizeCrashingLayers function

SanitizeResult sanitizeCrashingLayers(
  1. Map<String, dynamic> doc
)

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,
  );
}