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