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