Browse Source

Deduper works

pull/2588/head
James Jackson-South 3 years ago
parent
commit
958c9c9b10
  1. 157
      src/ImageSharp/Formats/AnimationUtilities.cs
  2. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  3. 479
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 12
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  5. 14
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  6. 79
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  7. 4
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  8. 18
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  9. 20
      tests/ImageSharp.Tests/TestImages.cs
  10. 3
      tests/Images/Input/Gif/mixed-disposal.gif

157
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;
/// <summary>
/// Utility methods for animated formats.
/// </summary>
internal static class AnimationUtilities
{
/// <summary>
/// Deduplicates pixels between the previous and current frame returning only the changed pixels and bounds.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="previousFrame">The previous frame if present.</param>
/// <param name="currentFrame">The current frame.</param>
/// <param name="resultFrame">The resultant output.</param>
/// <param name="replacement">The value to use when replacing duplicate pixels.</param>
/// <returns>The <see cref="ValueTuple{Boolean, Rectangle}"/> representing the operation result.</returns>
public static (bool Difference, Rectangle Bounds) DeDuplicatePixels<TPixel>(
Configuration configuration,
ImageFrame<TPixel>? previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel> resultFrame,
Vector4 replacement)
where TPixel : unmanaged, IPixel<TPixel>
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
IMemoryOwner<Vector4> buffers = memoryAllocator.Allocate<Vector4>(currentFrame.Width * 3, AllocationOptions.Clean);
Span<Vector4> previous = buffers.GetSpan()[..currentFrame.Width];
Span<Vector4> current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
Span<Vector4> 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<TPixel>.Instance.ToVector4(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous, PixelConversionModifiers.Scale);
}
PixelOperations<TPixel>.Instance.ToVector4(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current, PixelConversionModifiers.Scale);
ref Vector256<float> previousBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(previous));
ref Vector256<float> currentBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(current));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
int size = Unsafe.SizeOf<Vector4>();
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<float> p = Unsafe.Add(ref previousBase, x);
Vector256<float> c = Unsafe.Add(ref currentBase, x);
// Compare the previous and current pixels
Vector256<float> neq = Avx.CompareEqual(p, c);
Vector256<int> mask = neq.AsInt32();
neq = Avx.Xor(neq, Vector256<float>.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<float> 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<Vector256<float>, Vector4>(ref previousBase), x);
Vector4 c = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref currentBase), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, 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<TPixel>.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);
}
}

2
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;
/// <summary>

479
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<TPixel>() : 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<TPixel>(Image<TPixel> image)
@ -194,12 +195,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
return new();
}
private static GifFrameMetadata? GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(
Stream stream,
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex)
int globalTransparencyIndex,
GifDisposalMethod previousDisposalMethod)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -251,15 +253,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{
// Gather the metadata for this frame.
ImageFrame<TPixel> 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<TPixel>(
Stream stream,
GifFrameMetadata? metadata,
IndexedImageFrame<TPixel> quantized,
int transparencyIndex)
GifFrameMetadata metadata,
IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel>
{
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D<byte> 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<TPixel>(
@ -312,371 +315,121 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel> encodingFrame,
bool useLocal,
GifFrameMetadata? metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer)
GifFrameMetadata metadata,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
GifDisposalMethod previousDisposal)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<Color> palette = metadata.LocalColorTable.Value.Span;
if (transparencyIndex < palette.Length)
{
replacement = palette[transparencyIndex].ToScaledVector4();
}
}
}
else
{
ReadOnlySpan<TPixel> 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<TPixel> sourceRow = currentFrame.PixelBuffer.DangerousGetRowSpan(y);
Span<TPixel> destinationRow = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
sourceRow.CopyTo(destinationRow);
}
}
else
{
this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
}
ImageFrame<TPixel>? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
IndexedImageFrame<TPixel> 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<Color> palette = metadata.LocalColorTable.Value;
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = TrimTransparentPixels(indices, transparencyIndex);
using IndexedImageFrame<TPixel> 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<byte> indices = ((IPixelSource)quantized).PixelBuffer;
this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
private void DeDuplicatePixels<TPixel>(
ImageFrame<TPixel> backgroundFrame,
ImageFrame<TPixel> sourceFrame,
ImageFrame<TPixel> resultFrame,
Vector4 replacement)
private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixel>(
ImageFrame<TPixel> encodingFrame,
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
bool hasDuplicates,
int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
IMemoryOwner<Vector4> buffers = this.memoryAllocator.Allocate<Vector4>(backgroundFrame.Width * 3);
Span<Vector4> background = buffers.GetSpan()[..backgroundFrame.Width];
Span<Vector4> source = buffers.GetSpan()[backgroundFrame.Width..];
Span<Vector4> 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<TPixel>.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
ref Vector256<float> backgroundBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(background));
ref Vector256<float> sourceBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
uint x = 0;
int remaining = background.Length;
if (Avx2.IsSupported && remaining >= 2)
{
Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
while (remaining >= 2)
{
Vector256<float> b = Unsafe.Add(ref backgroundBase, x);
Vector256<float> s = Unsafe.Add(ref sourceBase, x);
Vector256<int> 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<Vector256<float>, Vector4>(ref backgroundBase), x);
Vector4 s = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref sourceBase), x);
ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
r = (b == s) ? replacement : s;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
}
}
private static Rectangle TrimTransparentPixels(Buffer2D<byte> 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<TPixel> quantized;
if (useLocal)
{
isTransparentRow = true;
Span<byte> 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<byte>.Count)
{
Vector256<byte> trimmableVec256 = Vector256.Create(trimmableIndex);
if (Vector256.IsHardwareAccelerated && rowLength >= Vector256<byte>.Count)
{
do
{
Vector256<byte> vec = Vector256.LoadUnsafe(ref rowPtr, (nuint)x);
Vector256<byte> 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<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector256<byte>.Count;
}
while (x <= rowLength - Vector256<byte>.Count);
}
Vector128<byte> trimmableVec = Vector256.IsHardwareAccelerated
? trimmableVec256.GetLower()
: Vector128.Create(trimmableIndex);
while (x <= rowLength - Vector128<byte>.Count)
{
Vector128<byte> vec = Vector128.LoadUnsafe(ref rowPtr, (nuint)x);
Vector128<byte> 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<byte>.Count;
// end is from the end, but we need the index from the beginning
end = x + Vector128<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector128<byte>.Count;
}
}
#else
if (Sse41.IsSupported && rowLength >= Vector128<byte>.Count)
// Reassign using the current frame and details.
if (metadata.LocalColorTable?.Length > 0)
{
Vector256<byte> 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<Color> palette = metadata.LocalColorTable.Value;
if (Avx2.IsSupported && rowLength >= Vector256<byte>.Count)
if (hasDuplicates && !metadata.HasTransparency)
{
do
{
Vector256<byte> vec = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref rowPtr, x));
Vector256<byte> notEquals = Avx2.CompareEqual(vec, trimmableVec256);
notEquals = Avx2.Xor(notEquals, Vector256<byte>.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<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector256<byte>.Count;
}
while (x <= rowLength - Vector256<byte>.Count);
// A difference was captured but the metadata does not have transparency.
metadata.HasTransparency = true;
transparencyIndex = palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
Vector128<byte> trimmableVec = Sse41.IsSupported
? trimmableVec256.GetLower()
: Vector128.Create(trimmableIndex);
while (x <= rowLength - Vector128<byte>.Count)
{
Vector128<byte> vec = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref rowPtr, x));
Vector128<byte> notEquals = Sse2.CompareEqual(vec, trimmableVec);
notEquals = Sse2.Xor(notEquals, Vector128<byte>.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<byte>.Count;
// end is from the end, but we need the index from the beginning
end = x + Vector128<byte>.Count - 1 - end;
left = Math.Min(left, (int)start);
right = Math.Max(right, (int)end);
}
x += Vector128<byte>.Count;
}
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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);
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
@ -868,30 +621,19 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Writes the optional graphics control extension to the stream.
/// </summary>
/// <param name="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</param>
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.
/// </summary>
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
/// <param name="interest">The region of interest.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="paletteLength">The length of the frame color palette.</param>
/// <param name="transparencyIndex">The index of the color used to represent transparency.</param>
private void WriteImageData(Buffer2D<byte> indices, Rectangle interest, Stream stream, int paletteLength, int transparencyIndex)
private void WriteImageData(Buffer2D<byte> indices, Stream stream, int paletteLength, int transparencyIndex)
{
Buffer2DRegion<byte> 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);
}
}

12
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
/// </summary>
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
public void Encode(Buffer2DRegion<byte> indexedPixels, Stream stream)
public void Encode(Buffer2D<byte> indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@ -204,7 +204,7 @@ internal sealed class LzwEncoder : IDisposable
/// <param name="bitCount">The number of bits</param>
/// <returns>See <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetMaxcode(int bitCount) => (1 << bitCount) - 1;
private static int GetMaxCode(int bitCount) => (1 << bitCount) - 1;
/// <summary>
/// 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
/// <param name="indexedPixels">The 2D buffer of indexed pixels.</param>
/// <param name="initialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</param>
private void Compress(Buffer2DRegion<byte> indexedPixels, int initialBits, Stream stream)
private void Compress(Buffer2D<byte> 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);
}
}

14
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -107,15 +107,15 @@ public readonly partial struct ErrorDither : IDither, IEquatable<ErrorDither>, I
float scale = quantizer.Options.DitherScale;
Buffer2D<TPixel> 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<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
Span<byte> 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<ErrorDither>, I
}
ref TPixel pixel = ref rowSpan[targetX];
var result = pixel.ToVector4();
Vector4 result = pixel.ToVector4();
result += error * coefficient;
pixel.FromVector4(result);

79
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<TPixel> : IDisposable
{
private Rgba32[] rgbaPalette;
private int transparentIndex;
private readonly TPixel transparentMatch;
/// <summary>
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@ -54,8 +54,9 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.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);
}
/// <summary>
@ -97,32 +98,40 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette);
this.transparentIndex = -1;
this.cache.Clear();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
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<TPixel> : 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).
/// </para>
/// </remarks>
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<short> table;
private readonly short* tablePointer;
@ -202,22 +217,14 @@ internal sealed class EuclideanPixelMap<TPixel> : 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<TPixel> : 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()
{

4
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -156,10 +156,10 @@ public static class QuantizerUtilities
for (int y = 0; y < destination.Height; y++)
{
Span<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
ReadOnlySpan<TPixel> sourceRow = sourceBuffer.DangerousGetRowSpan(y + offsetY);
Span<byte> 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 _);
}

18
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<TPixel> output = Image.Load<TPixel>(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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
//image.DebugSaveMultiFrame(provider);
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { ColorTableMode = GifColorTableMode.Local}, "animated");
}
}

20
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

3
tests/Images/Input/Gif/mixed-disposal.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd7d4093d6d75aa149418936bac73f66c3d81d9e01252993f321ee792514a47a
size 16636
Loading…
Cancel
Save