diff --git a/src/ImageSharp/Color/Color.Conversions.cs b/src/ImageSharp/Color/Color.Conversions.cs
index bbb848867d..309ab83ec4 100644
--- a/src/ImageSharp/Color/Color.Conversions.cs
+++ b/src/ImageSharp/Color/Color.Conversions.cs
@@ -139,7 +139,7 @@ public Color(Vector4 vector)
///
/// The .
/// The .
- public static explicit operator Vector4(Color color) => color.ToVector4();
+ public static explicit operator Vector4(Color color) => color.ToScaledVector4();
///
/// Converts an to .
@@ -228,7 +228,7 @@ internal Bgr24 ToBgr24()
}
[MethodImpl(InliningOptions.ShortMethod)]
- internal Vector4 ToVector4()
+ internal Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
{
diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
index e87872a707..7caaa5868d 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
@@ -629,6 +629,33 @@ public static Vector256 MultiplyAddNegated(
return Avx.Subtract(c, Avx.Multiply(a, b));
}
+ ///
+ /// Blend packed 8-bit integers from and using .
+ /// The high bit of each corresponding byte determines the selection.
+ /// If the high bit is set the element of is selected.
+ /// The element of is selected otherwise.
+ ///
+ /// The left vector.
+ /// The right vector.
+ /// The mask vector.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask)
+ {
+ if (Sse41.IsSupported)
+ {
+ return Sse41.BlendVariable(left, right, mask);
+ }
+ else if (Sse2.IsSupported)
+ {
+ return Sse2.Or(Sse2.And(right, mask), Sse2.AndNot(mask, left));
+ }
+
+ // Use a signed shift right to create a mask with the sign bit.
+ Vector128 signedMask = AdvSimd.ShiftRightArithmetic(mask.AsInt16(), 7);
+ return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte();
+ }
+
///
/// as many elements as possible, slicing them down (keeping the remainder).
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
index 156e2f9610..aecea7dedf 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Bmp;
@@ -10,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp;
///
public sealed class BmpEncoder : QuantizingImageEncoder
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
+
///
/// Gets the number of bits per pixel.
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index ce1660a912..9d5b8d0cfd 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -9,6 +9,7 @@
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp;
@@ -100,7 +101,7 @@ public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
- this.quantizer = encoder.Quantizer;
+ this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index 55ad2c4585..bc41c89dcf 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -29,6 +29,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
///
private IMemoryOwner? globalColorTable;
+ ///
+ /// The current local color table.
+ ///
+ private IMemoryOwner? currentLocalColorTable;
+
+ ///
+ /// Gets the size in bytes of the current local color table.
+ ///
+ private int currentLocalColorTableSize;
+
///
/// The area to restore.
///
@@ -159,6 +169,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
}
if (image is null)
@@ -229,6 +240,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
}
if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
@@ -332,7 +344,7 @@ private void ReadApplicationExtension(BufferedReadStream stream)
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
- this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount;
+ this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
stream.Skip(1); // Skip the terminator.
return;
}
@@ -415,25 +427,27 @@ private void ReadFrame(BufferedReadStream stream, ref Image? ima
{
this.ReadImageDescriptor(stream);
- IMemoryOwner? localColorTable = null;
Buffer2D? indices = null;
try
{
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
- if (this.imageDescriptor.LocalColorTableFlag)
+ bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
+
+ if (hasLocalColorTable)
{
- int length = this.imageDescriptor.LocalColorTableSize * 3;
- localColorTable = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean);
- stream.Read(localColorTable.GetSpan());
+ // Read and store the local color table. We allocate the maximum possible size and slice to match.
+ int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
+ this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
indices = this.configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices);
Span rawColorTable = default;
- if (localColorTable != null)
+ if (hasLocalColorTable)
{
- rawColorTable = localColorTable.GetSpan();
+ rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
}
else if (this.globalColorTable != null)
{
@@ -448,7 +462,6 @@ private void ReadFrame(BufferedReadStream stream, ref Image? ima
}
finally
{
- localColorTable?.Dispose();
indices?.Dispose();
}
}
@@ -509,7 +522,10 @@ private void ReadFrameColors(ref Image? image, ref ImageFrame(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
// Skip the frame indices. Pixels length + mincode size.
@@ -682,7 +701,6 @@ private void SetFrameMetadata(ImageFrameMetadata metadata)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Global;
- gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
if (this.imageDescriptor.LocalColorTableFlag
@@ -690,13 +708,23 @@ private void SetFrameMetadata(ImageFrameMetadata metadata)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
gifMeta.ColorTableMode = GifColorTableMode.Local;
- gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
+
+ Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize];
+ ReadOnlySpan rgbTable = MemoryMarshal.Cast(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]);
+ for (int i = 0; i < colorTable.Length; i++)
+ {
+ colorTable[i] = new Color(rgbTable[i]);
+ }
+
+ gifMeta.LocalColorTable = colorTable;
}
// Graphics control extensions is optional.
if (this.graphicsControlExtension != default)
{
GifFrameMetadata gifMeta = metadata.GetGifMetadata();
+ gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag;
+ gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex;
gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}
@@ -751,14 +779,22 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
- this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
-
if (globalColorTableLength > 0)
{
this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean);
- // Read the global color table data from the stream
- stream.Read(this.globalColorTable.GetSpan());
+ // Read the global color table data from the stream and preserve it in the gif metadata
+ Span globalColorTableSpan = this.globalColorTable.GetSpan();
+ stream.Read(globalColorTableSpan);
+
+ Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
+ ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan);
+ for (int i = 0; i < colorTable.Length; i++)
+ {
+ colorTable[i] = new Color(rgbTable[i]);
+ }
+
+ this.gifMetadata.GlobalColorTable = colorTable;
}
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index c01cc78ef0..ccf8feaccd 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -2,13 +2,17 @@
// 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.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif;
@@ -36,17 +40,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// The quantizer used to generate the color palette.
///
- private readonly IQuantizer quantizer;
+ private IQuantizer? quantizer;
///
- /// The color table mode: Global or local.
+ /// Whether the quantizer was supplied via options.
///
- private GifColorTableMode? colorTableMode;
+ private readonly bool hasQuantizer;
///
- /// The number of bits requires to store the color palette.
+ /// The color table mode: Global or local.
///
- private int bitDepth;
+ private GifColorTableMode? colorTableMode;
///
/// The pixel sampling strategy for global quantization.
@@ -56,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// Initializes a new instance of the class.
///
- /// The configuration which allows altering default behaviour or extending the library.
+ /// The configuration which allows altering default behavior or extending the library.
/// The encoder with options.
public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{
@@ -64,6 +68,7 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder)
this.memoryAllocator = configuration.MemoryAllocator;
this.skipMetadata = encoder.SkipMetadata;
this.quantizer = encoder.Quantizer;
+ this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
}
@@ -86,8 +91,28 @@ public void Encode(Image image, Stream stream, CancellationToken
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
- // Quantize the image returning a palette.
- IndexedImageFrame? quantized;
+ // Quantize the first image frame returning a palette.
+ IndexedImageFrame? quantized = null;
+
+ // 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.
+ image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
+ int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
+
+ if (this.quantizer is null)
+ {
+ // Is this a gif with color information. If so use that, otherwise use octree.
+ if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
+ {
+ // We avoid dithering by default to preserve the original colors.
+ this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
+ }
+ else
+ {
+ this.quantizer = KnownQuantizers.Octree;
+ }
+ }
+
using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration))
{
if (useGlobalTable)
@@ -102,19 +127,24 @@ public void Encode(Image image, Stream stream, CancellationToken
}
}
- // Get the number of bits.
- this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
-
// Write the header.
WriteHeader(stream);
// Write the LSD.
- int index = GetTransparentIndex(quantized);
- this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream);
+ transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
+ byte backgroundIndex = unchecked((byte)transparencyIndex);
+ if (transparencyIndex == -1)
+ {
+ backgroundIndex = gifMetadata.BackgroundColorIndex;
+ }
+
+ // Get the number of bits.
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
if (useGlobalTable)
{
- this.WriteColorTable(quantized, stream);
+ this.WriteColorTable(quantized, bitDepth, stream);
}
if (!this.skipMetadata)
@@ -127,41 +157,68 @@ public void Encode(Image image, Stream stream, CancellationToken
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
- this.EncodeFrames(stream, image, quantized, quantized.Palette.ToArray());
+ this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex);
+
+ // 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);
stream.WriteByte(GifConstants.EndIntroducer);
}
- private void EncodeFrames(
+ private void EncodeAdditionalFrames(
Stream stream,
Image image,
- IndexedImageFrame quantized,
- ReadOnlyMemory palette)
+ ReadOnlyMemory globalPalette)
where TPixel : unmanaged, IPixel
{
+ if (image.Frames.Count == 1)
+ {
+ return;
+ }
+
PaletteQuantizer paletteQuantizer = default;
bool hasPaletteQuantizer = false;
- for (int i = 0; i < image.Frames.Count; i++)
+
+ // Store the first frame as a reference for de-duplication comparison.
+ ImageFrame previousFrame = image.Frames.RootFrame;
+
+ // This frame is reused to store de-duplicated pixel buffers.
+ // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
+ // frames using both local and global palettes.
+ using ImageFrame encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size());
+
+ for (int i = 1; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
- ImageFrame frame = image.Frames[i];
- ImageFrameMetadata metadata = frame.Metadata;
- bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local);
+ ImageFrame currentFrame = image.Frames[i];
+ ImageFrameMetadata metadata = currentFrame.Metadata;
+ metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
- // The palette quantizer can reuse the same 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.
+ // 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;
+ paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
- paletteQuantizer = new(this.configuration, this.quantizer.Options, palette);
}
- this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer);
+ this.EncodeAdditionalFrame(
+ stream,
+ previousFrame,
+ currentFrame,
+ encodingFrame,
+ useLocal,
+ gifMetadata,
+ paletteQuantizer);
- // Clean up for the next run.
- quantized.Dispose();
+ previousFrame = currentFrame;
}
if (hasPaletteQuantizer)
@@ -170,88 +227,419 @@ private void EncodeFrames(
}
}
- private void EncodeFrame(
+ private void EncodeFirstFrame(
Stream stream,
- ImageFrame frame,
- int frameIndex,
+ GifFrameMetadata? metadata,
+ IndexedImageFrame quantized,
+ int transparencyIndex)
+ where TPixel : unmanaged, IPixel
+ {
+ this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+
+ Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
+ Rectangle interest = indices.FullRectangle();
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local);
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+
+ this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
+
+ if (useLocal)
+ {
+ this.WriteColorTable(quantized, bitDepth, stream);
+ }
+
+ this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ }
+
+ private void EncodeAdditionalFrame(
+ Stream stream,
+ ImageFrame previousFrame,
+ ImageFrame currentFrame,
+ ImageFrame encodingFrame,
bool useLocal,
GifFrameMetadata? metadata,
- ref IndexedImageFrame quantized,
- ref PaletteQuantizer paletteQuantizer)
+ PaletteQuantizer globalPaletteQuantizer)
where TPixel : unmanaged, IPixel
{
- // The first frame has already been quantized so we do not need to do so again.
- if (frameIndex > 0)
+ // 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)
{
- // Reassign using the current frame and details.
- QuantizerOptions? options = null;
- int colorTableLength = metadata?.ColorTableLength ?? 0;
- if (colorTableLength > 0)
+ if (metadata?.LocalColorTable?.Length > 0)
{
- options = new()
+ ReadOnlySpan palette = metadata.LocalColorTable.Value.Span;
+ if (transparencyIndex < palette.Length)
{
- Dither = this.quantizer.Options.Dither,
- DitherScale = this.quantizer.Options.DitherScale,
- MaxColors = colorTableLength
- };
+ replacement = palette[transparencyIndex].ToScaledVector4();
+ }
+ }
+ }
+ else
+ {
+ ReadOnlySpan palette = globalPaletteQuantizer.Palette.Span;
+ if (transparencyIndex < palette.Length)
+ {
+ replacement = palette[transparencyIndex].ToScaledVector4();
}
+ }
+ }
+
+ this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
- using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, options ?? this.quantizer.Options);
- quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
+ 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
{
- // Quantize the image using the global palette.
- quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
+ // 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());
+ }
+
+ // Recalculate the transparency index as depending on the quantizer used could have a new value.
+ transparencyIndex = GetTransparentIndex(quantized, metadata);
- this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ // Trim down the buffer to the minimum size required.
+ Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
+ Rectangle interest = TrimTransparentPixels(indices, transparencyIndex);
+
+ this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
+
+ if (useLocal)
+ {
+ this.WriteColorTable(quantized, bitDepth, stream);
}
- // Do we have extension information to write?
- int index = GetTransparentIndex(quantized);
- if (metadata != null || index > -1)
+ this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ }
+
+ private void DeDuplicatePixels(
+ ImageFrame backgroundFrame,
+ ImageFrame sourceFrame,
+ ImageFrame resultFrame,
+ Vector4 replacement)
+ 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++)
{
- this.WriteGraphicalControlExtension(metadata ?? new(), index, stream);
+ 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);
}
+ }
- this.WriteImageDescriptor(frame, useLocal, stream);
+ private static Rectangle TrimTransparentPixels(Buffer2D buffer, int transparencyIndex)
+ {
+ if (transparencyIndex < 0)
+ {
+ return buffer.FullRectangle();
+ }
- if (useLocal)
+ 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++)
+ {
+ 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)
+ {
+ Vector256 trimmableVec256 = Vector256.Create(trimmableIndex);
+
+ if (Avx2.IsSupported && rowLength >= Vector256.Count)
+ {
+ 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);
+ }
+
+ 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;
+ }
+ }
+#endif
+ for (; x < rowLength; ++x)
+ {
+ if (Unsafe.Add(ref rowPtr, x) != trimmableIndex)
+ {
+ isTransparentRow = false;
+ left = Math.Min(left, (int)x);
+ right = Math.Max(right, (int)x);
+ }
+ }
+
+ if (!isTransparentRow)
+ {
+ if (y == 0)
+ {
+ // First row is opaque.
+ // Capture to prevent over assignment when a match is found below.
+ top = 0;
+ }
+
+ // 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++;
+ }
+
+ minY = top;
+ bottom = y;
+ }
+ else
+ {
+ // We've yet to hit an opaque row. Capture the top position.
+ if (minY < 0)
+ {
+ top = Math.Max(top, y);
+ }
+
+ bottom = Math.Min(bottom, y);
+ }
+ }
+
+ if (left == int.MaxValue)
+ {
+ left = 0;
+ }
+
+ if (right == int.MinValue)
{
- this.WriteColorTable(quantized, stream);
+ right = buffer.Width;
}
- this.WriteImageData(quantized, stream);
+ if (top == bottom || left == right)
+ {
+ // The entire image is transparent.
+ return buffer.FullRectangle();
+ }
+
+ if (!isTransparentRow)
+ {
+ // Last row is opaque.
+ bottom = buffer.Height;
+ }
+
+ return Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height));
}
///
/// Returns the index of the most transparent color in the palette.
///
- /// The quantized frame.
+ /// The current quantized frame.
+ /// The current gif frame metadata.
/// The pixel format.
///
/// The .
///
- private static int GetTransparentIndex(IndexedImageFrame quantized)
+ private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel
{
- // Transparent pixels are much more likely to be found at the end of a palette.
- int index = -1;
- ReadOnlySpan paletteSpan = quantized.Palette.Span;
-
- using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length);
- Span rgbaSpan = rgbaOwner.GetSpan();
- PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
- ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
+ if (metadata?.HasTransparency == true)
+ {
+ return metadata.TransparencyIndex;
+ }
- for (int i = rgbaSpan.Length - 1; i >= 0; i--)
+ int index = -1;
+ if (quantized != null)
{
- if (Unsafe.Add(ref rgbaSpanRef, (uint)i).Equals(default))
+ TPixel transparentPixel = default;
+ transparentPixel.FromScaledVector4(Vector4.Zero);
+ ReadOnlySpan palette = quantized.Palette.Span;
+
+ // Transparent pixels are much more likely to be found at the end of a palette.
+ for (int i = palette.Length - 1; i >= 0; i--)
{
- index = i;
+ if (palette[i].Equals(transparentPixel))
+ {
+ index = i;
+ }
}
}
@@ -271,18 +659,20 @@ private static int GetTransparentIndex(IndexedImageFrame quantiz
/// The image metadata.
/// The image width.
/// The image height.
- /// The transparency index to set the default background index to.
+ /// The index to set the default background index to.
/// Whether to use a global or local color table.
+ /// The bit depth of the color palette.
/// The stream to write to.
private void WriteLogicalScreenDescriptor(
ImageMetadata metadata,
int width,
int height,
- int transparencyIndex,
+ byte backgroundIndex,
bool useGlobalTable,
+ int bitDepth,
Stream stream)
{
- byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
+ byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, bitDepth - 1, false, bitDepth - 1);
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@@ -316,7 +706,7 @@ private void WriteLogicalScreenDescriptor(
width: (ushort)width,
height: (ushort)height,
packed: packedValue,
- backgroundColorIndex: unchecked((byte)transparencyIndex),
+ backgroundColorIndex: backgroundIndex,
ratio);
Span buffer = stackalloc byte[20];
@@ -412,16 +802,28 @@ private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan comme
/// 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, int transparencyIndex, Stream stream)
{
+ GifFrameMetadata? data = metadata;
+ bool hasTransparency;
+ if (metadata is null)
+ {
+ data = new();
+ hasTransparency = transparencyIndex >= 0;
+ }
+ else
+ {
+ hasTransparency = metadata.HasTransparency;
+ }
+
byte packedValue = GifGraphicControlExtension.GetPackedValue(
- disposalMethod: metadata.DisposalMethod,
- transparencyFlag: transparencyIndex > -1);
+ disposalMethod: data!.DisposalMethod,
+ transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
- delayTime: (ushort)metadata.FrameDelay,
- transparencyIndex: unchecked((byte)transparencyIndex));
+ delayTime: (ushort)data.FrameDelay,
+ transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream);
}
@@ -443,7 +845,7 @@ private void WriteExtension(TGifExtension extension, Stream strea
}
IMemoryOwner? owner = null;
- Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
+ Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation
if (extensionSize > 128)
{
owner = this.memoryAllocator.Allocate(extensionSize + 3);
@@ -466,26 +868,25 @@ private void WriteExtension(TGifExtension extension, Stream strea
}
///
- /// Writes the image descriptor to the stream.
+ /// Writes the image frame descriptor to the stream.
///
- /// The pixel format.
- /// The to be encoded.
+ /// The frame location and size.
/// Whether to use the global color table.
+ /// The bit depth of the color palette.
/// The stream to write to.
- private void WriteImageDescriptor(ImageFrame image, bool hasColorTable, Stream stream)
- where TPixel : unmanaged, IPixel
+ private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream)
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
- localColorTableSize: this.bitDepth - 1);
+ localColorTableSize: bitDepth - 1);
GifImageDescriptor descriptor = new(
- left: 0,
- top: 0,
- width: (ushort)image.Width,
- height: (ushort)image.Height,
+ left: (ushort)rectangle.X,
+ top: (ushort)rectangle.Y,
+ width: (ushort)rectangle.Width,
+ height: (ushort)rectangle.Height,
packed: packedValue);
Span buffer = stackalloc byte[20];
@@ -499,12 +900,13 @@ private void WriteImageDescriptor(ImageFrame image, bool hasColo
///
/// The pixel format.
/// The to encode.
+ /// The bit depth of the color palette.
/// The stream to write to.
- private void WriteColorTable(IndexedImageFrame image, Stream stream)
+ private void WriteColorTable(IndexedImageFrame image, int bitDepth, Stream stream)
where TPixel : unmanaged, IPixel
{
// The maximum number of colors for the bit depth
- int colorTableLength = ColorNumerics.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf();
+ int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf();
using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength, AllocationOptions.Clean);
Span colorTableSpan = colorTable.GetSpan();
@@ -521,13 +923,23 @@ private void WriteColorTable(IndexedImageFrame image, Stream str
///
/// Writes the image pixel data to the stream.
///
- /// The pixel format.
- /// The containing indexed pixels.
+ /// The containing indexed pixels.
+ /// The region of interest.
/// The stream to write to.
- private void WriteImageData(IndexedImageFrame image, Stream stream)
- where TPixel : unmanaged, IPixel
+ /// 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)
{
- using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
- encoder.Encode(((IPixelSource)image).PixelBuffer, stream);
+ 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.
+ int padding = transparencyIndex >= paletteLength
+ ? 1
+ : 0;
+
+ using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding));
+ encoder.Encode(region, stream);
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
index 7f4b49f0bb..faabf7dfa8 100644
--- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats.Gif;
///
@@ -22,9 +24,16 @@ public GifFrameMetadata()
private GifFrameMetadata(GifFrameMetadata other)
{
this.ColorTableMode = other.ColorTableMode;
- this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
+
+ if (other.LocalColorTable?.Length > 0)
+ {
+ this.LocalColorTable = other.LocalColorTable.Value.ToArray();
+ }
+
+ this.HasTransparency = other.HasTransparency;
+ this.TransparencyIndex = other.TransparencyIndex;
}
///
@@ -33,11 +42,22 @@ private GifFrameMetadata(GifFrameMetadata other)
public GifColorTableMode ColorTableMode { get; set; }
///
- /// Gets or sets the length of the color table.
- /// If not 0, then this field indicates the maximum number of colors to use when quantizing the
- /// image frame.
+ /// Gets or sets the local color table, if any.
+ /// The underlying pixel format is represented by .
+ ///
+ public ReadOnlyMemory? LocalColorTable { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the frame has transparency
+ ///
+ public bool HasTransparency { get; set; }
+
+ ///
+ /// Gets or sets the transparency index.
+ /// When is set to this value indicates the index within
+ /// the color palette at which the transparent color is located.
///
- public int ColorTableLength { get; set; }
+ public byte TransparencyIndex { get; set; }
///
/// Gets or sets the frame delay for animated images.
diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs
index da21e134ec..d25e2a5cc2 100644
--- a/src/ImageSharp/Formats/Gif/GifMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
namespace SixLabors.ImageSharp.Formats.Gif;
///
@@ -23,7 +25,12 @@ private GifMetadata(GifMetadata other)
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
- this.GlobalColorTableLength = other.GlobalColorTableLength;
+ this.BackgroundColorIndex = other.BackgroundColorIndex;
+
+ if (other.GlobalColorTable?.Length > 0)
+ {
+ this.GlobalColorTable = other.GlobalColorTable.Value.ToArray();
+ }
for (int i = 0; i < other.Comments.Count; i++)
{
@@ -45,9 +52,16 @@ private GifMetadata(GifMetadata other)
public GifColorTableMode ColorTableMode { get; set; }
///
- /// Gets or sets the length of the global color table if present.
+ /// Gets or sets the global color table, if any.
+ /// The underlying pixel format is represented by .
+ ///
+ public ReadOnlyMemory? GlobalColorTable { get; set; }
+
+ ///
+ /// Gets or sets the index at the for the background color.
+ /// The background color is the color used for those pixels on the screen that are not covered by an image.
///
- public int GlobalColorTableLength { get; set; }
+ public byte BackgroundColorIndex { get; set; }
///
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any
diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index 5253c0978a..4b40c44e45 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -186,7 +186,7 @@ public LzwEncoder(MemoryAllocator memoryAllocator, int colorDepth)
///
/// The 2D buffer of indexed pixels.
/// The stream to write to.
- public void Encode(Buffer2D indexedPixels, Stream stream)
+ public void Encode(Buffer2DRegion indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@@ -249,7 +249,7 @@ private void ClearBlock(Stream stream)
/// The 2D buffer of indexed pixels.
/// The initial bits.
/// The stream to write to.
- private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream)
+ private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;
diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
index e20b9dd177..9ba95952e7 100644
--- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
@@ -17,14 +17,16 @@ public static partial class MetadataExtensions
///
/// The metadata this method extends.
/// The .
- public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
+ public static GifMetadata GetGifMetadata(this ImageMetadata source)
+ => source.GetFormatMetadata(GifFormat.Instance);
///
/// Gets the gif format specific metadata for the image frame.
///
/// The metadata this method extends.
/// The .
- public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance);
+ public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
+ => source.GetFormatMetadata(GifFormat.Instance);
///
/// Gets the gif format specific metadata for the image frame.
@@ -38,5 +40,6 @@ public static partial class MetadataExtensions
///
/// if the gif frame metadata exists; otherwise, .
///
- public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
+ public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
}
diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
index f2226974c9..b8324a0809 100644
--- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
@@ -35,9 +35,9 @@ public static void Decode(Span scanline, Span previousScanline, int
// row: a d
// The Paeth function predicts d to be whichever of a, b, or c is nearest to
// p = a + b - c.
- if (Sse41.IsSupported && bytesPerPixel is 4)
+ if (Sse2.IsSupported && bytesPerPixel is 4)
{
- DecodeSse41(scanline, previousScanline);
+ DecodeSse3(scanline, previousScanline);
}
else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{
@@ -50,7 +50,7 @@ public static void Decode(Span scanline, Span previousScanline, int
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void DecodeSse41(Span scanline, Span previousScanline)
+ private static void DecodeSse3(Span scanline, Span previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -90,8 +90,8 @@ private static void DecodeSse41(Span scanline, Span previousScanline
Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
- Vector128 mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
- Vector128 nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
+ Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
+ Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
// Note `_epi8`: we need addition to wrap modulo 255.
d = Sse2.Add(d, nearest);
@@ -143,8 +143,8 @@ public static void DecodeArm(Span scanline, Span previousScanline)
Vector128 smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
- Vector128 mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
- Vector128 nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
+ Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
+ Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest);
@@ -157,27 +157,6 @@ public static void DecodeArm(Span scanline, Span previousScanline)
}
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector128 BlendVariable(Vector128 a, Vector128 b, Vector128 c)
- {
- // Equivalent of Sse41.BlendVariable:
- // Blend packed 8-bit integers from a and b using mask, and store the results in
- // dst.
- //
- // FOR j := 0 to 15
- // i := j*8
- // IF mask[i+7]
- // dst[i+7:i] := b[i+7:i]
- // ELSE
- // dst[i+7:i] := a[i+7:i]
- // FI
- // ENDFOR
- //
- // Use a signed shift right to create a mask with the sign bit.
- Vector128 mask = AdvSimd.ShiftRightArithmetic(c.AsInt16(), 7);
- return AdvSimd.BitwiseSelect(mask, b.AsInt16(), a.AsInt16()).AsByte();
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span scanline, Span previousScanline, uint bytesPerPixel)
{
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index 1d068303bc..595601522e 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -11,15 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
///
public class PngEncoder : QuantizingImageEncoder
{
- ///
- /// Initializes a new instance of the class.
- ///
- public PngEncoder() =>
-
- // We set the quantizer to null here to allow the underlying encoder to create a
- // quantizer with options appropriate to the encoding bit depth.
- this.Quantizer = null;
-
///
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all values.
diff --git a/src/ImageSharp/Formats/QuantizingImageEncoder.cs b/src/ImageSharp/Formats/QuantizingImageEncoder.cs
index b7eb86afb0..330d8988c7 100644
--- a/src/ImageSharp/Formats/QuantizingImageEncoder.cs
+++ b/src/ImageSharp/Formats/QuantizingImageEncoder.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
@@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder
///
/// Gets the quantizer used to generate the color palette.
///
- public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree;
+ public IQuantizer? Quantizer { get; init; }
///
/// Gets the used for quantization when building color palettes.
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
index 24cca41dc2..fb5b9f2ed7 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs
@@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
+using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Tiff;
@@ -12,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff;
///
public class TiffEncoder : QuantizingImageEncoder
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
+
///
/// Gets the number of bits per pixel.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index b7338ac20a..d0634cf259 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -11,6 +11,7 @@
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff;
@@ -85,7 +86,7 @@ public TiffEncoderCore(TiffEncoder options, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
- this.quantizer = options.Quantizer;
+ this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
@@ -325,7 +326,7 @@ private long WriteIfd(TiffStreamWriter writer, List entries)
{
int sz = ExifWriter.WriteValue(entry, buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
- writer.WritePadded(buffer.Slice(0, sz));
+ writer.WritePadded(buffer[..sz]);
}
else
{
diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs
index 3734402d30..0e7eef11e9 100644
--- a/src/ImageSharp/ImageFrame{TPixel}.cs
+++ b/src/ImageSharp/ImageFrame{TPixel}.cs
@@ -21,6 +21,16 @@ public sealed class ImageFrame : ImageFrame, IPixelSource
{
private bool isDisposed;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration which allows altering default behaviour or extending the library.
+ /// The of the frame.
+ internal ImageFrame(Configuration configuration, Size size)
+ : this(configuration, size.Width, size.Height, new ImageFrameMetadata())
+ {
+ }
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs
index 1173e02e17..f4b2dfc08c 100644
--- a/src/ImageSharp/Memory/Buffer2D{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2D{T}.cs
@@ -173,13 +173,15 @@ internal Memory GetSafeRowMemory(int y)
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
/// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
///
+ /// The destination buffer.
+ /// The source buffer.
+ /// Attempt to copy/swap incompatible buffers.
internal static bool SwapOrCopyContent(Buffer2D destination, Buffer2D source)
{
bool swapped = false;
if (MemoryGroup.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{
- (destination.FastMemoryGroup, source.FastMemoryGroup) =
- (source.FastMemoryGroup, destination.FastMemoryGroup);
+ (destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true;
@@ -201,7 +203,6 @@ internal static bool SwapOrCopyContent(Buffer2D destination, Buffer2D sour
}
[MethodImpl(InliningOptions.ColdPath)]
- private void ThrowYOutOfRangeException(int y) =>
- throw new ArgumentOutOfRangeException(
- $"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
+ private void ThrowYOutOfRangeException(int y)
+ => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
index 0c6ba7ddc9..f75664903d 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
@@ -2,6 +2,7 @@
// 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;
@@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
///
/// The pixel format.
///
-/// This class is not threadsafe and should not be accessed in parallel.
+/// This class is not thread safe and should not be accessed in parallel.
/// Doing so will result in non-idempotent results.
///
internal sealed class EuclideanPixelMap : IDisposable
where TPixel : unmanaged, IPixel
{
private Rgba32[] rgbaPalette;
+ private int transparentIndex;
///
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@@ -34,26 +36,33 @@ internal sealed class EuclideanPixelMap : IDisposable
/// The configuration.
/// The color palette to map from.
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette)
+ : this(configuration, palette, -1)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ /// The color palette to map from.
+ /// An explicit index at which to match transparent pixels.
+ public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette, int transparentIndex = -1)
{
this.configuration = configuration;
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
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;
}
///
/// Gets the color palette of this .
/// The palette memory is owned by the palette source that created it.
///
- public ReadOnlyMemory Palette
- {
- [MethodImpl(InliningOptions.ShortMethod)]
- get;
-
- [MethodImpl(InliningOptions.ShortMethod)]
- private set;
- }
+ public ReadOnlyMemory Palette { get; private set; }
///
/// Returns the closest color in the palette and the index of that pixel.
@@ -91,16 +100,33 @@ public void Clear(ReadOnlyMemory palette)
this.cache.Clear();
}
+ ///
+ /// Allows setting the transparent index after construction. If the provided transparentIndex is outside of the palette, silently ignore it.
+ ///
+ /// An explicit index at which to match transparent pixels.
+ public void SetTransparentIndex(int index) => this.transparentIndex = index < this.Palette.Length ? index : -1;
+
[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);
+ return index;
+ }
+
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Rgba32 candidate = this.rgbaPalette[i];
- int distance = DistanceSquared(rgba, candidate);
+ float distance = DistanceSquared(rgba, candidate);
// If it's an exact match, exit the loop
if (distance == 0)
@@ -130,12 +156,12 @@ private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel m
/// The second point.
/// The distance squared.
[MethodImpl(InliningOptions.ShortMethod)]
- private static int DistanceSquared(Rgba32 a, Rgba32 b)
+ private static float DistanceSquared(Rgba32 a, Rgba32 b)
{
- int deltaR = a.R - b.R;
- int deltaG = a.G - b.G;
- int deltaB = a.B - b.B;
- int deltaA = a.A - b.A;
+ float deltaR = a.R - b.R;
+ float deltaG = a.G - b.G;
+ float deltaB = a.B - b.B;
+ float deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
index fe4af9005a..acd179ffcc 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
public class PaletteQuantizer : IQuantizer
{
private readonly ReadOnlyMemory colorPalette;
+ private readonly int transparentIndex;
///
/// Initializes a new instance of the class.
@@ -27,12 +28,24 @@ public PaletteQuantizer(ReadOnlyMemory palette)
/// The color palette.
/// The quantizer options defining quantization rules.
public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options)
+ : this(palette, options, -1)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The color palette.
+ /// The quantizer options defining quantization rules.
+ /// An explicit index at which to match transparent pixels.
+ internal PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options, int transparentIndex)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.colorPalette = palette;
this.Options = options;
+ this.transparentIndex = transparentIndex;
}
///
@@ -52,6 +65,6 @@ public IQuantizer CreatePixelSpecificQuantizer(Configuration con
// Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
- return new PaletteQuantizer(configuration, options, palette);
+ return new PaletteQuantizer(configuration, options, palette, this.transparentIndex);
}
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
index 86db9f6f01..3df80ea9b7 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
@@ -25,18 +25,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
///
/// Initializes a new instance of the struct.
///
- /// The configuration which allows altering default behaviour or extending the library.
+ /// The configuration which allows altering default behavior or extending the library.
/// The quantizer options defining quantization rules.
/// The palette to use.
+ /// An explicit index at which to match transparent pixels.
[MethodImpl(InliningOptions.ShortMethod)]
- public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory palette)
+ public PaletteQuantizer(
+ Configuration configuration,
+ QuantizerOptions options,
+ ReadOnlyMemory palette,
+ int transparentIndex)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
- this.pixelMap = new EuclideanPixelMap(configuration, palette);
+ this.pixelMap = new EuclideanPixelMap(configuration, palette, transparentIndex);
}
///
@@ -59,6 +64,12 @@ public void AddPaletteColors(Buffer2DRegion pixelRegion)
{
}
+ ///
+ /// Allows setting the transparent index after construction.
+ ///
+ /// An explicit index at which to match transparent pixels.
+ public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index);
+
///
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
index b3d03d9338..a6bb265a81 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
@@ -25,8 +25,8 @@ public class QuantizerOptions
///
public float DitherScale
{
- get { return this.ditherScale; }
- set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); }
+ get => this.ditherScale;
+ set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
}
///
@@ -35,7 +35,7 @@ public float DitherScale
///
public int MaxColors
{
- get { return this.maxColors; }
- set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); }
+ get => this.maxColors;
+ set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index a0d91c2088..42cbd90f3b 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -46,17 +46,20 @@ public class BmpEncoderTests
{ Bit32Rgb, BmpBitsPerPixel.Pixel32 }
};
+ [Fact]
+ public void BmpEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(BmpEncoder.Quantizer);
+
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
- var testFile = TestFile.Create(imagePath);
+ TestFile testFile = TestFile.Create(imagePath);
using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder);
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
@@ -67,13 +70,13 @@ public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolut
[MemberData(nameof(BmpBitsPerPixelFiles))]
public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel)
{
- var testFile = TestFile.Create(imagePath);
+ TestFile testFile = TestFile.Create(imagePath);
using Image input = testFile.CreateRgba32Image();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
input.Save(memStream, BmpEncoder);
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
BmpMetadata meta = output.Metadata.GetBmpMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
@@ -196,8 +199,8 @@ public void Encode_2Bit_WithV3Header_Works(
where TPixel : unmanaged, IPixel
{
// arrange
- var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
- using var memoryStream = new MemoryStream();
+ BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
+ using MemoryStream memoryStream = new();
using Image input = provider.GetImage(BmpDecoder.Instance);
// act
@@ -205,7 +208,7 @@ public void Encode_2Bit_WithV3Header_Works(
memoryStream.Position = 0;
// assert
- using var actual = Image.Load(memoryStream);
+ using Image actual = Image.Load(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
}
@@ -218,8 +221,8 @@ public void Encode_2Bit_WithV4Header_Works(
where TPixel : unmanaged, IPixel
{
// arrange
- var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
- using var memoryStream = new MemoryStream();
+ BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel };
+ using MemoryStream memoryStream = new();
using Image input = provider.GetImage(BmpDecoder.Instance);
// act
@@ -227,7 +230,7 @@ public void Encode_2Bit_WithV4Header_Works(
memoryStream.Position = 0;
// assert
- using var actual = Image.Load(memoryStream);
+ using Image actual = Image.Load(memoryStream);
ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual);
Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image");
}
@@ -266,7 +269,7 @@ public void Encode_8BitColor_WithWuQuantizer(TestImageProvider p
}
using Image image = provider.GetImage();
- var encoder = new BmpEncoder
+ BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new WuQuantizer()
@@ -298,7 +301,7 @@ public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider image = provider.GetImage();
- var encoder = new BmpEncoder
+ BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new OctreeQuantizer()
@@ -333,11 +336,11 @@ public void Encode_PreservesColorProfile(TestImageProvider provi
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
input.Save(memStream, new BmpEncoder());
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
byte[] actualProfileBytes = actualProfile.ToByteArray();
@@ -353,7 +356,7 @@ public void Encode_WorksWithSizeGreaterThen65k(int width, int height)
Exception exception = Record.Exception(() =>
{
using Image image = new Image(width, height);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
image.Save(memStream, BmpEncoder);
});
@@ -411,7 +414,7 @@ private static void TestBmpEncoderCore(
image.Mutate(c => c.MakeOpaque());
}
- var encoder = new BmpEncoder
+ BmpEncoder encoder = new()
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency,
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index 376bb4a06f..8b23927418 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -34,6 +34,20 @@ public void Decode_VerifyAllFrames(TestImageProvider provider)
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.Issue2450_A, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Gif.Issues.Issue2450_B, PixelTypes.Rgba32)]
+ public void Decode_Issue2450(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // Images have many frames, only compare a selection of them.
+ static bool Predicate(int i, int _) => i % 8 == 0;
+
+ using Image image = provider.GetImage();
+ image.DebugSaveMultiFrame(provider, predicate: Predicate);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate);
+ }
+
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void GifDecoder_Decode_Resize(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
index 7fc61066a7..31001e31b4 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
@@ -33,9 +33,12 @@ public GifEncoderTests()
}
}
+ [Fact]
+ public void GifEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(new GifEncoder().Quantizer);
+
[Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]
- [WithTestPatternImages(100, 100, TestPixelTypes, false)]
+ [WithTestPatternImages(100, 100, TestPixelTypes, true)]
public void EncodeGeneratedPatterns(TestImageProvider provider, bool limitAllocationBuffer)
where TPixel : unmanaged, IPixel
{
@@ -171,10 +174,21 @@ public void NonMutatingEncodePreservesPaletteCount()
GifMetadata metaData = image.Metadata.GetGifMetadata();
GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
GifColorTableMode colorMode = metaData.ColorTableMode;
+
+ int maxColors;
+ if (colorMode == GifColorTableMode.Global)
+ {
+ maxColors = metaData.GlobalColorTable.Value.Length;
+ }
+ else
+ {
+ maxColors = frameMetadata.LocalColorTable.Value.Length;
+ }
+
GifEncoder encoder = new()
{
ColorTableMode = colorMode,
- Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
+ Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
};
image.Save(outStream, encoder);
@@ -187,15 +201,31 @@ public void NonMutatingEncodePreservesPaletteCount()
Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
- Assert.Equal(64, frameMetadata.ColorTableLength);
+ colorMode = cloneMetadata.ColorTableMode;
+ if (colorMode == GifColorTableMode.Global)
+ {
+ maxColors = metaData.GlobalColorTable.Value.Length;
+ }
+ else
+ {
+ maxColors = frameMetadata.LocalColorTable.Value.Length;
+ }
+
+ Assert.Equal(64, maxColors);
for (int i = 0; i < image.Frames.Count; i++)
{
- GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata();
- GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata();
+ GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata();
+ GifFrameMetadata cMeta = clone.Frames[i].Metadata.GetGifMetadata();
+
+ if (iMeta.ColorTableMode == GifColorTableMode.Local)
+ {
+ Assert.Equal(iMeta.LocalColorTable.Value.Length, cMeta.LocalColorTable.Value.Length);
+ }
- Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
- Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
+ Assert.Equal(iMeta.FrameDelay, cMeta.FrameDelay);
+ Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency);
+ Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex);
}
image.Dispose();
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
index 9a8b41d541..774638311d 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs
@@ -11,21 +11,22 @@ public class GifFrameMetadataTests
[Fact]
public void CloneIsDeep()
{
- var meta = new GifFrameMetadata
+ GifFrameMetadata meta = new()
{
FrameDelay = 1,
DisposalMethod = GifDisposalMethod.RestoreToBackground,
- ColorTableLength = 2
+ LocalColorTable = new[] { Color.Black, Color.White }
};
- var clone = (GifFrameMetadata)meta.DeepClone();
+ GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone();
clone.FrameDelay = 2;
clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious;
- clone.ColorTableLength = 1;
+ clone.LocalColorTable = new[] { Color.Black };
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod));
- Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength));
+ Assert.False(meta.LocalColorTable.Value.Length == clone.LocalColorTable.Value.Length);
+ Assert.Equal(1, clone.LocalColorTable.Value.Length);
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
index 40ac94eea6..fb4445cdac 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using Microsoft.CodeAnalysis;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
@@ -35,7 +34,7 @@ public void CloneIsDeep()
{
RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global,
- GlobalColorTableLength = 2,
+ GlobalColorTable = new[] { Color.Black, Color.White },
Comments = new List { "Foo" }
};
@@ -43,11 +42,12 @@ public void CloneIsDeep()
clone.RepeatCount = 2;
clone.ColorTableMode = GifColorTableMode.Local;
- clone.GlobalColorTableLength = 1;
+ clone.GlobalColorTable = new[] { Color.Black };
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode));
- Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength));
+ Assert.False(meta.GlobalColorTable.Value.Length == clone.GlobalColorTable.Value.Length);
+ Assert.Equal(1, clone.GlobalColorTable.Value.Length);
Assert.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(clone.Comments));
}
@@ -205,7 +205,12 @@ public void Identify_Frames(
GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata();
Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode);
- Assert.Equal(globalColorTableLength, gifFrameMetadata.ColorTableLength);
+
+ if (colorTableMode == GifColorTableMode.Global)
+ {
+ Assert.Equal(globalColorTableLength, gifMetadata.GlobalColorTable.Value.Length);
+ }
+
Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay);
Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod);
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index b4fda5d32f..b20ec0675a 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -99,6 +99,9 @@ public static readonly TheoryData CompressionLevels
{ TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
+ [Fact]
+ public void PngEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(PngEncoder.Quantizer);
+
[Theory]
[WithFile(TestImages.Png.Palette8Bpp, nameof(PngColorTypes), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(PngColorTypes), 48, 24, PixelTypes.Rgba32)]
@@ -595,7 +598,7 @@ private static void TestPngEncoderCore(
string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
string pngInterlaceModeInfo = interlaceMode != PngInterlaceMode.None ? $"_{interlaceMode}" : string.Empty;
- string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}{pngInterlaceModeInfo}";
+ string debugInfo = pngColorTypeInfo + pngFilterMethodInfo + compressionLevelInfo + paletteSizeInfo + pngBitDepthInfo + pngInterlaceModeInfo;
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index f8aa1551fc..1fafb4cd04 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -11,6 +11,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
[Trait("Format", "Tiff")]
public class TiffEncoderTests : TiffEncoderBaseTester
{
+ [Fact]
+ public void TiffEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(new TiffEncoder().Quantizer);
+
[Theory]
[InlineData(null, TiffBitsPerPixel.Bit24)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)]
@@ -28,18 +31,18 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel)
{
// arrange
- var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
+ TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image(10, 10)
: new Image(10, 10);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
Assert.Equal(TiffCompression.None, frameMetaData.Compression);
@@ -54,16 +57,17 @@ public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInt
public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel)
{
// arrange
- var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel };
+ TiffEncoder tiffEncoder = new()
+ { BitsPerPixel = bitsPerPixel };
using Image input = new Image(10, 10);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel);
@@ -81,16 +85,17 @@ public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel)
public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel)
{
// arrange
- var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel };
+ TiffEncoder tiffEncoder = new()
+ { BitsPerPixel = bitsPerPixel };
using Image input = new Image(10, 10);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@@ -103,16 +108,17 @@ public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPix
public void EncoderOptions_WithInvalidCompressionAndPixelTypeCombination_DefaultsToRgb(TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
{
// arrange
- var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
+ TiffEncoder tiffEncoder = new()
+ { PhotometricInterpretation = photometricInterpretation, Compression = compression };
using Image input = new Image(10, 10);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
@@ -149,18 +155,19 @@ public void EncoderOptions_SetPhotometricInterpretationAndCompression_Works(
TiffCompression expectedCompression)
{
// arrange
- var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
+ TiffEncoder tiffEncoder = new()
+ { PhotometricInterpretation = photometricInterpretation, Compression = compression };
using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
? new Image(10, 10)
: new Image(10, 10);
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata rootFrameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, rootFrameMetaData.Compression);
@@ -178,16 +185,16 @@ public void TiffEncoder_PreservesBitsPerPixel(TestImageProvider
where TPixel : unmanaged, IPixel
{
// arrange
- var tiffEncoder = new TiffEncoder();
+ TiffEncoder tiffEncoder = new();
using Image input = provider.GetImage();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
}
@@ -196,17 +203,17 @@ public void TiffEncoder_PreservesBitsPerPixel(TestImageProvider
public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8()
{
// arrange
- var tiffEncoder = new TiffEncoder();
+ TiffEncoder tiffEncoder = new();
using Image input = new Image(10, 10);
- using var memStream = new MemoryStream();
- TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
+ using MemoryStream memStream = new();
+ const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8;
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel);
}
@@ -220,16 +227,16 @@ public void TiffEncoder_PreservesCompression(TestImageProvider p
where TPixel : unmanaged, IPixel
{
// arrange
- var tiffEncoder = new TiffEncoder();
+ TiffEncoder tiffEncoder = new();
using Image input = provider.GetImage();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
Assert.Equal(expectedCompression, output.Frames.RootFrame.Metadata.GetTiffMetadata().Compression);
}
@@ -242,16 +249,16 @@ public void TiffEncoder_PreservesPredictor(TestImageProvider pro
where TPixel : unmanaged, IPixel
{
// arrange
- var tiffEncoder = new TiffEncoder();
+ TiffEncoder tiffEncoder = new();
using Image input = provider.GetImage();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetadata = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(expectedPredictor, frameMetadata.Predictor);
}
@@ -261,8 +268,8 @@ public void TiffEncoder_PreservesPredictor(TestImageProvider pro
public void TiffEncoder_WritesIfdOffsetAtWordBoundary()
{
// arrange
- var tiffEncoder = new TiffEncoder();
- using var memStream = new MemoryStream();
+ TiffEncoder tiffEncoder = new();
+ using MemoryStream memStream = new();
using Image image = new(1, 1);
byte[] expectedIfdOffsetBytes = { 12, 0 };
@@ -286,16 +293,16 @@ public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestIma
where TPixel : unmanaged, IPixel
{
// arrange
- var encoder = new TiffEncoder() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 };
+ TiffEncoder encoder = new() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 };
using Image input = provider.GetImage();
- using var memStream = new MemoryStream();
+ using MemoryStream memStream = new();
// act
input.Save(memStream, encoder);
// assert
memStream.Position = 0;
- using var output = Image.Load(memStream);
+ using Image output = Image.Load(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel);
Assert.Equal(expectedCompression, frameMetaData.Compression);
@@ -545,7 +552,8 @@ public void TiffEncode_WorksWithDiscontiguousBuffers(TestImageProvider image = provider.GetImage();
- var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
+ TiffEncoder encoder = new()
+ { PhotometricInterpretation = photometricInterpretation };
image.DebugSave(provider, encoder);
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
index bc22806c3c..9cfd393906 100644
--- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
+++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
@@ -279,6 +279,7 @@ public void ConstructGif_FromDifferentPixelTypes(TestImageProvider dest = new(source.GetConfiguration(), source.Width, source.Height);
+
// Giphy.gif has 5 frames
ImportFrameAs(source.Frames, dest.Frames, 0);
ImportFrameAs(source.Frames, dest.Frames, 1);
@@ -289,7 +290,7 @@ public void ConstructGif_FromDifferentPixelTypes(TestImageProvider());
+ IccProfile iccProfile = new()
{
Header = new IccProfileHeader()
{
CmmType = "Unittest"
}
};
- var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile();
- var metaData = new ImageFrameMetadata()
+ IptcProfile iptcProfile = new();
+ ImageFrameMetadata metaData = new()
{
XmpProfile = xmpProfile,
ExifProfile = exifProfile,
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index afde66ffaf..180a8594c0 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -492,6 +492,9 @@ public static class Issues
public const string Issue2288_B = "Gif/issues/issue_2288_2.gif";
public const string Issue2288_C = "Gif/issues/issue_2288_3.gif";
public const string Issue2288_D = "Gif/issues/issue_2288_4.gif";
+ public const string Issue2450_A = "Gif/issues/issue_2450.gif";
+ public const string Issue2450_B = "Gif/issues/issue_2450_2.gif";
+ public const string Issue2198 = "Gif/issues/issue_2198.gif";
}
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
index 29f9d1626d..7153674e6b 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
@@ -46,19 +46,38 @@ public static ImageSimilarityReport CompareImagesOrFrames> CompareImages(
this ImageComparer comparer,
Image expected,
- Image actual)
+ Image actual,
+ Func predicate = null)
where TPixelA : unmanaged, IPixel
where TPixelB : unmanaged, IPixel
{
- var result = new List>();
+ List> result = new();
- if (expected.Frames.Count != actual.Frames.Count)
+ int expectedFrameCount = actual.Frames.Count;
+ if (predicate != null)
+ {
+ expectedFrameCount = 0;
+ for (int i = 0; i < actual.Frames.Count; i++)
+ {
+ if (predicate(i, actual.Frames.Count))
+ {
+ expectedFrameCount++;
+ }
+ }
+ }
+
+ if (expected.Frames.Count != expectedFrameCount)
{
- throw new Exception("Frame count does not match!");
+ throw new ImagesSimilarityException("Frame count does not match!");
}
for (int i = 0; i < expected.Frames.Count; i++)
{
+ if (predicate != null && !predicate(i, expected.Frames.Count))
+ {
+ continue;
+ }
+
ImageSimilarityReport report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]);
if (!report.IsEmpty)
{
@@ -72,7 +91,8 @@ public static IEnumerable> CompareImages
public static void VerifySimilarity(
this ImageComparer comparer,
Image expected,
- Image actual)
+ Image actual,
+ Func predicate = null)
where TPixelA : unmanaged, IPixel
where TPixelB : unmanaged, IPixel
{
@@ -81,12 +101,25 @@ public static void VerifySimilarity(
throw new ImageDimensionsMismatchException(expected.Size, actual.Size);
}
- if (expected.Frames.Count != actual.Frames.Count)
+ int expectedFrameCount = actual.Frames.Count;
+ if (predicate != null)
+ {
+ expectedFrameCount = 0;
+ for (int i = 0; i < actual.Frames.Count; i++)
+ {
+ if (predicate(i, actual.Frames.Count))
+ {
+ expectedFrameCount++;
+ }
+ }
+ }
+
+ if (expected.Frames.Count != expectedFrameCount)
{
throw new ImagesSimilarityException("Image frame count does not match!");
}
- IEnumerable reports = comparer.CompareImages(expected, actual);
+ IEnumerable reports = comparer.CompareImages(expected, actual, predicate);
if (reports.Any())
{
throw new ImageDifferenceIsOverThresholdException(reports);
diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
index 460ecac85a..3601344ee3 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
@@ -184,7 +184,8 @@ public IEnumerable GetTestOutputFileNamesMultiFrame(
string extension = null,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true,
- bool appendSourceFileOrDescription = true)
+ bool appendSourceFileOrDescription = true,
+ Func predicate = null)
{
string baseDir = this.GetTestOutputFileName(string.Empty, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription);
@@ -195,8 +196,12 @@ public IEnumerable GetTestOutputFileNamesMultiFrame(
for (int i = 0; i < frameCount; i++)
{
- string filePath = $"{baseDir}/{i:D2}.{extension}";
- yield return filePath;
+ if (predicate != null && !predicate(i, frameCount))
+ {
+ continue;
+ }
+
+ yield return $"{baseDir}/{i:D2}.{extension}";
}
}
@@ -205,27 +210,35 @@ public string[] SaveTestOutputFileMultiFrame(
string extension = "png",
IImageEncoder encoder = null,
object testOutputDetails = null,
- bool appendPixelTypeToFileName = true)
+ bool appendPixelTypeToFileName = true,
+ Func predicate = null)
where TPixel : unmanaged, IPixel
{
- encoder = encoder ?? TestEnvironment.GetReferenceEncoder($"foo.{extension}");
+ encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}");
string[] files = this.GetTestOutputFileNamesMultiFrame(
image.Frames.Count,
extension,
testOutputDetails,
- appendPixelTypeToFileName).ToArray();
+ appendPixelTypeToFileName,
+ predicate: predicate).ToArray();
for (int i = 0; i < image.Frames.Count; i++)
{
- using (Image frameImage = image.Frames.CloneFrame(i))
+ if (predicate != null && !predicate(i, image.Frames.Count))
{
- string filePath = files[i];
- using (FileStream stream = File.OpenWrite(filePath))
- {
- frameImage.Save(stream, encoder);
- }
+ continue;
}
+
+ if (i >= files.Length)
+ {
+ break;
+ }
+
+ using Image frameImage = image.Frames.CloneFrame(i);
+ string filePath = files[i];
+ using FileStream stream = File.OpenWrite(filePath);
+ frameImage.Save(stream, encoder);
}
return files;
@@ -236,20 +249,17 @@ internal string GetReferenceOutputFileName(
object testOutputDetails,
bool appendPixelTypeToFileName,
bool appendSourceFileOrDescription)
- {
- return TestEnvironment.GetReferenceOutputFileName(
+ => TestEnvironment.GetReferenceOutputFileName(
this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription));
- }
public string[] GetReferenceOutputFileNamesMultiFrame(
int frameCount,
string extension,
object testOutputDetails,
- bool appendPixelTypeToFileName = true)
- {
- return this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails)
- .Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
- }
+ bool appendPixelTypeToFileName = true,
+ Func predicate = null)
+ => this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate)
+ .Select(TestEnvironment.GetReferenceOutputFileName).ToArray();
internal void Init(string typeName, string methodName, string outputSubfolderName)
{
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
index 31c9f541ea..9951ecfa90 100644
--- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
@@ -67,10 +67,10 @@ public static Image DebugSave(
provider.Utility.SaveTestOutputFile(
image,
extension,
+ encoder: encoder,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName,
- appendSourceFileOrDescription: appendSourceFileOrDescription,
- encoder: encoder);
+ appendSourceFileOrDescription: appendSourceFileOrDescription);
return image;
}
@@ -107,7 +107,8 @@ public static Image DebugSaveMultiFrame(
ITestImageProvider provider,
object testOutputDetails = null,
string extension = "png",
- bool appendPixelTypeToFileName = true)
+ bool appendPixelTypeToFileName = true,
+ Func predicate = null)
where TPixel : unmanaged, IPixel
{
if (TestEnvironment.RunsWithCodeCoverage)
@@ -119,7 +120,8 @@ public static Image DebugSaveMultiFrame(
image,
extension,
testOutputDetails: testOutputDetails,
- appendPixelTypeToFileName: appendPixelTypeToFileName);
+ appendPixelTypeToFileName: appendPixelTypeToFileName,
+ predicate: predicate);
return image;
}
@@ -237,7 +239,6 @@ public static Image CompareFirstFrameToReferenceOutput(
ITestImageProvider provider,
FormattableString testOutputDetails,
string extension = "png",
- bool grayscale = false,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel
@@ -246,7 +247,6 @@ public static Image CompareFirstFrameToReferenceOutput(
provider,
(object)testOutputDetails,
extension,
- grayscale,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
@@ -256,12 +256,11 @@ public static Image CompareFirstFrameToReferenceOutput(
ITestImageProvider provider,
object testOutputDetails = null,
string extension = "png",
- bool grayscale = false,
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel
{
- using (var firstFrameOnlyImage = new Image(image.Width, image.Height))
+ using (Image firstFrameOnlyImage = new(image.Width, image.Height))
using (Image referenceImage = GetReferenceOutputImage(
provider,
testOutputDetails,
@@ -284,8 +283,8 @@ public static Image CompareToReferenceOutputMultiFrame(
ImageComparer comparer,
object testOutputDetails = null,
string extension = "png",
- bool grayscale = false,
- bool appendPixelTypeToFileName = true)
+ bool appendPixelTypeToFileName = true,
+ Func predicate = null)
where TPixel : unmanaged, IPixel
{
using (Image referenceImage = GetReferenceOutputImageMultiFrame(
@@ -293,9 +292,10 @@ public static Image CompareToReferenceOutputMultiFrame(
image.Frames.Count,
testOutputDetails,
extension,
- appendPixelTypeToFileName))
+ appendPixelTypeToFileName,
+ predicate: predicate))
{
- comparer.VerifySimilarity(referenceImage, image);
+ comparer.VerifySimilarity(referenceImage, image, predicate);
}
return image;
@@ -332,16 +332,18 @@ public static Image GetReferenceOutputImageMultiFrame(
int frameCount,
object testOutputDetails = null,
string extension = "png",
- bool appendPixelTypeToFileName = true)
+ bool appendPixelTypeToFileName = true,
+ Func predicate = null)
where TPixel : unmanaged, IPixel
{
string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame(
frameCount,
extension,
testOutputDetails,
- appendPixelTypeToFileName);
+ appendPixelTypeToFileName,
+ predicate);
- var temporaryFrameImages = new List>();
+ List> temporaryFrameImages = new();
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]);
@@ -359,7 +361,7 @@ public static Image GetReferenceOutputImageMultiFrame(
Image firstTemp = temporaryFrameImages[0];
- var result = new Image(firstTemp.Width, firstTemp.Height);
+ Image result = new(firstTemp.Width, firstTemp.Height);
foreach (Image fi in temporaryFrameImages)
{
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png
new file mode 100644
index 0000000000..98e823a955
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb615374f4c680ed4b7e4922e6a0404446c520e254365a1c2406c3dcdad8d02f
+size 2574
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png
new file mode 100644
index 0000000000..c54ed5a7a7
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7ac936ace1ea78c3aa7fb099853e32140278f0ce1b5f27cc1ac68aa9d256d5d6
+size 161248
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png
new file mode 100644
index 0000000000..ca5b28022d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cc1406b0b5c7fd60f539414249007112224388b2cc27785833cf229e1078c81
+size 181703
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png
new file mode 100644
index 0000000000..9e58a83cc4
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0fa21bee072c1e2563770759c6fb95f7dc16e467e9aa9e29c5ab482acdbee170
+size 182851
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png
new file mode 100644
index 0000000000..b37798cd28
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da813a5f5bbbf95f7f5c8464bdab10d1a7cb7b5f60169b64910f650b98056b3a
+size 183582
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png
new file mode 100644
index 0000000000..51949e9b4e
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e12217fb78a91a18b0d2110ce1c38159534647e49e9f8390ae8b33eda1bf1046
+size 183390
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png
new file mode 100644
index 0000000000..69899bf4d5
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f62ad66be6a04c50b47e1a047e54a177bbaf97ff8a3e4a170e114c3dcc2386c7
+size 183231
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png
new file mode 100644
index 0000000000..6bfbf7a89b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f91b0f28197e2dc9e2e010c32ae2c2cc79568c2e9158b40e383e88eb8d299f8
+size 183209
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png
new file mode 100644
index 0000000000..9970be2c2c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b17a8715a14e63e7b68f77a41eb15ce07f11fc4e652b27b1c071fda9182aa4e7
+size 183214
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png
new file mode 100644
index 0000000000..35e46fc69d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed78b0a881154b7867c749f4375a1341611d155aa100821211d76c70cacf70ae
+size 166536
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png
new file mode 100644
index 0000000000..e3da59988d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9c5ac7c97d903588ecd73205e85c732b72a708c35f1e88b3402f01e1a996222
+size 172363
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png
new file mode 100644
index 0000000000..810d6f3c03
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f56c8daa27477f2e20702176f01a1e35f40a250d461fc3d5c3f4ded436b81dd9
+size 173335
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png
new file mode 100644
index 0000000000..f4fcd2204b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:41d36b364522adf170aa87f331ce8e1243ef24f0a0d730d8d62116d451380069
+size 174487
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png
new file mode 100644
index 0000000000..905535d993
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f80e8fc0f32f5eebc24066e2dca4dc193cc253561aa2d34a80055c17c9911741
+size 174931
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png
new file mode 100644
index 0000000000..e029956ab7
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98ab9e6879e35841ed91a3c55d3daf26bed01f4b411cdac100caf21737e197e6
+size 176282
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png
new file mode 100644
index 0000000000..ddaa4de69e
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf1fba5a468f8944dec62b0ccf723a4843b46f0e1718c2b37deca00dc048cb20
+size 179139
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png
new file mode 100644
index 0000000000..ea5b52f549
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dea9bf39eb210bfcfeb573cc50f3a9676b3d1da729b3ae2fc5af72dbc687668f
+size 181197
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png
new file mode 100644
index 0000000000..5408de94d8
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa7aa1f601d12d20059bb51e8d642f72976e25f5e116a3f85b1741f0f557d8e9
+size 179779
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png
new file mode 100644
index 0000000000..c2a3e56e8d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:654fd1df2dec9c8694e60041a1fb8ebb3e213223038742da4b3f89173a3cf0c4
+size 180044
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png
new file mode 100644
index 0000000000..f3626cadfa
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8102e88f544bd06317e52b485a7aaf81bb46ed82e4b617af29b4c9823d46dcfc
+size 181874
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png
new file mode 100644
index 0000000000..1abcd510c4
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2cf3a4141ca32ab8f60060140f00fd79765b2950a542a146d3587596ad6770b
+size 4489
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png
new file mode 100644
index 0000000000..3b96f149a0
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3195912d89a03928926ba56e6a7845d2ea7b0f9d0efc4854d5b36d99541eb01a
+size 4596
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png
new file mode 100644
index 0000000000..cd625df7ed
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb10d95b54c4c2c3b589db0fe420a79f572752e27682666fc20eada3d001e281
+size 4654
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png
new file mode 100644
index 0000000000..7df0937a99
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ff9524242c8ad0fa5e87f32aa3a1365fe8062fee14d594c4f66a4aecf0bde05
+size 4642
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png
new file mode 100644
index 0000000000..244e8377da
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0d6b4b72c5ec38f36679a38d9c0e95f1aaf5a8dbe016174593a05ae6fa28f2b
+size 4317
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png
new file mode 100644
index 0000000000..112b70d4eb
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70be2794b20cec8ea558b9902b04dee6b1790bf5d867c8b8531ad71f238d8b73
+size 4417
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png
new file mode 100644
index 0000000000..9a3f80dc4a
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3abc1beaefbc9c95a9ca828bbd06de8d1bed504b7e1877b66e3f881bbed2dbf4
+size 4716
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png
new file mode 100644
index 0000000000..cf448a4f3b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0d279d361a77bd0d95204853adf7d575a93118688625f6ec2dad3979fadfb456
+size 4697
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png
new file mode 100644
index 0000000000..2130055dfd
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de60756ff2501e88c83e2732c38456b8fc66780bb2302452cdd21f8b7bd82108
+size 4936
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png
new file mode 100644
index 0000000000..79ed470286
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:606235a70e3b167192c0783c6eda9f2f5867cf14d5521a83af3441cfe1adc66f
+size 4917
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png
new file mode 100644
index 0000000000..3b74cb2dd8
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1931befb45c7eedfa44518d62cf2fc8ecfc64e5505c1639d0b6187d988fa06c5
+size 4951
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png
new file mode 100644
index 0000000000..122c566f0a
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:785ed19db48a60886bebe90223e9f48f9d6df45b1e1c7e5ac467f6a9211db1f0
+size 7528
diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png
new file mode 100644
index 0000000000..64159bbe97
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:511d2e3ffec299188a715389b7a17f35bc152e3830a8ecc34ce93c044d1c3962
+size 4897
diff --git a/tests/Images/Input/Gif/issues/issue_2198.gif b/tests/Images/Input/Gif/issues/issue_2198.gif
new file mode 100644
index 0000000000..4f9375a4b3
--- /dev/null
+++ b/tests/Images/Input/Gif/issues/issue_2198.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:48bd8a2992c3aeda920250effb53d4e9aef09c76dc5d0c5fade545ec5ba522a4
+size 1863378
diff --git a/tests/Images/Input/Gif/issues/issue_2450.gif b/tests/Images/Input/Gif/issues/issue_2450.gif
new file mode 100644
index 0000000000..7e85e2dad1
--- /dev/null
+++ b/tests/Images/Input/Gif/issues/issue_2450.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de38adf0b7347862db03ef10f17df231e2985e6f0bfa2eb824d9bbca007ff04e
+size 4107068
diff --git a/tests/Images/Input/Gif/issues/issue_2450_2.gif b/tests/Images/Input/Gif/issues/issue_2450_2.gif
new file mode 100644
index 0000000000..42c95fa329
--- /dev/null
+++ b/tests/Images/Input/Gif/issues/issue_2450_2.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:af7c04d8a5db464be782aba904ad1fc6168d5ab196fef84314b1e2f6d703e923
+size 29995