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,