Lottie-Windows/source/UIData/Tools/Canonicalizer.cs

860 строки
36 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.WinUI.Lottie.WinCompData;
using CommunityToolkit.WinUI.Lottie.WinCompData.Expressions;
using CommunityToolkit.WinUI.Lottie.WinCompData.Mgcg;
using CommunityToolkit.WinUI.Lottie.WinUIXamlMediaData;
using Expr = CommunityToolkit.WinUI.Lottie.WinCompData.Expressions;
using Sn = System.Numerics;
using Wg = CommunityToolkit.WinUI.Lottie.WinCompData.Wg;
using Wui = CommunityToolkit.WinUI.Lottie.WinCompData.Wui;
namespace CommunityToolkit.WinUI.Lottie.UIData.Tools
{
/// <summary>
/// Discovers objects that are shareable and updates a graph so that references to
/// equivalent shareable objects refer to the same object.
/// </summary>
static class Canonicalizer
{
// Generates a sequence of ints from 0..int.MaxValue. Used to attach indexes
// to sequences using Zip.
static readonly IEnumerable<int> PositiveInts = Enumerable.Range(0, int.MaxValue);
internal static void Canonicalize<T>(ObjectGraph<T> graph, bool ignoreCommentProperties)
where T : CanonicalizedNode<T>, new()
{
CanonicalizerWorker<T>.Canonicalize(graph, ignoreCommentProperties);
}
sealed class CanonicalizerWorker<TNode>
where TNode : CanonicalizedNode<TNode>, new()
{
readonly ObjectGraph<TNode> _graph;
readonly bool _ignoreCommentProperties;
CanonicalizerWorker(ObjectGraph<TNode> graph, bool ignoreCommentProperties)
{
_graph = graph;
_ignoreCommentProperties = ignoreCommentProperties;
}
internal static void Canonicalize(ObjectGraph<TNode> graph, bool ignoreCommentProperties)
{
var canonicalizer = new CanonicalizerWorker<TNode>(graph, ignoreCommentProperties);
canonicalizer.Canonicalize();
}
// Find the nodes that are equivalent and point them all to a single canonical representation.
void Canonicalize()
{
CanonicalizeDropShadows();
CanonicalizeInsetClips();
CanonicalizeEllipseGeometries();
CanonicalizeRectangleGeometries();
CanonicalizeRoundedRectangleGeometries();
CanonicalizeCanvasGeometryPaths();
// CompositionPath must be canonicalized after CanvasGeometry paths.
CanonicalizeCompositionPaths();
// CompositionPathGeometry must be canonicalized after CompositionPath.
CanonicalizeCompositionPathGeometries();
// Easing functions must be canonicalized before keyframes are canonicalized.
CanonicalizeLinearEasingFunctions();
CanonicalizeCubicBezierEasingFunctions();
CanonicalizeStepEasingFunctions();
CanonicalizeExpressionAnimations();
CanonicalizeKeyFrameAnimations<CompositionPath, Expr.Void>(CompositionObjectType.PathKeyFrameAnimation, CompositionPathEqualityComparer);
CanonicalizeKeyFrameAnimations<bool, Expr.Boolean>(CompositionObjectType.BooleanKeyFrameAnimation);
CanonicalizeKeyFrameAnimations<Wui.Color, Expr.Color>(CompositionObjectType.ColorKeyFrameAnimation);
CanonicalizeKeyFrameAnimations<float, Expr.Scalar>(CompositionObjectType.ScalarKeyFrameAnimation);
CanonicalizeKeyFrameAnimations<Sn.Vector2, Expr.Vector2>(CompositionObjectType.Vector2KeyFrameAnimation);
CanonicalizeKeyFrameAnimations<Sn.Vector3, Expr.Vector3>(CompositionObjectType.Vector3KeyFrameAnimation);
CanonicalizeKeyFrameAnimations<Sn.Vector4, Expr.Vector4>(CompositionObjectType.Vector4KeyFrameAnimation);
// Now that the path animations are canonicalized, canonicalize the CompositionPathGeometries
// that have animated paths.
CanonicalizeAnimatedCompositionPathGeometries();
// ColorKeyFrameAnimations and ExpressionAnimations must be canonicalized before color brushes are canonicalized.
CanonicalizeColorBrushes();
CanonicalizeThemeBrushes();
CanonicalizeLoadedImageSurface(LoadedImageSurface.LoadedImageSurfaceType.FromStream);
CanonicalizeLoadedImageSurface(LoadedImageSurface.LoadedImageSurfaceType.FromUri);
// LoadedImageSurfaces must be canonicalized before surface brushes are canonicalized.
CanonicalizeCompositionSurfaceBrushes();
// Expression animations and anything that can be referenced by an expression
// animation on a gradient stop must be canonicalized before gradient stops.
CanonicalizeColorGradientStops();
}
TNode NodeFor(Wg.IGeometrySource2D obj) => _graph[obj].Canonical;
TNode NodeFor(CompositionObject obj) => _graph[obj].Canonical;
TNode NodeFor(CompositionPath obj) => _graph[obj].Canonical;
TNode NodeFor(LoadedImageSurface obj) => _graph[obj].Canonical;
TNode NodeFor(ICompositionSurface obj)
{
return obj switch
{
CompositionObject compositionObject => _graph[compositionObject].Canonical,
LoadedImageSurface loadedImageSurface => _graph[loadedImageSurface].Canonical,
_ => throw new InvalidOperationException(),
};
}
TC CanonicalObject<TC>(Wg.IGeometrySource2D obj) => (TC)NodeFor(obj).Object;
TC CanonicalObject<TC>(CompositionObject obj) => (TC)NodeFor(obj).Object;
TC CanonicalObject<TC>(CompositionPath obj) => (TC)NodeFor(obj).Object;
TC CanonicalObject<TC>(LoadedImageSurface obj) => (TC)NodeFor(obj).Object;
TC CanonicalObject<TC>(ICompositionSurface obj) => (TC)NodeFor(obj).Object;
IEnumerable<(TNode Node, TC Object)> GetCompositionObjects<TC>(CompositionObjectType type)
where TC : CompositionObject
{
return _graph.CompositionObjectNodes.Where(n => n.Object.Type == type).Select(n => (n.Node, (TC)n.Object));
}
IEnumerable<(TNode Node, TC Object)> GetCanonicalizableCompositionObjects<TC>(CompositionObjectType type)
where TC : CompositionObject
{
var items = GetCompositionObjects<TC>(type);
return
from item in items
let obj = item.Object
where (_ignoreCommentProperties || obj.Comment is null)
&& obj.Properties.Names.Count == 0
&& obj.Animators.Count == 0
select (item.Node, obj);
}
IEnumerable<(TNode Node, TC Object)> GetCanonicalizableCanvasGeometries<TC>(CanvasGeometry.GeometryType type)
where TC : CanvasGeometry
{
return
from item in _graph.CanvasGeometryNodes
let obj = item.Object
where obj.Type == type
select (item.Node, (TC)obj);
}
IEnumerable<(TNode Node, TC Object)> GetCanonicalizableLoadedImageSurfaces<TC>(LoadedImageSurface.LoadedImageSurfaceType type)
where TC : LoadedImageSurface
{
return
from item in _graph.LoadedImageSurfaceNodes
let obj = item.Object
where obj.Type == type
select (item.Node, (TC)obj);
}
void CanonicalizeExpressionAnimations()
{
var items = GetCanonicalizableCompositionObjects<ExpressionAnimation>(CompositionObjectType.ExpressionAnimation);
// TODO - handle more than one reference parameter.
var grouping =
from item in items
where item.Object.ReferenceParameters.Count() == 1
group item.Node by GetExpressionAnimationKey1(item.Object)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
(string expression, string?, string, CompositionObject) GetExpressionAnimationKey1(ExpressionAnimation animation)
{
var rp0 = animation.ReferenceParameters.First();
return (animation.Expression.ToText(), animation.Target, rp0.Key, CanonicalObject<CompositionObject>(rp0.Value));
}
void CanonicalizeKeyFrameAnimations<TKFA, TExpression>(CompositionObjectType animationType)
where TExpression : Expression_<TExpression>
=> CanonicalizeKeyFrameAnimations<TKFA, TExpression>(animationType, SimpleEqualityComparer<TKFA>);
void CanonicalizeKeyFrameAnimations<TKFA, TExpression>(
CompositionObjectType animationType,
Func<TKFA, TKFA, bool> equalityComparer)
where TExpression : Expression_<TExpression>
{
var items = GetCanonicalizableCompositionObjects<KeyFrameAnimation<TKFA, TExpression>>(animationType);
var grouping =
from item in items
group item.Node by new KeyFrameAnimationKey<TKFA, TExpression>(this, item.Object, equalityComparer)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
// Returns true if the a and b are the same CompositionObject after canonicalization.
bool CompositionPathEqualityComparer(CompositionPath a, CompositionPath b)
=> ReferenceEquals(NodeFor(a), NodeFor(b));
bool SimpleEqualityComparer<T>(T a, T b)
=> a!.Equals(b);
sealed class KeyFrameAnimationKey<TKFA, TExpression>
where TExpression : Expression_<TExpression>
{
readonly CanonicalizerWorker<TNode> _owner;
readonly KeyFrameAnimation<TKFA, TExpression> _obj;
readonly Func<TKFA, TKFA, bool> _equalityComparer;
internal KeyFrameAnimationKey(
CanonicalizerWorker<TNode> owner,
KeyFrameAnimation<TKFA, TExpression> obj,
Func<TKFA, TKFA, bool> equalityComparer)
{
_owner = owner;
_obj = obj;
_equalityComparer = equalityComparer;
}
public override int GetHashCode()
{
// Not the perfect hash, but not terrible
return _obj.KeyFrameCount ^ (int)_obj.Duration.Ticks;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
var other = obj as KeyFrameAnimationKey<TKFA, TExpression>;
if (other is null)
{
return false;
}
var thisObj = _obj;
var otherObj = other._obj;
if (thisObj.Duration != otherObj.Duration)
{
return false;
}
if (thisObj.KeyFrameCount != otherObj.KeyFrameCount)
{
return false;
}
if (thisObj.Target != otherObj.Target)
{
return false;
}
var thisKfs = thisObj.KeyFrames.ToArray();
var otherKfs = otherObj.KeyFrames.ToArray();
for (var i = 0; i < thisKfs.Length; i++)
{
var thisKf = thisKfs[i];
var otherKf = otherKfs[i];
if (thisKf.Progress != otherKf.Progress)
{
return false;
}
if (thisKf.Type != otherKf.Type)
{
return false;
}
if (thisKf.Easing is not null)
{
if (otherKf.Easing is null || _owner.NodeFor(thisKf.Easing) != _owner.NodeFor(otherKf.Easing))
{
return false;
}
}
switch (thisKf.Type)
{
case KeyFrameType.Expression:
var thisExpressionKeyFrame = (KeyFrameAnimation<TKFA, TExpression>.ExpressionKeyFrame)thisKf;
var otherExpressionKeyFrame = (KeyFrameAnimation<TKFA, TExpression>.ExpressionKeyFrame)otherKf;
if (thisExpressionKeyFrame.Expression != otherExpressionKeyFrame.Expression)
{
return false;
}
break;
case KeyFrameType.Value:
var thisValueKeyFrame = (KeyFrameAnimation<TKFA, TExpression>.ValueKeyFrame)thisKf;
var otherValueKeyFrame = (KeyFrameAnimation<TKFA, TExpression>.ValueKeyFrame)otherKf;
if (!_equalityComparer(thisValueKeyFrame.Value, otherValueKeyFrame.Value))
{
return false;
}
break;
default:
break;
}
}
return true;
}
}
void CanonicalizeColorBrushes()
{
// Canonicalize color brushes that have no animations, or have just a Color animation.
var nodes = GetCompositionObjects<CompositionColorBrush>(CompositionObjectType.CompositionColorBrush);
var items =
from item in nodes
let obj = item.Object
where (_ignoreCommentProperties || obj.Comment is null)
&& obj.Properties.Names.Count == 0
select (item.Node, obj);
var grouping =
from item in items
let obj = item.obj
let animators = obj.Animators.ToArray()
where animators.Length == 0 || (animators.Length == 1 && animators[0].AnimatedProperty == "Color")
let animator = animators.FirstOrDefault()
let canonicalAnimator = animator is null ? null : CanonicalObject<CompositionAnimation>(animator.Animation)
group item.Node by (obj.Color, canonicalAnimator) into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeThemeBrushes()
{
// Canonicalize color brushes that have a single property set value. These
// are likely to be themed color brushes.
var nodes = GetCompositionObjects<CompositionColorBrush>(CompositionObjectType.CompositionColorBrush);
var items =
from item in nodes
let obj = item.Object
where (_ignoreCommentProperties || obj.Comment is null)
&& obj.Color is null
&& obj.Properties.Names.Count == 1
select (item.Node, obj);
var grouping =
from item in items
let obj = item.obj
let animators = obj.Animators.ToArray()
where animators.Length == 1
let animator = animators[0]
where animator.AnimatedProperty == "Color"
&& animator.Animation.Type == CompositionObjectType.ExpressionAnimation
let key = new ThemeBrushKey(this, obj)
group item.Node by key into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
sealed class ThemeBrushKey : IEquatable<ThemeBrushKey>
{
readonly CanonicalizerWorker<TNode> _owner;
readonly CompositionColorBrush _brush;
readonly ExpressionAnimation _animation;
internal ThemeBrushKey(CanonicalizerWorker<TNode> owner, CompositionColorBrush brush)
{
_owner = owner;
var animators = brush.Animators.ToArray();
if (animators.Length != 1)
{
throw new InvalidOperationException();
}
var animator = animators[0];
if (animator.AnimatedProperty != "Color")
{
throw new InvalidOperationException();
}
if (animator.Animation.Type != CompositionObjectType.ExpressionAnimation)
{
throw new InvalidOperationException();
}
_brush = brush;
_animation = (ExpressionAnimation)animator.Animation;
}
public bool Equals(ThemeBrushKey? other)
{
if (other is null)
{
return false;
}
var otherAnimation = other._animation;
var thisText = _animation.Expression.ToText();
var otherText = otherAnimation.Expression.ToText();
if (thisText != otherText)
{
return false;
}
// The animations have the same text. Are their reference parameters the same?
var thisRefs = _animation.ReferenceParameters.ToArray();
var otherRefs = otherAnimation.ReferenceParameters.ToArray();
if (thisRefs.Length != otherRefs.Length)
{
return false;
}
// Compare the reference parameters. They are always returned in alphabetical order.
for (var i = 0; i < thisRefs.Length; i++)
{
var thisRef = thisRefs[i];
var otherRef = otherRefs[i];
if (thisRef.Key != otherRef.Key)
{
// The reference have different names.
return false;
}
var thisRefValue = thisRef.Value;
var otherRefValue = otherRef.Value;
if (thisRefValue != otherRefValue)
{
// The values of the references are different, but they might be self
// references (i.e. references back to the property set of the brush).
// Check that.
if (thisRefValue != _brush || otherRefValue != other._brush)
{
// They're not direct self references. They may be references to
// the property set owned by the brush.
if (thisRefValue is CompositionPropertySet thisPropertySet &&
otherRefValue is CompositionPropertySet otherPropertySet)
{
// They're references to a property set. Is it the property set on the brush?
if (thisPropertySet.Owner != _brush ||
otherPropertySet.Owner != other._brush)
{
return false;
}
// They're both references to their own property set. Make sure each property set
// has the same properties and the same animations.
var thispAnimators = thisPropertySet.Animators;
var otherpAnimators = otherPropertySet.Animators;
if (thispAnimators.Count != otherpAnimators.Count)
{
return false;
}
if (thispAnimators.Count != 1)
{
// For now we only handle a single animator.
return false;
}
var thisAnimator = thispAnimators[0];
var otherAnimator = otherpAnimators[0];
if (thisAnimator.AnimatedProperty != otherAnimator.AnimatedProperty)
{
return false;
}
if (_owner.NodeFor(thisAnimator.Animation).Canonical != _owner.NodeFor(otherAnimator.Animation))
{
return false;
}
}
else
{
// They're not references to a property set.
return false;
}
}
}
}
return true;
}
public override int GetHashCode()
=> _animation.Expression.ToText().GetHashCode();
public override bool Equals(object? obj) => Equals(obj as ThemeBrushKey);
}
void CanonicalizeColorGradientStops()
{
// CompositionColorGradientStopCollections do not support having the same
// CompositionColorGradientStop appearing more than once in the same collection, so
// we have to special-case canonicalization so that they are shared between
// collections, but not within a collection.
// Get the gradient brushes.
var gradientBrushes = GetCompositionObjects<CompositionGradientBrush>(CompositionObjectType.CompositionLinearGradientBrush).Concat(
GetCompositionObjects<CompositionGradientBrush>(CompositionObjectType.CompositionRadialGradientBrush));
// For each CompositionGradientBrush, get the non-animated stops, and give each an index
// indicating whether it is the 1st, 2nd, 3rd, etc stop with that color and offset
// in the collection.
var nonAnimatedStopsWithIndex =
from b in gradientBrushes
let nonAnimatedStops = from s in b.Object.ColorStops
where (_ignoreCommentProperties || s.Comment is null)
&& s.Properties.Names.Count == 0 && !s.Animators.Any()
group s by (s.Color, s.Offset) into g
from s2 in g.Zip(PositiveInts, (x, y) => (Stop: x, Index: y))
select s2
from s in nonAnimatedStops
select s;
// Group by stops that have the same color and index.
var grouping =
from item in nonAnimatedStopsWithIndex
let obj = item.Stop
group NodeFor(obj) by (obj.Color, obj.Offset, item.Index)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeCompositionSurfaceBrushes()
{
// Canonicalize surface brushes that have no animations.
var nodes = GetCompositionObjects<CompositionSurfaceBrush>(CompositionObjectType.CompositionSurfaceBrush);
var items =
from item in nodes
let obj = item.Object
where (_ignoreCommentProperties || obj.Comment is null)
&& obj.Properties.Names.Count == 0
select (item.Node, obj);
var grouping =
from item in items
let obj = item.obj
where obj.Animators.Count == 0
group item.Node by CanonicalObject<ICompositionSurface>(obj.Surface) into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeEllipseGeometries()
{
var items = GetCanonicalizableCompositionObjects<CompositionEllipseGeometry>(CompositionObjectType.CompositionEllipseGeometry);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.Center,
obj.Radius,
obj.TrimStart,
obj.TrimEnd,
obj.TrimOffset)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeRectangleGeometries()
{
var items = GetCanonicalizableCompositionObjects<CompositionRectangleGeometry>(CompositionObjectType.CompositionRectangleGeometry);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.Offset,
obj.Size,
obj.TrimStart,
obj.TrimEnd,
obj.TrimOffset)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeRoundedRectangleGeometries()
{
var items = GetCanonicalizableCompositionObjects<CompositionRoundedRectangleGeometry>(CompositionObjectType.CompositionRoundedRectangleGeometry);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.Offset,
obj.Size,
obj.CornerRadius,
obj.TrimStart,
obj.TrimEnd,
obj.TrimOffset)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeCompositionPathGeometries()
{
var items = GetCanonicalizableCompositionObjects<CompositionPathGeometry>(CompositionObjectType.CompositionPathGeometry);
var grouping =
from item in items
let obj = item.Object
let path = obj.Path is null ? null : CanonicalObject<CompositionPath>(obj.Path)
group item.Node by (
path,
obj.TrimStart,
obj.TrimEnd,
obj.TrimOffset)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeAnimatedCompositionPathGeometries()
{
var items =
from item in GetCompositionObjects<CompositionPathGeometry>(CompositionObjectType.CompositionPathGeometry)
let obj = item.Object
where (_ignoreCommentProperties || obj.Comment is null)
&& obj.Properties.Names.Count == 0
&& obj.Animators.Count == 1
let animator = obj.Animators[0]
where animator.AnimatedProperty == "Path"
select (Node: item.Node, Object: obj);
var grouping =
from item in items
let obj = item.Object
let animation = CanonicalObject<PathKeyFrameAnimation>(obj.Animators[0].Animation)
group item.Node by (
animation,
obj.TrimStart,
obj.TrimEnd,
obj.TrimOffset)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeCanvasGeometryPaths()
{
var items = GetCanonicalizableCanvasGeometries<CanvasGeometry.Path>(CanvasGeometry.GeometryType.Path);
var grouping =
from item in items
let obj = item.Object
group item.Node by obj into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeCompositionPaths()
{
var grouping =
from item in _graph.CompositionPathNodes
let obj = item.Object
let canonicalSource = CanonicalObject<CanvasGeometry>(obj.Source)
group item.Node by canonicalSource into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeDropShadows()
{
var items = GetCanonicalizableCompositionObjects<DropShadow>(CompositionObjectType.DropShadow);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.BlurRadius,
obj.Color,
obj.Mask,
obj.Offset,
obj.Opacity,
obj.SourcePolicy)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeInsetClips()
{
var items = GetCanonicalizableCompositionObjects<InsetClip>(CompositionObjectType.InsetClip);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.BottomInset,
obj.LeftInset,
obj.RightInset,
obj.TopInset,
obj.CenterPoint,
obj.Scale)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeCubicBezierEasingFunctions()
{
var items = GetCanonicalizableCompositionObjects<CubicBezierEasingFunction>(CompositionObjectType.CubicBezierEasingFunction);
var grouping =
from item in items
let obj = item.Object
group item.Node by (obj.ControlPoint1, obj.ControlPoint2)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeLinearEasingFunctions()
{
var items = GetCanonicalizableCompositionObjects<LinearEasingFunction>(CompositionObjectType.LinearEasingFunction);
// Every LinearEasingFunction is equivalent.
var grouping =
from item in items
group item.Node by true into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeStepEasingFunctions()
{
var items = GetCanonicalizableCompositionObjects<StepEasingFunction>(CompositionObjectType.StepEasingFunction);
var grouping =
from item in items
let obj = item.Object
group item.Node by (
obj.FinalStep,
obj.InitialStep,
obj.IsFinalStepSingleFrame,
obj.IsInitialStepSingleFrame,
obj.StepCount)
into grouped
select grouped;
CanonicalizeGrouping(grouping);
}
void CanonicalizeLoadedImageSurface(LoadedImageSurface.LoadedImageSurfaceType type)
{
// Canonicalize LoadedImageSurfaces.
var items = GetCanonicalizableLoadedImageSurfaces<LoadedImageSurface>(type);
switch (type)
{
case LoadedImageSurface.LoadedImageSurfaceType.FromStream:
var grouping = items.GroupBy(i => ((LoadedImageSurfaceFromStream)i.Object).Bytes, i => i.Node, ByteArrayComparer.Instance);
CanonicalizeGrouping(grouping);
break;
case LoadedImageSurface.LoadedImageSurfaceType.FromUri:
var groupingExternal =
from item in items
let obj = (LoadedImageSurfaceFromUri)item.Object
group item.Node by obj.Uri into grouped
select grouped;
CanonicalizeGrouping(groupingExternal);
break;
default:
throw new InvalidOperationException();
}
}
static void CanonicalizeGrouping<TKey>(IEnumerable<IGrouping<TKey, TNode>> grouping)
{
foreach (var group in grouping)
{
// Pick a node to be the canonical node. Any node in the group is suitable
// because, by definition, they are all equivalent, but for consistency
// we always pick the node that appears first in the traversal of the tree.
var nodes = group.ToArray();
if (nodes.Length > 1)
{
var canonical = nodes.OrderBy(n => n.Position).FirstOrDefault();
if (canonical is null)
{
throw new InvalidOperationException();
}
// Point every node to the designated canonical node.
foreach (var node in nodes)
{
node.Canonical = canonical;
}
}
}
}
sealed class ByteArrayComparer : IEqualityComparer<byte[]>
{
ByteArrayComparer()
{
}
internal static ByteArrayComparer Instance { get; } = new ByteArrayComparer();
bool IEqualityComparer<byte[]>.Equals(byte[]? x, byte[]? y) =>
x is null ? y is null : (y is null ? true : x.SequenceEqual(y));
int IEqualityComparer<byte[]>.GetHashCode(byte[] obj)
{
// This is not a great hash code but is good enough for what we need.
return obj.Length;
}
}
}
}
}