diff --git a/src/ImageSharp/Color/Color.Conversions.cs b/src/ImageSharp/Color/Color.Conversions.cs
index bbb848867..309ab83ec 100644
--- a/src/ImageSharp/Color/Color.Conversions.cs
+++ b/src/ImageSharp/Color/Color.Conversions.cs
@@ -139,7 +139,7 @@ public readonly partial struct Color
///
/// The .
/// The .
- public static explicit operator Vector4(Color color) => color.ToVector4();
+ public static explicit operator Vector4(Color color) => color.ToScaledVector4();
///
/// Converts an to .
@@ -228,7 +228,7 @@ public readonly partial struct Color
}
[MethodImpl(InliningOptions.ShortMethod)]
- internal Vector4 ToVector4()
+ internal Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
{
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 45819b751..4ea3795d7 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -2,10 +2,10 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
-using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@@ -52,11 +52,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
private GifColorTableMode? colorTableMode;
- ///
- /// The number of bits requires to store the color palette.
- ///
- private int bitDepth;
-
///
/// The pixel sampling strategy for global quantization.
///
@@ -65,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// Initializes a new instance of the class.
///
- /// The configuration which allows altering default behaviour or extending the library.
+ /// The configuration which allows altering default behavior or extending the library.
/// The encoder with options.
public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{
@@ -96,8 +91,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
- // Quantize the image returning a palette.
- IndexedImageFrame? quantized;
+ // Quantize the first image frame returning a palette.
+ IndexedImageFrame? quantized = null;
+
+ // Work out if there is an explicit transparent index set for the frame. We use that to ensure the
+ // correct value is set for the background index when quantizing.
+ image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
+ int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (this.quantizer is null)
{
@@ -105,7 +105,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
- this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null });
+ this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
}
else
{
@@ -127,27 +127,24 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
- // Get the number of bits.
- this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
-
// Write the header.
WriteHeader(stream);
// Write the LSD.
- image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
-
- int transparentIndex = GetTransparentIndex(quantized, frameMetadata);
- byte backgroundIndex = unchecked((byte)transparentIndex);
- if (transparentIndex == -1)
+ transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
+ byte backgroundIndex = unchecked((byte)transparencyIndex);
+ if (transparencyIndex == -1)
{
backgroundIndex = gifMetadata.BackgroundColor;
}
- this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, stream);
+ // Get the number of bits.
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
if (useGlobalTable)
{
- this.WriteColorTable(quantized, stream);
+ this.WriteColorTable(quantized, bitDepth, stream);
}
if (!this.skipMetadata)
@@ -160,67 +157,69 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
- this.EncodeFrames(stream, image, backgroundIndex, quantized, quantized.Palette.ToArray());
+ this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex);
+
+ // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
+ TPixel[] globalPalette = image.Frames.Count == 1 ? Array.Empty() : quantized.Palette.ToArray();
+
+ quantized.Dispose();
+
+ this.EncodeAdditionalFrames(stream, image, globalPalette);
stream.WriteByte(GifConstants.EndIntroducer);
}
- private void EncodeFrames(
+ private void EncodeAdditionalFrames(
Stream stream,
Image image,
- byte backgroundIndex,
- IndexedImageFrame quantized,
- ReadOnlyMemory palette)
+ ReadOnlyMemory globalPalette)
where TPixel : unmanaged, IPixel
{
+ if (image.Frames.Count == 1)
+ {
+ return;
+ }
+
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++)
+ ImageFrame previousFrame = image.Frames.RootFrame;
+
+ // This frame is reused to store de-duplicated pixel buffers.
+ // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
+ // frames using both local and global palettes.
+ using ImageFrame encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size());
+
+ for (int i = 1; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
- ImageFrame frame = image.Frames[i];
- ImageFrameMetadata metadata = frame.Metadata;
- bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local);
+ ImageFrame currentFrame = image.Frames[i];
+ ImageFrameMetadata metadata = currentFrame.Metadata;
+ metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
- // The palette quantizer can reuse the same pixel map across multiple frames
- // since the palette is unchanging. This allows a reduction of memory usage across
- // multi frame gifs using a global palette.
+ // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
+ // This allows a reduction of memory usage across multi-frame gifs using a global palette
+ // and also allows use to reuse the cache from previous runs.
+ int transparencyIndex = gifMetadata?.HasTransparency == true ? gifMetadata.TransparencyIndex : -1;
+ paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
- paletteQuantizer = new(this.configuration, this.quantizer!.Options, palette);
}
- this.EncodeFrame(
+ this.EncodeAdditionalFrame(
stream,
- frame,
- i,
+ previousFrame,
+ currentFrame,
+ encodingFrame,
useLocal,
- frameMetadata,
- indices,
- backgroundIndex,
- ref previousQuantized,
- ref quantized!,
- ref paletteQuantizer);
-
- // Clean up for the next run.
- if (quantized != previousQuantized)
- {
- quantized.Dispose();
- }
- }
+ gifMetadata,
+ paletteQuantizer);
- previousQuantized.Dispose();
- indices.Dispose();
+ previousFrame = currentFrame;
+ }
if (hasPaletteQuantizer)
{
@@ -228,161 +227,175 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
- private void EncodeFrame(
+ private void EncodeFirstFrame(
Stream stream,
- ImageFrame frame,
- int frameIndex,
+ GifFrameMetadata? metadata,
+ IndexedImageFrame quantized,
+ int transparencyIndex)
+ where TPixel : unmanaged, IPixel
+ {
+ this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+
+ Buffer2DRegion region = ((IPixelSource)quantized).PixelBuffer.GetRegion();
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local);
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+
+ this.WriteImageDescriptor(region.Rectangle, useLocal, bitDepth, stream);
+
+ if (useLocal)
+ {
+ this.WriteColorTable(quantized, bitDepth, stream);
+ }
+
+ this.WriteImageData(region, stream, quantized.Palette.Length, transparencyIndex);
+ }
+
+ private void EncodeAdditionalFrame(
+ Stream stream,
+ ImageFrame previousFrame,
+ ImageFrame currentFrame,
+ ImageFrame encodingFrame,
bool useLocal,
GifFrameMetadata? metadata,
- Buffer2D indices,
- byte backgroundIndex,
- ref IndexedImageFrame previousQuantized,
- ref IndexedImageFrame quantized,
- ref PaletteQuantizer globalPaletteQuantizer)
+ 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)
+ // Capture any explicit transparency index from the metadata.
+ // We use it to determine the value to use to replace duplicate pixels.
+ int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1;
+ Vector4 replacement = Vector4.Zero;
+ if (transparencyIndex >= 0)
{
if (useLocal)
{
- // Reassign using the current frame and details.
if (metadata?.LocalColorTable?.Length > 0)
{
- // We can use the color data from the decoded metadata here.
- // We avoid dithering by default to preserve the original colors.
- PaletteQuantizer localQuantizer = new(metadata.LocalColorTable.Value, 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());
+ ReadOnlySpan palette = metadata.LocalColorTable.Value.Span;
+ if (transparencyIndex < palette.Length)
+ {
+ replacement = palette[transparencyIndex].ToScaledVector4();
+ }
}
}
else
{
- // Quantize the image using the global palette.
- quantized = globalPaletteQuantizer.QuantizeFrame(frame, frame.Bounds());
- transparencyIndex = GetTransparentIndex(quantized, metadata);
-
- byte replacementIndex = unchecked((byte)transparencyIndex);
- if (transparencyIndex == -1)
+ ReadOnlySpan palette = globalPaletteQuantizer.Palette.Span;
+ if (transparencyIndex < palette.Length)
{
- replacementIndex = backgroundIndex;
+ replacement = palette[transparencyIndex].ToScaledVector4();
}
-
- // 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, replacementIndex);
}
+ }
+
+ this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
- this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ IndexedImageFrame quantized;
+ if (useLocal)
+ {
+ // Reassign using the current frame and details.
+ if (metadata?.LocalColorTable?.Length > 0)
+ {
+ // We can use the color data from the decoded metadata here.
+ // We avoid dithering by default to preserve the original colors.
+ ReadOnlyMemory palette = metadata.LocalColorTable.Value;
+ PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
+ }
+ else
+ {
+ // We must quantize the frame to generate a local color table.
+ IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
+ }
}
else
{
- transparencyIndex = GetTransparentIndex(quantized, metadata);
+ // Quantize the image using the global palette.
+ // Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
+ globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
+ quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds());
}
- this.WriteGraphicalControlExtension(metadata, transparencyIndex, 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;
+ // Recalculate the transparency index as depending on the quantizer used could have a new value.
+ transparencyIndex = GetTransparentIndex(quantized, metadata);
// Trim down the buffer to the minimum size required.
- Buffer2DRegion region = TrimTransparentPixels(buffer, transparencyIndex);
- this.WriteImageDescriptor(region.Rectangle, useLocal, stream);
+ // Buffer2DRegion region = ((IPixelSource)quantized).PixelBuffer.GetRegion();
+ Buffer2DRegion region = TrimTransparentPixels(((IPixelSource)quantized).PixelBuffer, transparencyIndex);
+
+ this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+
+ int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
+ this.WriteImageDescriptor(region.Rectangle, useLocal, bitDepth, stream);
if (useLocal)
{
- this.WriteColorTable(quantized, stream);
+ this.WriteColorTable(quantized, bitDepth, stream);
}
- this.WriteImageData(region, stream);
-
- // Swap the buffers.
- (quantized, previousQuantized) = (previousQuantized, quantized);
+ this.WriteImageData(region, stream, quantized.Palette.Length, transparencyIndex);
}
- private static void DeDuplicatePixels(
- IndexedImageFrame background,
- IndexedImageFrame source,
- Buffer2D indices,
- byte replacementIndex)
+ private void DeDuplicatePixels(
+ ImageFrame backgroundFrame,
+ ImageFrame sourceFrame,
+ ImageFrame resultFrame,
+ Vector4 replacement)
where TPixel : unmanaged, IPixel
{
- for (int y = 0; y < background.Height; y++)
+ IMemoryOwner buffers = this.memoryAllocator.Allocate(backgroundFrame.Width * 3);
+ Span background = buffers.GetSpan()[..backgroundFrame.Width];
+ Span source = buffers.GetSpan()[backgroundFrame.Width..];
+ Span result = buffers.GetSpan()[(backgroundFrame.Width * 2)..];
+
+ // TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color
+ // is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better.
+ // This would require a more complex algorithm.
+ for (int y = 0; y < backgroundFrame.Height; y++)
{
- 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));
+ PixelOperations.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
+ PixelOperations.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
+
+ ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background));
+ ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source));
+ ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result));
uint x = 0;
- if (Avx2.IsSupported)
+ int remaining = background.Length;
+ if (Avx.IsSupported && remaining >= 2)
{
- 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);
+ Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
- x += (uint)Vector256.Count;
- remaining -= Vector256.Count;
- }
- }
- else if (Sse2.IsSupported)
- {
- int remaining = background.Width;
- Vector128 transparentVector = Vector128.Create(replacementIndex);
- while (remaining >= Vector128.Count)
+ while (remaining >= 2)
{
- 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);
+ Vector256 b = Unsafe.Add(ref backgroundBase, x);
+ Vector256 s = Unsafe.Add(ref sourceBase, x);
- Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
+ Vector256 m = Avx.CompareEqual(b, s).AsInt32();
- 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);
+ m = Avx2.HorizontalAdd(m, m);
+ m = Avx2.HorizontalAdd(m, m);
+ m = Avx2.CompareEqual(m, Vector256.Create(-4));
- Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
+ Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle());
- x += (uint)Vector128.Count;
- remaining -= Vector128.Count;
+ x++;
+ remaining -= 2;
}
}
- for (; x < (uint)background.Width; x++)
+ for (int i = remaining; i >= 0; i--)
{
- 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;
+ x = (uint)i;
+ Vector4 b = Unsafe.Add(ref Unsafe.As, Vector4>(ref backgroundBase), x);
+ Vector4 s = Unsafe.Add(ref Unsafe.As, Vector4>(ref sourceBase), x);
+ ref Vector4 r = ref Unsafe.Add(ref Unsafe.As, Vector4>(ref resultBase), x);
+ r = (b == s) ? replacement : s;
}
+
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
}
}
@@ -395,33 +408,85 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
byte trimmableIndex = unchecked((byte)transparencyIndex);
- int top = int.MaxValue;
- int bottom = int.MinValue;
+ int top = int.MinValue;
+ int bottom = int.MaxValue;
int left = int.MaxValue;
int right = int.MinValue;
+ // Run through th buffer in a single pass. Use variables to track the min/max values.
+ int minY = -1;
+ bool isTransparentRow = true;
for (int y = 0; y < buffer.Height; y++)
{
+ isTransparentRow = true;
Span rowSpan = buffer.DangerousGetRowSpan(y);
+
+ // TODO: It may be possible to optimize this inner loop using SIMD.
for (int x = 0; x < rowSpan.Length; x++)
{
if (rowSpan[x] != trimmableIndex)
{
- top = Math.Min(top, y);
- bottom = Math.Max(bottom, y);
+ isTransparentRow = false;
left = Math.Min(left, x);
right = Math.Max(right, x);
}
}
+
+ if (!isTransparentRow)
+ {
+ if (y == 0)
+ {
+ // First row is opaque.
+ // Capture to prevent over assignment when a match is found below.
+ top = 0;
+ }
+
+ // The minimum top bounds have already been captured.
+ // Increment the bottom to include the current opaque row.
+ if (minY < 0 && top != 0)
+ {
+ // Increment to the first opaque row.
+ top++;
+ }
+
+ minY = top;
+ bottom = y;
+ }
+ else
+ {
+ // We've yet to hit an opaque row. Capture the top position.
+ if (minY < 0)
+ {
+ top = Math.Max(top, y);
+ }
+
+ bottom = Math.Min(bottom, y);
+ }
}
- if (top == int.MaxValue || bottom == int.MinValue)
+ if (left == int.MaxValue)
{
- // No valid rectangle found
+ left = 0;
+ }
+
+ if (right == int.MinValue)
+ {
+ right = buffer.Width;
+ }
+
+ if (top == bottom || left == right)
+ {
+ // The entire image is transparent.
return buffer.GetRegion();
}
- return buffer.GetRegion(Rectangle.FromLTRB(left, top, right, bottom));
+ if (!isTransparentRow)
+ {
+ // Last row is opaque.
+ bottom = buffer.Height;
+ }
+
+ return buffer.GetRegion(Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height)));
}
///
@@ -433,29 +498,29 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// The .
///
- private static int GetTransparentIndex(IndexedImageFrame quantized, GifFrameMetadata? metadata)
+ private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel
{
- // Transparent pixels are much more likely to be found at the end of a palette.
- int index = -1;
- ReadOnlySpan paletteSpan = quantized.Palette.Span;
-
- using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length);
- Span rgbaSpan = rgbaOwner.GetSpan();
- PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
- ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
-
- for (int i = rgbaSpan.Length - 1; i >= 0; i--)
+ if (metadata?.HasTransparency == true)
{
- if (Unsafe.Add(ref rgbaSpanRef, (uint)i).Equals(default))
- {
- index = i;
- }
+ return metadata.TransparencyIndex;
}
- if (metadata?.HasTransparency == true && index == -1)
+ int index = -1;
+ if (quantized != null)
{
- index = metadata.TransparencyIndex;
+ TPixel transparentPixel = default;
+ transparentPixel.FromScaledVector4(Vector4.Zero);
+ ReadOnlySpan palette = quantized.Palette.Span;
+
+ // Transparent pixels are much more likely to be found at the end of a palette.
+ for (int i = palette.Length - 1; i >= 0; i--)
+ {
+ if (palette[i].Equals(transparentPixel))
+ {
+ index = i;
+ }
+ }
}
return index;
@@ -476,6 +541,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// The image height.
/// The index to set the default background index to.
/// Whether to use a global or local color table.
+ /// The bit depth of the color palette.
/// The stream to write to.
private void WriteLogicalScreenDescriptor(
ImageMetadata metadata,
@@ -483,9 +549,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
int height,
byte backgroundIndex,
bool useGlobalTable,
+ int bitDepth,
Stream stream)
{
- byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
+ byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, bitDepth - 1, false, bitDepth - 1);
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@@ -617,10 +684,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// The stream to write to.
private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
{
+ GifFrameMetadata? data = metadata;
bool hasTransparency;
if (metadata is null)
{
- metadata = new();
+ data = new();
hasTransparency = transparencyIndex >= 0;
}
else
@@ -629,12 +697,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
byte packedValue = GifGraphicControlExtension.GetPackedValue(
- disposalMethod: metadata!.DisposalMethod,
+ disposalMethod: data!.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
- delayTime: (ushort)metadata.FrameDelay,
+ delayTime: (ushort)data.FrameDelay,
transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream);
@@ -684,14 +752,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// The frame location and size.
/// Whether to use the global color table.
+ /// The bit depth of the color palette.
/// The stream to write to.
- private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream)
+ private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream)
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
- localColorTableSize: this.bitDepth - 1);
+ localColorTableSize: bitDepth - 1);
GifImageDescriptor descriptor = new(
left: (ushort)rectangle.X,
@@ -711,12 +780,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// The pixel format.
/// The to encode.
+ /// The bit depth of the color palette.
/// The stream to write to.
- private void WriteColorTable(IndexedImageFrame image, Stream stream)
+ private void WriteColorTable(IndexedImageFrame image, int bitDepth, Stream stream)
where TPixel : unmanaged, IPixel
{
// The maximum number of colors for the bit depth
- int colorTableLength = ColorNumerics.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf();
+ int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf();
using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength, AllocationOptions.Clean);
Span colorTableSpan = colorTable.GetSpan();
@@ -735,9 +805,18 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
///
/// The containing indexed pixels.
/// The stream to write to.
- private void WriteImageData(Buffer2DRegion indices, Stream stream)
+ /// The length of the frame color palette.
+ /// The index of the color used to represent transparency.
+ private void WriteImageData(Buffer2DRegion indices, Stream stream, int paletteLength, int transparencyIndex)
{
- using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth);
+ // Pad the bit depth when required for encoding the image data.
+ // This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette
+ // as decoders skip indexes that are out of range.
+ int padding = transparencyIndex >= paletteLength
+ ? 1
+ : 0;
+
+ using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding));
encoder.Encode(indices, stream);
}
}
diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs
index 3734402d3..0e7eef11e 100644
--- a/src/ImageSharp/ImageFrame{TPixel}.cs
+++ b/src/ImageSharp/ImageFrame{TPixel}.cs
@@ -21,6 +21,16 @@ public sealed class ImageFrame : ImageFrame, IPixelSource
{
private bool isDisposed;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration which allows altering default behaviour or extending the library.
+ /// The of the frame.
+ internal ImageFrame(Configuration configuration, Size size)
+ : this(configuration, size.Width, size.Height, new ImageFrameMetadata())
+ {
+ }
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
index 0c6ba7ddc..e767ac4f7 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
@@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
///
/// The pixel format.
///
-/// This class is not threadsafe and should not be accessed in parallel.
+/// This class is not thread safe and should not be accessed in parallel.
/// Doing so will result in non-idempotent results.
///
internal sealed class EuclideanPixelMap : IDisposable
where TPixel : unmanaged, IPixel
{
private Rgba32[] rgbaPalette;
+ private int transparentIndex;
///
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@@ -34,12 +36,24 @@ internal sealed class EuclideanPixelMap : IDisposable
/// The configuration.
/// The color palette to map from.
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette)
+ : this(configuration, palette, -1)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ /// The color palette to map from.
+ /// An explicit index at which to match transparent pixels.
+ public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette, int transparentIndex = -1)
{
this.configuration = configuration;
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
+ this.transparentIndex = transparentIndex;
}
///
@@ -91,16 +105,43 @@ internal sealed class EuclideanPixelMap : IDisposable
this.cache.Clear();
}
+ ///
+ /// Allows setting the transparent index after construction.
+ ///
+ /// An explicit index at which to match transparent pixels.
+ public void SetTransparentIndex(int index) => this.transparentIndex = index;
+
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
+
+ if (this.transparentIndex >= 0 && rgba == default)
+ {
+ // We have explicit instructions. No need to search.
+ index = this.transparentIndex;
+ this.cache.Add(rgba, (byte)index);
+
+ if (index >= 0 && index < this.Palette.Length)
+ {
+ match = Unsafe.Add(ref paletteRef, (uint)index);
+ }
+ else
+ {
+ Unsafe.SkipInit(out TPixel pixel);
+ pixel.FromScaledVector4(Vector4.Zero);
+ match = pixel;
+ }
+
+ return index;
+ }
+
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Rgba32 candidate = this.rgbaPalette[i];
- int distance = DistanceSquared(rgba, candidate);
+ float distance = DistanceSquared(rgba, candidate);
// If it's an exact match, exit the loop
if (distance == 0)
@@ -130,12 +171,12 @@ internal sealed class EuclideanPixelMap : IDisposable
/// The second point.
/// The distance squared.
[MethodImpl(InliningOptions.ShortMethod)]
- private static int DistanceSquared(Rgba32 a, Rgba32 b)
+ private static float DistanceSquared(Rgba32 a, Rgba32 b)
{
- int deltaR = a.R - b.R;
- int deltaG = a.G - b.G;
- int deltaB = a.B - b.B;
- int deltaA = a.A - b.A;
+ float deltaR = a.R - b.R;
+ float deltaG = a.G - b.G;
+ float deltaB = a.B - b.B;
+ float deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
index fe4af9005..acd179ffc 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
public class PaletteQuantizer : IQuantizer
{
private readonly ReadOnlyMemory colorPalette;
+ private readonly int transparentIndex;
///
/// Initializes a new instance of the class.
@@ -27,12 +28,24 @@ public class PaletteQuantizer : IQuantizer
/// The color palette.
/// The quantizer options defining quantization rules.
public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options)
+ : this(palette, options, -1)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The color palette.
+ /// The quantizer options defining quantization rules.
+ /// An explicit index at which to match transparent pixels.
+ internal PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options, int transparentIndex)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.colorPalette = palette;
this.Options = options;
+ this.transparentIndex = transparentIndex;
}
///
@@ -52,6 +65,6 @@ public class PaletteQuantizer : IQuantizer
// Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
- return new PaletteQuantizer(configuration, options, palette);
+ return new PaletteQuantizer(configuration, options, palette, this.transparentIndex);
}
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
index 86db9f6f0..3df80ea9b 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
@@ -25,18 +25,23 @@ internal readonly struct PaletteQuantizer : IQuantizer
///
/// Initializes a new instance of the struct.
///
- /// The configuration which allows altering default behaviour or extending the library.
+ /// The configuration which allows altering default behavior or extending the library.
/// The quantizer options defining quantization rules.
/// The palette to use.
+ /// An explicit index at which to match transparent pixels.
[MethodImpl(InliningOptions.ShortMethod)]
- public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory palette)
+ public PaletteQuantizer(
+ Configuration configuration,
+ QuantizerOptions options,
+ ReadOnlyMemory palette,
+ int transparentIndex)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
- this.pixelMap = new EuclideanPixelMap(configuration, palette);
+ this.pixelMap = new EuclideanPixelMap(configuration, palette, transparentIndex);
}
///
@@ -59,6 +64,12 @@ internal readonly struct PaletteQuantizer : IQuantizer
{
}
+ ///
+ /// Allows setting the transparent index after construction.
+ ///
+ /// An explicit index at which to match transparent pixels.
+ public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index);
+
///
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
index 43114fa7e..31001e31b 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
@@ -38,7 +38,7 @@ public class GifEncoderTests
[Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]
- [WithTestPatternImages(100, 100, TestPixelTypes, false)]
+ [WithTestPatternImages(100, 100, TestPixelTypes, true)]
public void EncodeGeneratedPatterns(TestImageProvider provider, bool limitAllocationBuffer)
where TPixel : unmanaged, IPixel
{