diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs index e87872a70..5c0902f05 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs @@ -629,6 +629,33 @@ internal static partial class SimdUtils 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(in Vector128 left, in Vector128 right, in 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 156e2f961..aecea7ded 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 fd23a29e3..f4fadd44e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers; 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 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals { 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 55ad2c458..7f5276a25 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 @@ internal sealed class GifDecoderCore : IImageDecoderInternals finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); } if (image is null) @@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); } if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) @@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals 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 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { 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 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } finally { - localColorTable?.Dispose(); indices?.Dispose(); } } @@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals prevFrame = previousFrame; } - currentFrame = image!.Frames.CreateFrame(); + // We create a clone of the frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 + currentFrame = image!.Frames.AddFrame(previousFrame); this.SetFrameMetadata(currentFrame.Metadata); @@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals // Skip the color table for this frame if local. if (this.imageDescriptor.LocalColorTableFlag) { - stream.Skip(this.imageDescriptor.LocalColorTableSize * 3); + // 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]); } // Skip the frame indices. Pixels length + mincode size. @@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); gifMeta.ColorTableMode = GifColorTableMode.Global; - gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; } if (this.imageDescriptor.LocalColorTableFlag @@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); gifMeta.ColorTableMode = GifColorTableMode.Local; - gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; + + Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize]; + ref Rgb24 localBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize])); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(Unsafe.Add(ref localBase, (uint)i)); + } + + gifMeta.DecodedLocalColorTable = 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,21 @@ internal sealed class GifDecoderCore : IImageDecoderInternals 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 + // Read the global color table data from the stream and preserve it in the gif metadata stream.Read(this.globalColorTable.GetSpan()); + + Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize]; + ref Rgb24 globalBase = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(this.globalColorTable.GetSpan())); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(Unsafe.Add(ref globalBase, (uint)i)); + } + + this.gifMetadata.DecodedGlobalColorTable = colorTable; } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c01cc78ef..b6955b8c0 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -4,11 +4,15 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +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,7 +40,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// The quantizer used to generate the color palette. /// - private readonly IQuantizer quantizer; + private IQuantizer? quantizer; + + /// + /// Whether the quantizer was supplied via options. + /// + private readonly bool hasQuantizer; /// /// The color table mode: Global or local. @@ -64,6 +73,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals 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; } @@ -88,6 +98,21 @@ internal sealed class GifEncoderCore : IImageEncoderInternals // Quantize the image returning a palette. IndexedImageFrame? quantized; + + 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.DecodedGlobalColorTable.Length > 0) + { + // We avoid dithering by default to preserve the original colors. + this.quantizer = new PaletteQuantizer(gifMetadata.DecodedGlobalColorTable, new() { Dither = null }); + } + else + { + this.quantizer = KnownQuantizers.Octree; + } + } + using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration)) { if (useGlobalTable) @@ -109,7 +134,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals WriteHeader(stream); // Write the LSD. - int index = GetTransparentIndex(quantized); + image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); + int index = GetTransparentIndex(quantized, frameMetadata); + if (index == -1) + { + index = gifMetadata.BackgroundColor; + } + this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream); if (useGlobalTable) @@ -141,6 +172,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals { PaletteQuantizer paletteQuantizer = default; bool hasPaletteQuantizer = false; + + // Create a buffer to store de-duplicated pixel indices for encoding. + // These are used when the color table is global but we must always allocate since we don't know + // in advance whether the frames will use a local palette. + Buffer2D indices = this.memoryAllocator.Allocate2D(image.Width, image.Height); + + // Store the first frame as a reference for de-duplication comparison. + IndexedImageFrame previousQuantized = quantized; for (int i = 0; i < image.Frames.Count; i++) { // Gather the metadata for this frame. @@ -155,15 +194,21 @@ internal sealed class GifEncoderCore : IImageEncoderInternals // since the palette is unchanging. This allows a reduction of memory usage across // multi frame gifs using a global palette. hasPaletteQuantizer = true; - paletteQuantizer = new(this.configuration, this.quantizer.Options, palette); + paletteQuantizer = new(this.configuration, this.quantizer!.Options, palette); } - this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer); + this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, indices, ref previousQuantized, ref quantized!, ref paletteQuantizer); // Clean up for the next run. - quantized.Dispose(); + if (quantized != previousQuantized) + { + quantized.Dispose(); + } } + previousQuantized.Dispose(); + indices.Dispose(); + if (hasPaletteQuantizer) { paletteQuantizer.Dispose(); @@ -176,47 +221,55 @@ internal sealed class GifEncoderCore : IImageEncoderInternals int frameIndex, bool useLocal, GifFrameMetadata? metadata, + Buffer2D indices, + ref IndexedImageFrame previousQuantized, ref IndexedImageFrame quantized, - ref PaletteQuantizer paletteQuantizer) + ref PaletteQuantizer globalPaletteQuantizer) where TPixel : unmanaged, IPixel { // The first frame has already been quantized so we do not need to do so again. + int transparencyIndex = -1; if (frameIndex > 0) { if (useLocal) { // Reassign using the current frame and details. - QuantizerOptions? options = null; - int colorTableLength = metadata?.ColorTableLength ?? 0; - if (colorTableLength > 0) + if (metadata?.DecodedLocalColorTable.Length > 0) { - options = new() - { - Dither = this.quantizer.Options.Dither, - DitherScale = this.quantizer.Options.DitherScale, - MaxColors = colorTableLength - }; + // We can use the color data from the decoded metadata here. + // We avoid dithering by default to preserve the original colors. + PaletteQuantizer localQuantizer = new(metadata.DecodedLocalColorTable, new() { Dither = null }); + using IQuantizer frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer(this.configuration, localQuantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + } + else + { + // We must quantize the frame to generate a local color table. + IQuantizer localQuantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree; + using IQuantizer frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer(this.configuration, localQuantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); } - - using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, options ?? this.quantizer.Options); - quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); } else { // Quantize the image using the global palette. - quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds()); + quantized = globalPaletteQuantizer.QuantizeFrame(frame, frame.Bounds()); + transparencyIndex = GetTransparentIndex(quantized, metadata); + + // De-duplicate pixels comparing to the previous frame. + // Only global is supported for now as the color palettes as the operation required to compare + // and offset the index lookups is too expensive for local palettes. + DeDuplicatePixels(previousQuantized, quantized, indices, transparencyIndex); } this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); } - - // Do we have extension information to write? - int index = GetTransparentIndex(quantized); - if (metadata != null || index > -1) + else { - this.WriteGraphicalControlExtension(metadata ?? new(), index, stream); + transparencyIndex = GetTransparentIndex(quantized, metadata); } + this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream); this.WriteImageDescriptor(frame, useLocal, stream); if (useLocal) @@ -224,18 +277,103 @@ internal sealed class GifEncoderCore : IImageEncoderInternals this.WriteColorTable(quantized, stream); } - this.WriteImageData(quantized, stream); + // Assign the correct buffer to compress. + // If we are using a local palette or it's the first run then we want to use the quantized frame. + Buffer2D buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices; + this.WriteImageData(buffer, stream); + + // Swap the buffers. + (quantized, previousQuantized) = (previousQuantized, quantized); + } + + private static void DeDuplicatePixels( + IndexedImageFrame background, + IndexedImageFrame source, + Buffer2D indices, + int transparencyIndex) + where TPixel : unmanaged, IPixel + { + // TODO: This should be the background color if not transparent. + byte replacementIndex = unchecked((byte)transparencyIndex); + for (int y = 0; y < background.Height; y++) + { + ref byte backgroundRowBase = ref MemoryMarshal.GetReference(background.DangerousGetRowSpan(y)); + ref byte sourceRowBase = ref MemoryMarshal.GetReference(source.DangerousGetRowSpan(y)); + ref byte indicesRowBase = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y)); + + uint x = 0; + if (Avx2.IsSupported) + { + int remaining = background.Width; + Vector256 transparentVector = Vector256.Create(replacementIndex); + while (remaining >= Vector256.Count) + { + Vector256 b = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref backgroundRowBase, x)); + Vector256 s = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref sourceRowBase, x)); + Vector256 m = Avx2.CompareEqual(b, s); + Vector256 i = Avx2.BlendVariable(s, transparentVector, m); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i); + + x += (uint)Vector256.Count; + remaining -= Vector256.Count; + } + } + else if (Sse2.IsSupported) + { + int remaining = background.Width; + Vector128 transparentVector = Vector128.Create(replacementIndex); + while (remaining >= Vector128.Count) + { + Vector128 b = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref backgroundRowBase, x)); + Vector128 s = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref sourceRowBase, x)); + Vector128 m = Sse2.CompareEqual(b, s); + Vector128 i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i); + + x += (uint)Vector128.Count; + remaining -= Vector128.Count; + } + } + else if (AdvSimd.Arm64.IsSupported) + { + int remaining = background.Width; + Vector128 transparentVector = Vector128.Create(replacementIndex); + while (remaining >= Vector128.Count) + { + Vector128 b = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref backgroundRowBase, x)); + Vector128 s = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref sourceRowBase, x)); + Vector128 m = AdvSimd.CompareEqual(b, s); + Vector128 i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i); + + x += (uint)Vector128.Count; + remaining -= Vector128.Count; + } + } + + for (; x < (uint)background.Width; x++) + { + byte b = Unsafe.Add(ref backgroundRowBase, x); + byte s = Unsafe.Add(ref sourceRowBase, x); + ref byte i = ref Unsafe.Add(ref indicesRowBase, x); + i = (b == s) ? replacementIndex : s; + } + } } /// /// 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. @@ -255,6 +393,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } } + if (metadata?.HasTransparency == true && index == -1) + { + index = metadata.TransparencyIndex; + } + return index; } @@ -271,14 +414,14 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// 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 stream to write to. private void WriteLogicalScreenDescriptor( ImageMetadata metadata, int width, int height, - int transparencyIndex, + int backgroundIndex, bool useGlobalTable, Stream stream) { @@ -316,7 +459,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals width: (ushort)width, height: (ushort)height, packed: packedValue, - backgroundColorIndex: unchecked((byte)transparencyIndex), + backgroundColorIndex: unchecked((byte)backgroundIndex), ratio); Span buffer = stackalloc byte[20]; @@ -412,16 +555,26 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// 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) { + bool hasTransparency; + if (metadata is null) + { + hasTransparency = transparencyIndex > -1; + } + else + { + hasTransparency = metadata.HasTransparency; + } + byte packedValue = GifGraphicControlExtension.GetPackedValue( - disposalMethod: metadata.DisposalMethod, - transparencyFlag: transparencyIndex > -1); + disposalMethod: metadata!.DisposalMethod, + transparencyFlag: hasTransparency); GifGraphicControlExtension extension = new( packed: packedValue, delayTime: (ushort)metadata.FrameDelay, - transparencyIndex: unchecked((byte)transparencyIndex)); + transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue); this.WriteExtension(extension, stream); } @@ -521,13 +674,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// Writes the image pixel data to the stream. /// - /// The pixel format. - /// The containing indexed pixels. + /// The containing indexed pixels. /// The stream to write to. - private void WriteImageData(IndexedImageFrame image, Stream stream) - where TPixel : unmanaged, IPixel + private void WriteImageData(Buffer2D indices, Stream stream) { using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth); - encoder.Encode(((IPixelSource)image).PixelBuffer, stream); + encoder.Encode(indices, stream); } } diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index 7f4b49f0b..bcf990db5 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -22,9 +22,16 @@ public class GifFrameMetadata : IDeepCloneable private GifFrameMetadata(GifFrameMetadata other) { this.ColorTableMode = other.ColorTableMode; - this.ColorTableLength = other.ColorTableLength; this.FrameDelay = other.FrameDelay; this.DisposalMethod = other.DisposalMethod; + + if (other.DecodedLocalColorTable.Length > 0) + { + this.DecodedLocalColorTable = other.DecodedLocalColorTable.ToArray(); + } + + this.HasTransparency = other.HasTransparency; + this.TransparencyIndex = other.TransparencyIndex; } /// @@ -33,11 +40,21 @@ public class GifFrameMetadata : IDeepCloneable 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 the decoded global color table, if any. + /// + public ReadOnlyMemory DecodedLocalColorTable { get; internal 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 da21e134e..251cde0bb 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -23,7 +23,12 @@ public class GifMetadata : IDeepCloneable { this.RepeatCount = other.RepeatCount; this.ColorTableMode = other.ColorTableMode; - this.GlobalColorTableLength = other.GlobalColorTableLength; + this.BackgroundColor = other.BackgroundColor; + + if (other.DecodedGlobalColorTable.Length > 0) + { + this.DecodedGlobalColorTable = other.DecodedGlobalColorTable.ToArray(); + } for (int i = 0; i < other.Comments.Count; i++) { @@ -45,9 +50,15 @@ public class GifMetadata : IDeepCloneable public GifColorTableMode ColorTableMode { get; set; } /// - /// Gets or sets the length of the global color table if present. + /// Gets the decoded global color table, if any. + /// + public ReadOnlyMemory DecodedGlobalColorTable { get; internal 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 BackgroundColor { get; set; } /// /// Gets or sets the collection of comments about the graphics, credits, descriptions or any diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index e20b9dd17..9ba95952e 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 f2226974c..b8324a080 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -35,9 +35,9 @@ internal static class PaethFilter // 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 @@ internal static class PaethFilter } [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 @@ internal static class PaethFilter 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 @@ internal static class PaethFilter 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 @@ internal static class PaethFilter } } - [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 1d068303b..595601522 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 b7eb86afb..330d8988c 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 24cca41dc..fb5b9f2ed 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 d7243c696..7d4ebb3f1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory; 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 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals { 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; @@ -320,7 +321,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals { 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/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index 1173e02e1..f4b2dfc08 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -173,13 +173,15 @@ public sealed class Buffer2D : IDisposable /// 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 @@ public sealed class Buffer2D : IDisposable } [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/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs index b3d03d933..a6bb265a8 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 class QuantizerOptions /// 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/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 7fc61066a..0b8034572 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -171,10 +171,21 @@ public class GifEncoderTests GifMetadata metaData = image.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); GifColorTableMode colorMode = metaData.ColorTableMode; + + int maxColors; + if (colorMode == GifColorTableMode.Global) + { + maxColors = metaData.DecodedGlobalColorTable.Length; + } + else + { + maxColors = frameMetadata.DecodedLocalColorTable.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 +198,31 @@ public class GifEncoderTests 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.DecodedGlobalColorTable.Length; + } + else + { + maxColors = frameMetadata.DecodedLocalColorTable.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.DecodedLocalColorTable.Length, cMeta.DecodedLocalColorTable.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 9a8b41d54..70d75cdd4 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 + DecodedLocalColorTable = 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.DecodedLocalColorTable = 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.DecodedLocalColorTable.Length == clone.DecodedLocalColorTable.Length); + Assert.Equal(1, clone.DecodedLocalColorTable.Length); } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index 40ac94eea..82a999b69 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 class GifMetadataTests { RepeatCount = 1, ColorTableMode = GifColorTableMode.Global, - GlobalColorTableLength = 2, + DecodedGlobalColorTable = new[] { Color.Black, Color.White }, Comments = new List { "Foo" } }; @@ -43,11 +42,12 @@ public class GifMetadataTests clone.RepeatCount = 2; clone.ColorTableMode = GifColorTableMode.Local; - clone.GlobalColorTableLength = 1; + clone.DecodedGlobalColorTable = 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.DecodedGlobalColorTable.Length == clone.DecodedGlobalColorTable.Length); + Assert.Equal(1, clone.DecodedGlobalColorTable.Length); Assert.False(meta.Comments.Equals(clone.Comments)); Assert.True(meta.Comments.SequenceEqual(clone.Comments)); } @@ -205,7 +205,12 @@ public class GifMetadataTests 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.DecodedGlobalColorTable.Length); + } + Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay); Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod); } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index bc22806c3..7c1150b77 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -279,6 +279,7 @@ public abstract partial class ImageFrameCollectionTests { using Image source = provider.GetImage(); using Image 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 abstract partial class ImageFrameCollectionTests // Drop the original empty root frame: dest.Frames.RemoveFrame(0); - dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); + dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false); dest.CompareToOriginal(provider); for (int i = 0; i < 5; i++) @@ -314,7 +315,11 @@ public abstract partial class ImageFrameCollectionTests Assert.Equal(aData.DisposalMethod, bData.DisposalMethod); Assert.Equal(aData.FrameDelay, bData.FrameDelay); - Assert.Equal(aData.ColorTableLength, bData.ColorTableLength); + + if (aData.ColorTableMode == GifColorTableMode.Local && bData.ColorTableMode == GifColorTableMode.Local) + { + Assert.Equal(aData.DecodedLocalColorTable.Length, bData.DecodedLocalColorTable.Length); + } } } } diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index e9c61db6f..9d11bb897 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; @@ -22,17 +23,17 @@ public class ImageFrameMetadataTests const int colorTableLength = 128; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; - var metaData = new ImageFrameMetadata(); + ImageFrameMetadata metaData = new(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); gifFrameMetadata.FrameDelay = frameDelay; - gifFrameMetadata.ColorTableLength = colorTableLength; + gifFrameMetadata.DecodedLocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray(); gifFrameMetadata.DisposalMethod = disposalMethod; - var clone = new ImageFrameMetadata(metaData); + ImageFrameMetadata clone = new(metaData); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); - Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength); + Assert.Equal(colorTableLength, cloneGifFrameMetadata.DecodedLocalColorTable.Length); Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); } @@ -40,19 +41,19 @@ public class ImageFrameMetadataTests public void CloneIsDeep() { // arrange - var exifProfile = new ExifProfile(); + ExifProfile exifProfile = new(); exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest"); - var xmpProfile = new XmpProfile(new byte[0]); - var iccProfile = new IccProfile() + XmpProfile xmpProfile = new(Array.Empty()); + 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,