diff --git a/src/ImageSharp/Formats/AnimationUtilities.cs b/src/ImageSharp/Formats/AnimationUtilities.cs
new file mode 100644
index 0000000000..1bca34eae4
--- /dev/null
+++ b/src/ImageSharp/Formats/AnimationUtilities.cs
@@ -0,0 +1,157 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Utility methods for animated formats.
+///
+internal static class AnimationUtilities
+{
+ ///
+ /// Deduplicates pixels between the previous and current frame returning only the changed pixels and bounds.
+ ///
+ /// The type of pixel format.
+ /// The configuration.
+ /// The previous frame if present.
+ /// The current frame.
+ /// The resultant output.
+ /// The value to use when replacing duplicate pixels.
+ /// The representing the operation result.
+ public static (bool Difference, Rectangle Bounds) DeDuplicatePixels(
+ Configuration configuration,
+ ImageFrame? previousFrame,
+ ImageFrame currentFrame,
+ ImageFrame resultFrame,
+ Vector4 replacement)
+ where TPixel : unmanaged, IPixel
+ {
+ 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)..];
+
+ int top = int.MinValue;
+ int bottom = int.MaxValue;
+ int left = int.MaxValue;
+ int right = int.MinValue;
+
+ bool hasDiff = false;
+ for (int y = 0; y < currentFrame.Height; y++)
+ {
+ if (previousFrame != null)
+ {
+ PixelOperations.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale);
+ }
+
+ 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));
+
+ Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
+
+ int size = Unsafe.SizeOf();
+
+ bool hasRowDiff = false;
+ int i = 0;
+ uint x = 0;
+ int length = current.Length;
+ int remaining = current.Length;
+ if (Avx2.IsSupported && remaining >= 2)
+ {
+ while (remaining >= 2)
+ {
+ Vector256 p = Unsafe.Add(ref previousBase, x);
+ Vector256 c = Unsafe.Add(ref currentBase, x);
+
+ // Compare the previous and current pixels
+ Vector256 neq = Avx.CompareEqual(p, c);
+ Vector256 mask = neq.AsInt32();
+
+ neq = Avx.Xor(neq, Vector256.AllBitsSet);
+ int m = Avx2.MoveMask(neq.AsByte());
+ if (m != 0)
+ {
+ // 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.
+ int diff = (int)(i + (uint)(BitOperations.TrailingZeroCount(m) / size));
+ left = Math.Min(left, diff);
+ right = Math.Max(right, diff + 1);
+ hasRowDiff = true;
+ hasDiff = true;
+ }
+
+ // Capture the original alpha values.
+ mask = Avx2.HorizontalAdd(mask, mask);
+ mask = Avx2.HorizontalAdd(mask, mask);
+ mask = Avx2.CompareEqual(mask, Vector256.Create(-4));
+
+ Vector256 r = Avx.BlendVariable(c, replacement256, mask.AsSingle());
+ Unsafe.Add(ref resultBase, x) = r;
+
+ x++;
+ i += 2;
+ remaining -= 2;
+ }
+ }
+
+ 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);
+
+ if (p != c)
+ {
+ r = c;
+
+ // 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);
+ right = Math.Max(right, (int)x + 1);
+ hasRowDiff = true;
+ hasDiff = true;
+ }
+ else
+ {
+ r = replacement;
+ }
+ }
+
+ if (hasRowDiff)
+ {
+ if (top == int.MinValue)
+ {
+ top = y;
+ }
+
+ bottom = y + 1;
+ }
+
+ PixelOperations.Instance.FromVector4Destructive(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
+ }
+
+ Rectangle bounds = Rectangle.FromLTRB(
+ left = Numerics.Clamp(left, 0, resultFrame.Width - 1),
+ top = Numerics.Clamp(top, 0, resultFrame.Height - 1),
+ Numerics.Clamp(right, left + 1, resultFrame.Width),
+ Numerics.Clamp(bottom, top + 1, resultFrame.Height));
+
+ return new(hasDiff, bounds);
+ }
+}
diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs
index 150ee9ccf0..ab05548ac5 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Advanced;
-
namespace SixLabors.ImageSharp.Formats.Gif;
///
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index d22b960ec6..2bc3e53f78 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -4,9 +4,6 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.Intrinsics;
-using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
@@ -97,8 +94,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
- GifFrameMetadata? frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
- int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
+ GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
if (this.quantizer is null)
{
@@ -106,6 +102,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
+ int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
}
else
@@ -132,13 +129,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
WriteHeader(stream);
// Write the LSD.
- transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
- byte backgroundIndex = unchecked((byte)transparencyIndex);
- if (transparencyIndex == -1)
+ int derivedTransparencyIndex = GetTransparentIndex(quantized, null);
+ if (derivedTransparencyIndex >= 0)
{
- backgroundIndex = gifMetadata.BackgroundColorIndex;
+ frameMetadata.HasTransparency = true;
+ frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}
+ byte backgroundIndex = derivedTransparencyIndex >= 0
+ ? frameMetadata.TransparencyIndex
+ : gifMetadata.BackgroundColorIndex;
+
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
@@ -158,16 +159,16 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
- this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex);
+ this.EncodeFirstFrame(stream, frameMetadata, quantized);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? Array.Empty() : quantized.Palette.ToArray();
- quantized.Dispose();
-
- this.EncodeAdditionalFrames(stream, image, globalPalette, transparencyIndex);
+ this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMethod);
stream.WriteByte(GifConstants.EndIntroducer);
+
+ quantized.Dispose();
}
private static GifMetadata GetGifMetadata(Image image)
@@ -194,12 +195,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
return new();
}
- private static GifFrameMetadata? GetGifFrameMetadata(ImageFrame frame, int transparencyIndex)
+ private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex)
where TPixel : unmanaged, IPixel
{
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
{
- return gif;
+ return (GifFrameMetadata)gif.DeepClone();
}
GifFrameMetadata? metadata = null;
@@ -218,17 +219,18 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (metadata?.ColorTableMode == GifColorTableMode.Global && transparencyIndex > -1)
{
metadata.HasTransparency = true;
- metadata.TransparencyIndex = unchecked((byte)transparencyIndex);
+ metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
- return metadata;
+ return metadata ?? new();
}
private void EncodeAdditionalFrames(
Stream stream,
Image image,
ReadOnlyMemory globalPalette,
- int globalTransparencyIndex)
+ int globalTransparencyIndex,
+ GifDisposalMethod previousDisposalMethod)
where TPixel : unmanaged, IPixel
{
if (image.Frames.Count == 1)
@@ -251,15 +253,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{
// Gather the metadata for this frame.
ImageFrame currentFrame = image.Frames[i];
- GifFrameMetadata? gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
+ GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
- int transparencyIndex = gifMetadata?.HasTransparency == true ? gifMetadata.TransparencyIndex : -1;
+ int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
@@ -271,9 +273,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
encodingFrame,
useLocal,
gifMetadata,
- paletteQuantizer);
+ paletteQuantizer,
+ previousDisposalMethod);
previousFrame = currentFrame;
+ previousDisposalMethod = gifMetadata.DisposalMethod;
}
if (hasPaletteQuantizer)
@@ -284,16 +288,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
private void EncodeFirstFrame(
Stream stream,
- GifFrameMetadata? metadata,
- IndexedImageFrame quantized,
- int transparencyIndex)
+ GifFrameMetadata metadata,
+ IndexedImageFrame quantized)
where TPixel : unmanaged, IPixel
{
- this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+ this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle();
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata.ColorTableMode == GifColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
@@ -303,7 +306,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteColorTable(quantized, bitDepth, stream);
}
- this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
private void EncodeAdditionalFrame(
@@ -312,371 +315,121 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame currentFrame,
ImageFrame encodingFrame,
bool useLocal,
- GifFrameMetadata? metadata,
- PaletteQuantizer globalPaletteQuantizer)
+ GifFrameMetadata metadata,
+ PaletteQuantizer globalPaletteQuantizer,
+ GifDisposalMethod previousDisposal)
where TPixel : unmanaged, IPixel
{
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
- int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1;
- Vector4 replacement = Vector4.Zero;
- if (transparencyIndex >= 0)
- {
- if (useLocal)
- {
- if (metadata?.LocalColorTable?.Length > 0)
- {
- ReadOnlySpan palette = metadata.LocalColorTable.Value.Span;
- if (transparencyIndex < palette.Length)
- {
- replacement = palette[transparencyIndex].ToScaledVector4();
- }
- }
- }
- else
- {
- ReadOnlySpan palette = globalPaletteQuantizer.Palette.Span;
- if (transparencyIndex < palette.Length)
- {
- replacement = palette[transparencyIndex].ToScaledVector4();
- }
- }
- }
+ int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
- // We can't deduplicate here as we need the background pixels to be present in the buffer.
- if (metadata?.DisposalMethod == GifDisposalMethod.RestoreToBackground)
- {
- for (int y = 0; y < currentFrame.PixelBuffer.Height; y++)
- {
- Span sourceRow = currentFrame.PixelBuffer.DangerousGetRowSpan(y);
- Span destinationRow = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
- sourceRow.CopyTo(destinationRow);
- }
- }
- else
- {
- this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
- }
+ ImageFrame? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
- IndexedImageFrame quantized;
- if (useLocal)
- {
- // Reassign using the current frame and details.
- if (metadata?.LocalColorTable?.Length > 0)
- {
- // We can use the color data from the decoded metadata here.
- // We avoid dithering by default to preserve the original colors.
- ReadOnlyMemory palette = metadata.LocalColorTable.Value;
- PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
- quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
- else
- {
- // We must quantize the frame to generate a local color table.
- IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
- quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
- }
- else
- {
- // Quantize the image using the global palette.
- // Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
- globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
- quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
+ // Deduplicate and quantize the frame capturing only required parts.
+ (bool difference, Rectangle bounds) = AnimationUtilities.DeDuplicatePixels(this.configuration, previous, currentFrame, encodingFrame, Vector4.Zero);
- // Recalculate the transparency index as depending on the quantizer used could have a new value.
- transparencyIndex = GetTransparentIndex(quantized, metadata);
-
- // Trim down the buffer to the minimum size required.
- Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
- Rectangle interest = TrimTransparentPixels(indices, transparencyIndex);
+ using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
+ encodingFrame,
+ bounds,
+ metadata,
+ useLocal,
+ globalPaletteQuantizer,
+ difference,
+ transparencyIndex);
- this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+ this.WriteGraphicalControlExtension(metadata, stream);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
- this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
+ this.WriteImageDescriptor(bounds, useLocal, bitDepth, stream);
if (useLocal)
{
this.WriteColorTable(quantized, bitDepth, stream);
}
- this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
+ this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
- private void DeDuplicatePixels(
- ImageFrame backgroundFrame,
- ImageFrame sourceFrame,
- ImageFrame resultFrame,
- Vector4 replacement)
+ private IndexedImageFrame QuantizeAdditionalFrameAndUpdateMetadata(
+ ImageFrame encodingFrame,
+ Rectangle bounds,
+ GifFrameMetadata metadata,
+ bool useLocal,
+ PaletteQuantizer globalPaletteQuantizer,
+ bool hasDuplicates,
+ int transparencyIndex)
where TPixel : unmanaged, IPixel
{
- IMemoryOwner buffers = this.memoryAllocator.Allocate(backgroundFrame.Width * 3);
- Span background = buffers.GetSpan()[..backgroundFrame.Width];
- Span source = buffers.GetSpan()[backgroundFrame.Width..];
- Span result = buffers.GetSpan()[(backgroundFrame.Width * 2)..];
-
- // TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color
- // is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better.
- // This would require a more complex algorithm.
- for (int y = 0; y < backgroundFrame.Height; y++)
- {
- PixelOperations.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
- PixelOperations.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
-
- ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background));
- ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source));
- ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result));
-
- uint x = 0;
- int remaining = background.Length;
- if (Avx2.IsSupported && remaining >= 2)
- {
- Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
-
- while (remaining >= 2)
- {
- Vector256 b = Unsafe.Add(ref backgroundBase, x);
- Vector256 s = Unsafe.Add(ref sourceBase, x);
-
- Vector256 m = Avx.CompareEqual(b, s).AsInt32();
-
- m = Avx2.HorizontalAdd(m, m);
- m = Avx2.HorizontalAdd(m, m);
- m = Avx2.CompareEqual(m, Vector256.Create(-4));
-
- Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle());
-
- x++;
- remaining -= 2;
- }
- }
-
- for (int i = remaining; i >= 0; i--)
- {
- x = (uint)i;
- Vector4 b = Unsafe.Add(ref Unsafe.As, Vector4>(ref backgroundBase), x);
- Vector4 s = Unsafe.Add(ref Unsafe.As, Vector4>(ref sourceBase), x);
- ref Vector4 r = ref Unsafe.Add(ref Unsafe.As, Vector4>(ref resultBase), x);
- r = (b == s) ? replacement : s;
- }
-
- PixelOperations.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
- }
- }
-
- private static Rectangle TrimTransparentPixels(Buffer2D buffer, int transparencyIndex)
- {
- if (transparencyIndex < 0)
- {
- return buffer.FullRectangle();
- }
-
- byte trimmableIndex = unchecked((byte)transparencyIndex);
-
- int top = int.MinValue;
- int bottom = int.MaxValue;
- int left = int.MaxValue;
- int right = int.MinValue;
- int minY = -1;
- bool isTransparentRow = true;
-
- // Run through the buffer in a single pass. Use variables to track the min/max values.
- for (int y = 0; y < buffer.Height; y++)
+ IndexedImageFrame quantized;
+ if (useLocal)
{
- isTransparentRow = true;
- Span rowSpan = buffer.DangerousGetRowSpan(y);
- ref byte rowPtr = ref MemoryMarshal.GetReference(rowSpan);
- nint rowLength = (nint)(uint)rowSpan.Length;
- nint x = 0;
-
-#if NET7_0_OR_GREATER
- if (Vector128.IsHardwareAccelerated && rowLength >= Vector128.Count)
- {
- Vector256 trimmableVec256 = Vector256.Create(trimmableIndex);
-
- if (Vector256.IsHardwareAccelerated && rowLength >= Vector256.Count)
- {
- do
- {
- Vector256 vec = Vector256.LoadUnsafe(ref rowPtr, (nuint)x);
- Vector256 notEquals = ~Vector256.Equals(vec, trimmableVec256);
- uint mask = notEquals.ExtractMostSignificantBits();
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)uint.TrailingZeroCount(mask);
- nint end = (nint)uint.LeadingZeroCount(mask);
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector256.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector256.Count;
- }
- while (x <= rowLength - Vector256.Count);
- }
-
- Vector128 trimmableVec = Vector256.IsHardwareAccelerated
- ? trimmableVec256.GetLower()
- : Vector128.Create(trimmableIndex);
-
- while (x <= rowLength - Vector128.Count)
- {
- Vector128 vec = Vector128.LoadUnsafe(ref rowPtr, (nuint)x);
- Vector128 notEquals = ~Vector128.Equals(vec, trimmableVec);
- uint mask = notEquals.ExtractMostSignificantBits();
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)uint.TrailingZeroCount(mask);
- nint end = (nint)uint.LeadingZeroCount(mask) - Vector128.Count;
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector128.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector128.Count;
- }
- }
-#else
- if (Sse41.IsSupported && rowLength >= Vector128.Count)
+ // Reassign using the current frame and details.
+ if (metadata.LocalColorTable?.Length > 0)
{
- Vector256 trimmableVec256 = Vector256.Create(trimmableIndex);
+ // We can use the color data from the decoded metadata here.
+ // We avoid dithering by default to preserve the original colors.
+ ReadOnlyMemory palette = metadata.LocalColorTable.Value;
- if (Avx2.IsSupported && rowLength >= Vector256.Count)
+ if (hasDuplicates && !metadata.HasTransparency)
{
- do
- {
- Vector256 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x));
- Vector256 notEquals = Avx2.CompareEqual(vec, trimmableVec256);
- notEquals = Avx2.Xor(notEquals, Vector256.AllBitsSet);
- int mask = Avx2.MoveMask(notEquals);
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
- nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask);
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector256.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector256.Count;
- }
- while (x <= rowLength - Vector256.Count);
+ // A difference was captured but the metadata does not have transparency.
+ metadata.HasTransparency = true;
+ transparencyIndex = palette.Length;
+ metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
- Vector128 trimmableVec = Sse41.IsSupported
- ? trimmableVec256.GetLower()
- : Vector128.Create(trimmableIndex);
-
- while (x <= rowLength - Vector128.Count)
- {
- Vector128 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x));
- Vector128 notEquals = Sse2.CompareEqual(vec, trimmableVec);
- notEquals = Sse2.Xor(notEquals, Vector128.AllBitsSet);
- int mask = Sse2.MoveMask(notEquals);
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
- nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask) - Vector128.Count;
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector128.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector128.Count;
- }
+ PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
-#endif
- for (; x < rowLength; ++x)
+ else
{
- if (Unsafe.Add(ref rowPtr, x) != trimmableIndex)
- {
- isTransparentRow = false;
- left = Math.Min(left, (int)x);
- right = Math.Max(right, (int)x);
- }
- }
+ // We must quantize the frame to generate a local color table.
+ IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
- if (!isTransparentRow)
- {
- if (y == 0)
+ // The transparency index derived by the quantizer might differ from the index
+ // within the metadata. We need to update the metadata to reflect this.
+ int derivedTransparencyIndex = GetTransparentIndex(quantized, null);
+ if (derivedTransparencyIndex < 0)
{
- // First row is opaque.
- // Capture to prevent over assignment when a match is found below.
- top = 0;
+ // If no index is found set to the palette length, this trick allows us to fake transparency without an explicit index.
+ derivedTransparencyIndex = quantized.Palette.Length;
}
- // The minimum top bounds have already been captured.
- // Increment the bottom to include the current opaque row.
- if (minY < 0 && top != 0)
- {
- // Increment to the first opaque row.
- top++;
- }
+ metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
- minY = top;
- bottom = y;
- }
- else
- {
- // We've yet to hit an opaque row. Capture the top position.
- if (minY < 0)
+ if (hasDuplicates)
{
- top = Math.Max(top, y);
+ metadata.HasTransparency = true;
}
-
- bottom = Math.Min(bottom, y);
}
}
-
- if (left == int.MaxValue)
- {
- left = 0;
- }
-
- if (right == int.MinValue)
+ else
{
- right = buffer.Width;
- }
+ // Quantize the image using the global palette.
+ // Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
- if (top == bottom || left == right)
- {
- // The entire image is transparent.
- return buffer.FullRectangle();
- }
+ // A difference was captured but the metadata does not have transparency.
+ if (hasDuplicates && !metadata.HasTransparency)
+ {
+ metadata.HasTransparency = true;
+ transparencyIndex = globalPaletteQuantizer.Palette.Length;
+ metadata.TransparencyIndex = ClampIndex(transparencyIndex);
+ }
- if (!isTransparentRow)
- {
- // Last row is opaque.
- bottom = buffer.Height;
+ globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
+ quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds);
}
- return Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height));
+ return quantized;
}
+ private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
+
///
/// Returns the index of the most transparent color in the palette.
///
@@ -868,30 +621,19 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Writes the optional graphics control extension to the stream.
///
/// The metadata of the image or frame.
- /// The index of the color in the color palette to make transparent.
/// The stream to write to.
- private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
+ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, Stream stream)
{
- GifFrameMetadata? data = metadata;
- bool hasTransparency;
- if (metadata is null)
- {
- data = new();
- hasTransparency = transparencyIndex >= 0;
- }
- else
- {
- hasTransparency = metadata.HasTransparency;
- }
+ bool hasTransparency = metadata.HasTransparency;
byte packedValue = GifGraphicControlExtension.GetPackedValue(
- disposalMethod: data!.DisposalMethod,
+ disposalMethod: metadata.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
- delayTime: (ushort)data.FrameDelay,
- transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
+ delayTime: (ushort)metadata.FrameDelay,
+ transparencyIndex: hasTransparency ? metadata.TransparencyIndex : byte.MinValue);
this.WriteExtension(extension, stream);
}
@@ -992,14 +734,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Writes the image pixel data to the stream.
///
/// The containing indexed pixels.
- /// The region of interest.
/// The stream to write to.
/// The length of the frame color palette.
/// The index of the color used to represent transparency.
- private void WriteImageData(Buffer2D indices, Rectangle interest, Stream stream, int paletteLength, int transparencyIndex)
+ private void WriteImageData(Buffer2D indices, Stream stream, int paletteLength, int transparencyIndex)
{
- Buffer2DRegion region = indices.GetRegion(interest);
-
// Pad the bit depth when required for encoding the image data.
// This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette
// as decoders skip indexes that are out of range.
@@ -1008,6 +747,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
: 0;
using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding));
- encoder.Encode(region, stream);
+ encoder.Encode(indices, stream);
}
}
diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index 4b40c44e45..d4050810df 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
///
/// The 2D buffer of indexed pixels.
/// The stream to write to.
- public void Encode(Buffer2DRegion indexedPixels, Stream stream)
+ public void Encode(Buffer2D indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@@ -204,7 +204,7 @@ internal sealed class LzwEncoder : IDisposable
/// The number of bits
/// See
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetMaxcode(int bitCount) => (1 << bitCount) - 1;
+ private static int GetMaxCode(int bitCount) => (1 << bitCount) - 1;
///
/// Add a character to the end of the current packet, and if it is 254 characters,
@@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable
/// The 2D buffer of indexed pixels.
/// The initial bits.
/// The stream to write to.
- private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream)
+ private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;
@@ -257,7 +257,7 @@ internal sealed class LzwEncoder : IDisposable
// Set up the necessary values
this.clearFlag = false;
this.bitCount = this.globalInitialBits;
- this.maxCode = GetMaxcode(this.bitCount);
+ this.maxCode = GetMaxCode(this.bitCount);
this.clearCode = 1 << (initialBits - 1);
this.eofCode = this.clearCode + 1;
this.freeEntry = this.clearCode + 2;
@@ -383,7 +383,7 @@ internal sealed class LzwEncoder : IDisposable
{
if (this.clearFlag)
{
- this.maxCode = GetMaxcode(this.bitCount = this.globalInitialBits);
+ this.maxCode = GetMaxCode(this.bitCount = this.globalInitialBits);
this.clearFlag = false;
}
else
@@ -391,7 +391,7 @@ internal sealed class LzwEncoder : IDisposable
++this.bitCount;
this.maxCode = this.bitCount == MaxBits
? MaxMaxCode
- : GetMaxcode(this.bitCount);
+ : GetMaxCode(this.bitCount);
}
}
diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
index 754aac90ea..3c56d0243b 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
@@ -107,15 +107,15 @@ public readonly partial struct ErrorDither : IDither, IEquatable, I
float scale = quantizer.Options.DitherScale;
Buffer2D sourceBuffer = source.PixelBuffer;
- for (int y = bounds.Top; y < bounds.Bottom; y++)
+ for (int y = 0; y < destination.Height; y++)
{
- ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(sourceBuffer.DangerousGetRowSpan(y));
- ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
+ ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
+ Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y);
- for (int x = bounds.Left; x < bounds.Right; x++)
+ for (int x = 0; x < destinationRow.Length; x++)
{
- TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, (uint)x);
- Unsafe.Add(ref destinationRowRef, (uint)(x - offsetX)) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
+ TPixel sourcePixel = sourceRow[x + offsetX];
+ destinationRow[x] = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
}
}
@@ -200,7 +200,7 @@ public readonly partial struct ErrorDither : IDither, IEquatable, I
}
ref TPixel pixel = ref rowSpan[targetX];
- var result = pixel.ToVector4();
+ Vector4 result = pixel.ToVector4();
result += error * coefficient;
pixel.FromVector4(result);
diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
index 8aa166d16c..3c7d576709 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
-using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
@@ -23,6 +22,7 @@ internal sealed class EuclideanPixelMap : IDisposable
{
private Rgba32[] rgbaPalette;
private int transparentIndex;
+ private readonly TPixel transparentMatch;
///
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@@ -54,8 +54,9 @@ internal sealed class EuclideanPixelMap : IDisposable
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
- // If the provided transparentIndex is outside of the palette, silently ignore it.
- this.transparentIndex = transparentIndex < this.Palette.Length ? transparentIndex : -1;
+ this.transparentIndex = transparentIndex;
+ Unsafe.SkipInit(out this.transparentMatch);
+ this.transparentMatch.FromRgba32(default);
}
///
@@ -97,32 +98,40 @@ internal sealed class EuclideanPixelMap : IDisposable
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
PixelOperations.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette);
+ this.transparentIndex = -1;
this.cache.Clear();
}
///
- /// Allows setting the transparent index after construction. If the provided transparentIndex is outside of the palette, silently ignore it.
+ /// Allows setting the transparent index after construction.
///
/// An explicit index at which to match transparent pixels.
- public void SetTransparentIndex(int index) => this.transparentIndex = index < this.Palette.Length ? index : -1;
+ public void SetTransparentIndex(int index)
+ {
+ if (index != this.transparentIndex)
+ {
+ this.cache.Clear();
+ }
+
+ this.transparentIndex = index;
+ }
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
- float leastDistance = float.MaxValue;
if (this.transparentIndex >= 0 && rgba == default)
{
// We have explicit instructions. No need to search.
index = this.transparentIndex;
- DebugGuard.MustBeLessThan(index, this.Palette.Length, nameof(index));
this.cache.Add(rgba, (byte)index);
- match = Unsafe.Add(ref paletteRef, (uint)index);
+ match = this.transparentMatch;
return index;
}
+ float leastDistance = float.MaxValue;
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Rgba32 candidate = this.rgbaPalette[i];
@@ -175,18 +184,24 @@ internal sealed class EuclideanPixelMap : IDisposable
/// The granularity of the cache has been determined based upon the current
/// suite of test images and provides the lowest possible memory usage while
/// providing enough match accuracy.
- /// Entry count is currently limited to 2335905 entries (4671810 bytes ~4.45MB).
+ /// Entry count is currently limited to 4601025 entries (8MB).
///
///
private unsafe struct ColorDistanceCache : IDisposable
{
- private const int IndexBits = 5;
- private const int IndexAlphaBits = 6;
- private const int IndexCount = (1 << IndexBits) + 1;
- private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
- private const int RgbShift = 8 - IndexBits;
- private const int AlphaShift = 8 - IndexAlphaBits;
- private const int Entries = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
+ private const int IndexRBits = 5;
+ private const int IndexGBits = 5;
+ private const int IndexBBits = 6;
+ private const int IndexABits = 6;
+ private const int IndexRCount = (1 << IndexRBits) + 1;
+ private const int IndexGCount = (1 << IndexGBits) + 1;
+ private const int IndexBCount = (1 << IndexBBits) + 1;
+ private const int IndexACount = (1 << IndexABits) + 1;
+ private const int RShift = 8 - IndexRBits;
+ private const int GShift = 8 - IndexGBits;
+ private const int BShift = 8 - IndexBBits;
+ private const int AShift = 8 - IndexABits;
+ private const int Entries = IndexRCount * IndexGCount * IndexBCount * IndexACount;
private MemoryHandle tableHandle;
private readonly IMemoryOwner table;
private readonly short* tablePointer;
@@ -202,22 +217,14 @@ internal sealed class EuclideanPixelMap : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
public readonly void Add(Rgba32 rgba, byte index)
{
- int r = rgba.R >> RgbShift;
- int g = rgba.G >> RgbShift;
- int b = rgba.B >> RgbShift;
- int a = rgba.A >> AlphaShift;
- int idx = GetPaletteIndex(r, g, b, a);
+ int idx = GetPaletteIndex(rgba);
this.tablePointer[idx] = index;
}
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryGetValue(Rgba32 rgba, out short match)
{
- int r = rgba.R >> RgbShift;
- int g = rgba.G >> RgbShift;
- int b = rgba.B >> RgbShift;
- int a = rgba.A >> AlphaShift;
- int idx = GetPaletteIndex(r, g, b, a);
+ int idx = GetPaletteIndex(rgba);
match = this.tablePointer[idx];
return match > -1;
}
@@ -229,15 +236,17 @@ internal sealed class EuclideanPixelMap : IDisposable
public readonly void Clear() => this.table.GetSpan().Fill(-1);
[MethodImpl(InliningOptions.ShortMethod)]
- private static int GetPaletteIndex(int r, int g, int b, int a)
- => (r << ((IndexBits << 1) + IndexAlphaBits))
- + (r << (IndexBits + IndexAlphaBits + 1))
- + (g << (IndexBits + IndexAlphaBits))
- + (r << (IndexBits << 1))
- + (r << (IndexBits + 1))
- + (g << IndexBits)
- + ((r + g + b) << IndexAlphaBits)
- + r + g + b + a;
+ private static int GetPaletteIndex(Rgba32 rgba)
+ {
+ int rIndex = rgba.R >> RShift;
+ int gIndex = rgba.G >> GShift;
+ int bIndex = rgba.B >> BShift;
+ int aIndex = rgba.A >> AShift;
+
+ return (aIndex * (IndexRCount * IndexGCount * IndexBCount)) +
+ (rIndex * (IndexGCount * IndexBCount)) +
+ (gIndex * IndexBCount) + bIndex;
+ }
public void Dispose()
{
diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
index a7edec662e..10ffe68dca 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
@@ -156,10 +156,10 @@ public static class QuantizerUtilities
for (int y = 0; y < destination.Height; y++)
{
- Span sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
+ ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y);
- for (int x = 0; x < destination.Width; x++)
+ for (int x = 0; x < destinationRow.Length; x++)
{
destinationRow[x] = Unsafe.AsRef(quantizer).GetQuantizedColor(sourceRow[x + offsetX], out TPixel _);
}
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
index cd485b5fab..ef04a4fbaf 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
@@ -226,8 +226,6 @@ public class GifEncoderTests
}
Assert.Equal(iMeta.FrameDelay, cMeta.FrameDelay);
- Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
- Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
}
image.Dispose();
@@ -328,6 +326,8 @@ public class GifEncoderTests
using Image output = Image.Load(memStream);
+ image.Save(provider.Utility.GetTestOutputFileName("gif"), new GifEncoder());
+
// TODO: Find a better way to compare.
// The image has been visually checked but the quantization and frame trimming pattern used in the gif encoder
// means we cannot use an exact comparison nor replicate using the quantizing processor.
@@ -357,4 +357,18 @@ public class GifEncoderTests
}
}
}
+
+ public static string[] Animated => TestImages.Gif.Animated;
+
+ [Theory]//(Skip = "Enable for visual animated testing")]
+ [WithFileCollection(nameof(Animated), PixelTypes.Rgba32)]
+ public void Encode_Animated_VisualTest(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+
+ //image.DebugSaveMultiFrame(provider);
+
+ provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { ColorTableMode = GifColorTableMode.Local}, "animated");
+ }
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 6ad93adfbd..5b49e5aba0 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -479,6 +479,7 @@ public static class TestImages
public const string Ratio1x4 = "Gif/base_1x4.gif";
public const string LargeComment = "Gif/large_comment.gif";
public const string GlobalQuantizationTest = "Gif/GlobalQuantizationTest.gif";
+ public const string MixedDisposal = "Gif/mixed-disposal.gif";
// Test images from https://github.com/robert-ancell/pygif/tree/master/test-suite
public const string ZeroSize = "Gif/image-zero-size.gif";
@@ -508,7 +509,24 @@ public static class TestImages
public const string Issue2198 = "Gif/issues/issue_2198.gif";
}
- public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
+ public static readonly string[] Animated =
+ {
+ Giphy,
+ Cheers,
+ Kumin,
+ Leo,
+ MixedDisposal,
+ GlobalQuantizationTest,
+ Issues.Issue2198,
+ Issues.Issue2288_A,
+ Issues.Issue2288_B,
+ Issues.Issue2288_C,
+ Issues.Issue2288_D,
+ Issues.Issue2450_A,
+ Issues.Issue2450_B,
+ Issues.BadDescriptorWidth,
+ Issues.Issue1530
+ };
}
public static class Tga
diff --git a/tests/Images/Input/Gif/mixed-disposal.gif b/tests/Images/Input/Gif/mixed-disposal.gif
new file mode 100644
index 0000000000..07ca32e915
--- /dev/null
+++ b/tests/Images/Input/Gif/mixed-disposal.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd7d4093d6d75aa149418936bac73f66c3d81d9e01252993f321ee792514a47a
+size 16636