diff --git a/src/ImageSharp/Formats/AnimationUtilities.cs b/src/ImageSharp/Formats/AnimationUtilities.cs index 4e322d2a2..23fc40cdf 100644 --- a/src/ImageSharp/Formats/AnimationUtilities.cs +++ b/src/ImageSharp/Formats/AnimationUtilities.cs @@ -25,26 +25,31 @@ internal static class AnimationUtilities /// The configuration. /// The previous frame if present. /// The current frame. + /// The next frame if present. /// The resultant output. /// The value to use when replacing duplicate pixels. + /// Whether the resultant frame represents an animation blend. /// The clamping bound to apply when calculating difference bounds. /// The representing the operation result. public static (bool Difference, Rectangle Bounds) DeDuplicatePixels( Configuration configuration, ImageFrame? previousFrame, ImageFrame currentFrame, + ImageFrame? nextFrame, ImageFrame resultFrame, - Vector4 replacement, + Color replacement, + bool blend, ClampingMode clampingMode = ClampingMode.None) where TPixel : unmanaged, IPixel { - // TODO: This would be faster (but more complicated to find diff bounds) if we operated on Rgba32. - // If someone wants to do that, they have my unlimited thanks. MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - IMemoryOwner buffers = memoryAllocator.Allocate(currentFrame.Width * 3, AllocationOptions.Clean); - Span previous = buffers.GetSpan()[..currentFrame.Width]; - Span current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width); - Span result = buffers.GetSpan()[(currentFrame.Width * 2)..]; + IMemoryOwner buffers = memoryAllocator.Allocate(currentFrame.Width * 4, AllocationOptions.Clean); + Span previous = buffers.GetSpan()[..currentFrame.Width]; + Span current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width); + Span next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width); + Span result = buffers.GetSpan()[(currentFrame.Width * 3)..]; + + Rgba32 bg = replacement; int top = int.MinValue; int bottom = int.MaxValue; @@ -56,70 +61,90 @@ internal static class AnimationUtilities { if (previousFrame != null) { - PixelOperations.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous); } - PixelOperations.Instance.ToVector4(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current, PixelConversionModifiers.Scale); - - ref Vector256 previousBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(previous)); - ref Vector256 currentBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(current)); - ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result)); + PixelOperations.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current); - Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W); + if (nextFrame != null) + { + PixelOperations.Instance.ToRgba32(configuration, nextFrame.DangerousGetPixelRowMemory(y).Span, next); + } - int size = Unsafe.SizeOf(); + ref Vector256 previousBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(previous)); + ref Vector256 currentBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(current)); + ref Vector256 nextBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(next)); + ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result)); - bool hasRowDiff = false; int i = 0; uint x = 0; + bool hasRowDiff = false; int length = current.Length; int remaining = current.Length; - while (Avx2.IsSupported && remaining >= 2) + if (Avx2.IsSupported && remaining >= 8) { - Vector256 p = Unsafe.Add(ref previousBase, x); - Vector256 c = Unsafe.Add(ref currentBase, x); - - // Compare the previous and current pixels - Vector256 mask = Avx2.CompareEqual(p.AsInt32(), c.AsInt32()); - mask = Avx2.CompareEqual(mask.AsInt64(), Vector256.AllBitsSet).AsInt32(); - mask = Avx2.And(mask, Avx2.Shuffle(mask, 0b_01_00_11_10)).AsInt32(); - - Vector256 neq = Avx2.Xor(mask.AsInt64(), Vector256.AllBitsSet).AsInt32(); - int m = Avx2.MoveMask(neq.AsByte()); - if (m != 0) + Vector256 r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256.Zero; + Vector256 vmb256 = Vector256.Zero; + if (blend) { - // If is diff is found, the left side is marked by the min of previously found left side and the start position. - // The right is the max of the previously found right side and the end position. - int start = i + (BitOperations.TrailingZeroCount(m) / size); - int end = i + (2 - (BitOperations.LeadingZeroCount((uint)m) / size)); - left = Math.Min(left, start); - right = Math.Max(right, end); - hasRowDiff = true; - hasDiff = true; + vmb256 = Avx2.CompareEqual(vmb256, vmb256); } - // Replace the pixel value with the replacement if the full pixel is matched. - Vector256 r = Avx.BlendVariable(c, replacement256, mask.AsSingle()); - Unsafe.Add(ref resultBase, x) = r; - - x++; - i += 2; - remaining -= 2; + while (remaining >= 8) + { + Vector256 p = Unsafe.Add(ref previousBase, x).AsUInt32(); + Vector256 c = Unsafe.Add(ref currentBase, x).AsUInt32(); + + Vector256 eq = Avx2.CompareEqual(p, c); + Vector256 r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256)); + + if (nextFrame != null) + { + Vector256 n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase, x).AsUInt32(), 24).AsInt32(); + eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq); + } + + Unsafe.Add(ref resultBase, x) = r.AsByte(); + + uint msk = (uint)Avx2.MoveMask(eq.AsByte()); + msk = ~msk; + + if (msk != 0) + { + // If is diff is found, the left side is marked by the min of previously found left side and the start position. + // The right is the max of the previously found right side and the end position. + int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint)); + int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint))); + left = Math.Min(left, start); + right = Math.Max(right, end); + hasRowDiff = true; + hasDiff = true; + } + + x++; + i += 8; + remaining -= 8; + } } for (i = remaining; i > 0; i--) { x = (uint)(length - i); - Vector4 p = Unsafe.Add(ref Unsafe.As, Vector4>(ref previousBase), x); - Vector4 c = Unsafe.Add(ref Unsafe.As, Vector4>(ref currentBase), x); - ref Vector4 r = ref Unsafe.Add(ref Unsafe.As, Vector4>(ref resultBase), x); + Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x); + Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x); + Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x); + ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x); - if (p != c) - { - r = c; + bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba); + Rgba32 val = (blend & peq) ? replacement : c; + + peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24); + r = val; + if (!peq) + { // If is diff is found, the left side is marked by the min of previously found left side and the diff position. // The right is the max of the previously found right side and the diff position + 1. left = Math.Min(left, (int)x); @@ -127,10 +152,6 @@ internal static class AnimationUtilities hasRowDiff = true; hasDiff = true; } - else - { - r = replacement; - } } if (hasRowDiff) @@ -143,7 +164,7 @@ internal static class AnimationUtilities bottom = y + 1; } - PixelOperations.Instance.FromVector4Destructive(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span); } Rectangle bounds = Rectangle.FromLTRB( @@ -163,19 +184,6 @@ internal static class AnimationUtilities return (hasDiff, bounds); } - - public static void CopySource(ImageFrame source, ImageFrame destination, Rectangle bounds) - where TPixel : unmanaged, IPixel - { - Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(bounds); - Buffer2DRegion destBuffer = destination.PixelBuffer.GetRegion(bounds); - for (int y = 0; y < destBuffer.Height; y++) - { - Span sourceRow = sourceBuffer.DangerousGetRowSpan(y); - Span destRow = destBuffer.DangerousGetRowSpan(y); - sourceRow.CopyTo(destRow); - } - } } #pragma warning disable SA1201 // Elements should appear in the correct order diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 37e31f32d..2186cc2e4 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -264,10 +264,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals hasPaletteQuantizer = true; } + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; + this.EncodeAdditionalFrame( stream, previousFrame, currentFrame, + nextFrame, encodingFrame, useLocal, gifMetadata, @@ -311,6 +314,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals Stream stream, ImageFrame previousFrame, ImageFrame currentFrame, + ImageFrame? nextFrame, ImageFrame encodingFrame, bool useLocal, GifFrameMetadata metadata, @@ -325,7 +329,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals ImageFrame? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame; // Deduplicate and quantize the frame capturing only required parts. - (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(this.configuration, previous, currentFrame, encodingFrame, Vector4.Zero); + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + this.configuration, + previous, + currentFrame, + nextFrame, + encodingFrame, + Color.Transparent, + true); using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( encodingFrame, diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 7908109e8..932916dec 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Buffers.Binary; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; @@ -208,21 +207,22 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable for (int i = 1; i < image.Frames.Count; i++) { - currentFrame = image.Frames[i]; - frameMetadata = GetPngFrameMetadata(currentFrame); - ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; - (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero); + currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - if (difference && previousDisposal != PngDisposalMethod.RestoreToBackground) - { - if (frameMetadata.BlendMethod == PngBlendMethod.Source) - { - // We've potentially introduced transparency within our area of interest - // so we need to overwrite the changed area with the full data. - AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds); - } - } + frameMetadata = GetPngFrameMetadata(currentFrame); + bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + Color.Transparent, + blend); if (clearTransparency) { diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index db12d7c67..e37c1d179 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Memory; @@ -161,22 +160,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals for (int i = 1; i < image.Frames.Count; i++) { - ImageFrame currentFrame = image.Frames[i]; - frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); - ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; + ImageFrame currentFrame = image.Frames[i]; + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even); - - if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground) - { - if (frameMetadata.BlendMethod == WebpBlendMethod.Source) - { - // We've potentially introduced transparency within our area of interest - // so we need to overwrite the changed area with the full data. - AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds); - } - } + frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); + bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + Color.Transparent, + blend, + ClampingMode.Even); using Vp8LEncoder animatedEncoder = new( this.memoryAllocator, @@ -232,21 +232,23 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals for (int i = 1; i < image.Frames.Count; i++) { + ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; ImageFrame currentFrame = image.Frames[i]; - frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); + ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; - ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame; - (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(image.Configuration, prev, currentFrame, encodingFrame, Vector4.Zero, ClampingMode.Even); - - if (difference && previousDisposal != WebpDisposalMethod.RestoreToBackground) - { - if (frameMetadata.BlendMethod == WebpBlendMethod.Source) - { - // We've potentially introduced transparency within our area of interest - // so we need to overwrite the changed area with the full data. - AnimationUtilities.CopySource(currentFrame, encodingFrame, bounds); - } - } + frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame); + bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + Color.Transparent, + blend, + ClampingMode.Even); using Vp8Encoder animatedEncoder = new( this.memoryAllocator, diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5430494a8..851c79217 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -480,6 +480,7 @@ public static class TestImages public const string LargeComment = "Gif/large_comment.gif"; public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif"; public const string MixedDisposal = "Gif/mixed-disposal.gif"; + public const string M4nb = "Gif/m4nb.gif"; // Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite public const string ZeroSize = "Gif/image-zero-size.gif"; @@ -511,6 +512,7 @@ public static class TestImages public static readonly string[] Animated = { + M4nb, Giphy, Cheers, Kumin, diff --git a/tests/Images/Input/Gif/m4nb.gif b/tests/Images/Input/Gif/m4nb.gif new file mode 100644 index 000000000..0c921b2af --- /dev/null +++ b/tests/Images/Input/Gif/m4nb.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b495caf1eb1f1cf7b15a1998faa33a6f4a49999e5edd435d4ff91265ff1ce5 +size 2100