Browse Source

Refactor and fix gif encoder

pull/2500/head
James Jackson-South 3 years ago
parent
commit
98ed0f1070
  1. 4
      src/ImageSharp/Color/Color.Conversions.cs
  2. 475
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 10
      src/ImageSharp/ImageFrame{TPixel}.cs
  4. 55
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  5. 15
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  6. 17
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  7. 2
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

4
src/ImageSharp/Color/Color.Conversions.cs

@ -139,7 +139,7 @@ public readonly partial struct Color
/// </summary> /// </summary>
/// <param name="color">The <see cref="Color"/>.</param> /// <param name="color">The <see cref="Color"/>.</param>
/// <returns>The <see cref="Vector4"/>.</returns> /// <returns>The <see cref="Vector4"/>.</returns>
public static explicit operator Vector4(Color color) => color.ToVector4(); public static explicit operator Vector4(Color color) => color.ToScaledVector4();
/// <summary> /// <summary>
/// Converts an <see cref="Vector4"/> to <see cref="Color"/>. /// Converts an <see cref="Vector4"/> to <see cref="Color"/>.
@ -228,7 +228,7 @@ public readonly partial struct Color
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal Vector4 ToVector4() internal Vector4 ToScaledVector4()
{ {
if (this.boxedHighPrecisionPixel is null) if (this.boxedHighPrecisionPixel is null)
{ {

475
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -2,10 +2,10 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -52,11 +52,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private GifColorTableMode? colorTableMode; private GifColorTableMode? colorTableMode;
/// <summary>
/// The number of bits requires to store the color palette.
/// </summary>
private int bitDepth;
/// <summary> /// <summary>
/// The pixel sampling strategy for global quantization. /// The pixel sampling strategy for global quantization.
/// </summary> /// </summary>
@ -65,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="encoder">The encoder with options.</param> /// <param name="encoder">The encoder with options.</param>
public GifEncoderCore(Configuration configuration, GifEncoder encoder) public GifEncoderCore(Configuration configuration, GifEncoder encoder)
{ {
@ -96,8 +91,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.colorTableMode ??= gifMetadata.ColorTableMode; this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette. // Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized; IndexedImageFrame<TPixel>? 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) if (this.quantizer is null)
{ {
@ -105,7 +105,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{ {
// We avoid dithering by default to preserve the original colors. // 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 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. // Write the header.
WriteHeader(stream); WriteHeader(stream);
// Write the LSD. // Write the LSD.
image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
byte backgroundIndex = unchecked((byte)transparencyIndex);
int transparentIndex = GetTransparentIndex(quantized, frameMetadata); if (transparencyIndex == -1)
byte backgroundIndex = unchecked((byte)transparentIndex);
if (transparentIndex == -1)
{ {
backgroundIndex = gifMetadata.BackgroundColor; 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) if (useGlobalTable)
{ {
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, bitDepth, stream);
} }
if (!this.skipMetadata) if (!this.skipMetadata)
@ -160,67 +157,69 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); 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<TPixel>() : quantized.Palette.ToArray();
quantized.Dispose();
this.EncodeAdditionalFrames(stream, image, globalPalette);
stream.WriteByte(GifConstants.EndIntroducer); stream.WriteByte(GifConstants.EndIntroducer);
} }
private void EncodeFrames<TPixel>( private void EncodeAdditionalFrames<TPixel>(
Stream stream, Stream stream,
Image<TPixel> image, Image<TPixel> image,
byte backgroundIndex, ReadOnlyMemory<TPixel> globalPalette)
IndexedImageFrame<TPixel> quantized,
ReadOnlyMemory<TPixel> palette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (image.Frames.Count == 1)
{
return;
}
PaletteQuantizer<TPixel> paletteQuantizer = default; PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false; 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<byte> indices = this.memoryAllocator.Allocate2D<byte>(image.Width, image.Height);
// Store the first frame as a reference for de-duplication comparison. // Store the first frame as a reference for de-duplication comparison.
IndexedImageFrame<TPixel> previousQuantized = quantized; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
for (int i = 0; i < image.Frames.Count; i++)
// 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<TPixel> encodingFrame = new(previousFrame.GetConfiguration(), previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)
{ {
// Gather the metadata for this frame. // Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata; ImageFrameMetadata metadata = currentFrame.Metadata;
bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local); bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0) if (!useLocal && !hasPaletteQuantizer && i > 0)
{ {
// The palette quantizer can reuse the same pixel map across multiple frames // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// since the palette is unchanging. This allows a reduction of memory usage across // This allows a reduction of memory usage across multi-frame gifs using a global palette
// 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; hasPaletteQuantizer = true;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, palette);
} }
this.EncodeFrame( this.EncodeAdditionalFrame(
stream, stream,
frame, previousFrame,
i, currentFrame,
encodingFrame,
useLocal, useLocal,
frameMetadata, gifMetadata,
indices, paletteQuantizer);
backgroundIndex,
ref previousQuantized,
ref quantized!,
ref paletteQuantizer);
// Clean up for the next run.
if (quantized != previousQuantized)
{
quantized.Dispose();
}
}
previousQuantized.Dispose(); previousFrame = currentFrame;
indices.Dispose(); }
if (hasPaletteQuantizer) if (hasPaletteQuantizer)
{ {
@ -228,161 +227,175 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
} }
private void EncodeFrame<TPixel>( private void EncodeFirstFrame<TPixel>(
Stream stream, Stream stream,
ImageFrame<TPixel> frame, GifFrameMetadata? metadata,
int frameIndex, IndexedImageFrame<TPixel> quantized,
int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
Buffer2DRegion<byte> 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<TPixel>(
Stream stream,
ImageFrame<TPixel> previousFrame,
ImageFrame<TPixel> currentFrame,
ImageFrame<TPixel> encodingFrame,
bool useLocal, bool useLocal,
GifFrameMetadata? metadata, GifFrameMetadata? metadata,
Buffer2D<byte> indices, PaletteQuantizer<TPixel> globalPaletteQuantizer)
byte backgroundIndex,
ref IndexedImageFrame<TPixel> previousQuantized,
ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> globalPaletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The first frame has already been quantized so we do not need to do so again. // Capture any explicit transparency index from the metadata.
int transparencyIndex = -1; // We use it to determine the value to use to replace duplicate pixels.
if (frameIndex > 0) int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1;
Vector4 replacement = Vector4.Zero;
if (transparencyIndex >= 0)
{ {
if (useLocal) if (useLocal)
{ {
// Reassign using the current frame and details.
if (metadata?.LocalColorTable?.Length > 0) if (metadata?.LocalColorTable?.Length > 0)
{ {
// We can use the color data from the decoded metadata here. ReadOnlySpan<Color> palette = metadata.LocalColorTable.Value.Span;
// We avoid dithering by default to preserve the original colors. if (transparencyIndex < palette.Length)
PaletteQuantizer localQuantizer = new(metadata.LocalColorTable.Value, new() { Dither = null }); {
using IQuantizer<TPixel> frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, localQuantizer.Options); replacement = palette[transparencyIndex].ToScaledVector4();
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<TPixel> frameQuantizer = localQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, localQuantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
} }
} }
else else
{ {
// Quantize the image using the global palette. ReadOnlySpan<TPixel> palette = globalPaletteQuantizer.Palette.Span;
quantized = globalPaletteQuantizer.QuantizeFrame(frame, frame.Bounds()); if (transparencyIndex < palette.Length)
transparencyIndex = GetTransparentIndex(quantized, metadata);
byte replacementIndex = unchecked((byte)transparencyIndex);
if (transparencyIndex == -1)
{ {
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<TPixel> 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<Color> palette = metadata.LocalColorTable.Value;
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(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<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
}
} }
else 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); // Recalculate the transparency index as depending on the quantizer used could have a new value.
transparencyIndex = GetTransparentIndex(quantized, metadata);
// 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<byte> buffer = useLocal || frameIndex == 0 ? ((IPixelSource)quantized).PixelBuffer : indices;
// Trim down the buffer to the minimum size required. // Trim down the buffer to the minimum size required.
Buffer2DRegion<byte> region = TrimTransparentPixels(buffer, transparencyIndex); // Buffer2DRegion<byte> region = ((IPixelSource)quantized).PixelBuffer.GetRegion();
this.WriteImageDescriptor(region.Rectangle, useLocal, stream); Buffer2DRegion<byte> 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) if (useLocal)
{ {
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, bitDepth, stream);
} }
this.WriteImageData(region, stream); this.WriteImageData(region, stream, quantized.Palette.Length, transparencyIndex);
// Swap the buffers.
(quantized, previousQuantized) = (previousQuantized, quantized);
} }
private static void DeDuplicatePixels<TPixel>( private void DeDuplicatePixels<TPixel>(
IndexedImageFrame<TPixel> background, ImageFrame<TPixel> backgroundFrame,
IndexedImageFrame<TPixel> source, ImageFrame<TPixel> sourceFrame,
Buffer2D<byte> indices, ImageFrame<TPixel> resultFrame,
byte replacementIndex) Vector4 replacement)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
for (int y = 0; y < background.Height; y++) IMemoryOwner<Vector4> buffers = this.memoryAllocator.Allocate<Vector4>(backgroundFrame.Width * 3);
Span<Vector4> background = buffers.GetSpan()[..backgroundFrame.Width];
Span<Vector4> source = buffers.GetSpan()[backgroundFrame.Width..];
Span<Vector4> 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)); PixelOperations<TPixel>.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
ref byte sourceRowBase = ref MemoryMarshal.GetReference(source.DangerousGetRowSpan(y)); PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
ref byte indicesRowBase = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y));
ref Vector256<float> backgroundBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(background));
ref Vector256<float> sourceBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<float> resultBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(result));
uint x = 0; uint x = 0;
if (Avx2.IsSupported) int remaining = background.Length;
if (Avx.IsSupported && remaining >= 2)
{ {
int remaining = background.Width; Vector256<float> replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
Vector256<byte> transparentVector = Vector256.Create(replacementIndex);
while (remaining >= Vector256<byte>.Count)
{
Vector256<byte> b = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref backgroundRowBase, x));
Vector256<byte> s = Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.Add(ref sourceRowBase, x));
Vector256<byte> m = Avx2.CompareEqual(b, s);
Vector256<byte> i = Avx2.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i);
x += (uint)Vector256<byte>.Count; while (remaining >= 2)
remaining -= Vector256<byte>.Count;
}
}
else if (Sse2.IsSupported)
{
int remaining = background.Width;
Vector128<byte> transparentVector = Vector128.Create(replacementIndex);
while (remaining >= Vector128<byte>.Count)
{ {
Vector128<byte> b = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref backgroundRowBase, x)); Vector256<float> b = Unsafe.Add(ref backgroundBase, x);
Vector128<byte> s = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref sourceRowBase, x)); Vector256<float> s = Unsafe.Add(ref sourceBase, x);
Vector128<byte> m = Sse2.CompareEqual(b, s);
Vector128<byte> i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i); Vector256<int> m = Avx.CompareEqual(b, s).AsInt32();
x += (uint)Vector128<byte>.Count; m = Avx2.HorizontalAdd(m, m);
remaining -= Vector128<byte>.Count; m = Avx2.HorizontalAdd(m, m);
} m = Avx2.CompareEqual(m, Vector256.Create(-4));
}
else if (AdvSimd.Arm64.IsSupported)
{
int remaining = background.Width;
Vector128<byte> transparentVector = Vector128.Create(replacementIndex);
while (remaining >= Vector128<byte>.Count)
{
Vector128<byte> b = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref backgroundRowBase, x));
Vector128<byte> s = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.Add(ref sourceRowBase, x));
Vector128<byte> m = AdvSimd.CompareEqual(b, s);
Vector128<byte> i = SimdUtils.HwIntrinsics.BlendVariable(s, transparentVector, m);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref indicesRowBase, x), i); Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle());
x += (uint)Vector128<byte>.Count; x++;
remaining -= Vector128<byte>.Count; remaining -= 2;
} }
} }
for (; x < (uint)background.Width; x++) for (int i = remaining; i >= 0; i--)
{ {
byte b = Unsafe.Add(ref backgroundRowBase, x); x = (uint)i;
byte s = Unsafe.Add(ref sourceRowBase, x); Vector4 b = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref backgroundBase), x);
ref byte i = ref Unsafe.Add(ref indicesRowBase, x); Vector4 s = Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref sourceBase), x);
i = (b == s) ? replacementIndex : s; ref Vector4 r = ref Unsafe.Add(ref Unsafe.As<Vector256<float>, Vector4>(ref resultBase), x);
r = (b == s) ? replacement : s;
} }
PixelOperations<TPixel>.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); byte trimmableIndex = unchecked((byte)transparencyIndex);
int top = int.MaxValue; int top = int.MinValue;
int bottom = int.MinValue; int bottom = int.MaxValue;
int left = int.MaxValue; int left = int.MaxValue;
int right = int.MinValue; 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++) for (int y = 0; y < buffer.Height; y++)
{ {
isTransparentRow = true;
Span<byte> rowSpan = buffer.DangerousGetRowSpan(y); Span<byte> rowSpan = buffer.DangerousGetRowSpan(y);
// TODO: It may be possible to optimize this inner loop using SIMD.
for (int x = 0; x < rowSpan.Length; x++) for (int x = 0; x < rowSpan.Length; x++)
{ {
if (rowSpan[x] != trimmableIndex) if (rowSpan[x] != trimmableIndex)
{ {
top = Math.Min(top, y); isTransparentRow = false;
bottom = Math.Max(bottom, y);
left = Math.Min(left, x); left = Math.Min(left, x);
right = Math.Max(right, 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();
} }
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)));
} }
/// <summary> /// <summary>
@ -433,29 +498,29 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <returns> /// <returns>
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized, GifFrameMetadata? metadata) private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Transparent pixels are much more likely to be found at the end of a palette. if (metadata?.HasTransparency == true)
int index = -1;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
for (int i = rgbaSpan.Length - 1; i >= 0; i--)
{ {
if (Unsafe.Add(ref rgbaSpanRef, (uint)i).Equals(default)) return metadata.TransparencyIndex;
{
index = i;
}
} }
if (metadata?.HasTransparency == true && index == -1) int index = -1;
if (quantized != null)
{ {
index = metadata.TransparencyIndex; TPixel transparentPixel = default;
transparentPixel.FromScaledVector4(Vector4.Zero);
ReadOnlySpan<TPixel> 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; return index;
@ -476,6 +541,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="height">The image height.</param> /// <param name="height">The image height.</param>
/// <param name="backgroundIndex">The index to set the default background index to.</param> /// <param name="backgroundIndex">The index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param> /// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor( private void WriteLogicalScreenDescriptor(
ImageMetadata metadata, ImageMetadata metadata,
@ -483,9 +549,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
int height, int height,
byte backgroundIndex, byte backgroundIndex,
bool useGlobalTable, bool useGlobalTable,
int bitDepth,
Stream stream) 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 // 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 // width over its height. The value range in this field allows
@ -617,10 +684,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream) private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
{ {
GifFrameMetadata? data = metadata;
bool hasTransparency; bool hasTransparency;
if (metadata is null) if (metadata is null)
{ {
metadata = new(); data = new();
hasTransparency = transparencyIndex >= 0; hasTransparency = transparencyIndex >= 0;
} }
else else
@ -629,12 +697,12 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
} }
byte packedValue = GifGraphicControlExtension.GetPackedValue( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata!.DisposalMethod, disposalMethod: data!.DisposalMethod,
transparencyFlag: hasTransparency); transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new( GifGraphicControlExtension extension = new(
packed: packedValue, packed: packedValue,
delayTime: (ushort)metadata.FrameDelay, delayTime: (ushort)data.FrameDelay,
transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue); transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
this.WriteExtension(extension, stream); this.WriteExtension(extension, stream);
@ -684,14 +752,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <param name="rectangle">The frame location and size.</param> /// <param name="rectangle">The frame location and size.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param> /// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, Stream stream) private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream)
{ {
byte packedValue = GifImageDescriptor.GetPackedValue( byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable, localColorTableFlag: hasColorTable,
interfaceFlag: false, interfaceFlag: false,
sortFlag: false, sortFlag: false,
localColorTableSize: this.bitDepth - 1); localColorTableSize: bitDepth - 1);
GifImageDescriptor descriptor = new( GifImageDescriptor descriptor = new(
left: (ushort)rectangle.X, left: (ushort)rectangle.X,
@ -711,12 +780,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, Stream stream) private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, int bitDepth, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The maximum number of colors for the bit depth // The maximum number of colors for the bit depth
int colorTableLength = ColorNumerics.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>(); int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf<Rgb24>();
using IMemoryOwner<byte> colorTable = this.memoryAllocator.Allocate<byte>(colorTableLength, AllocationOptions.Clean); using IMemoryOwner<byte> colorTable = this.memoryAllocator.Allocate<byte>(colorTableLength, AllocationOptions.Clean);
Span<byte> colorTableSpan = colorTable.GetSpan(); Span<byte> colorTableSpan = colorTable.GetSpan();
@ -735,9 +805,18 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param> /// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageData(Buffer2DRegion<byte> indices, Stream stream) /// <param name="paletteLength">The length of the frame color palette.</param>
/// <param name="transparencyIndex">The index of the color used to represent transparency.</param>
private void WriteImageData(Buffer2DRegion<byte> 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); encoder.Encode(indices, stream);
} }
} }

10
src/ImageSharp/ImageFrame{TPixel}.cs

@ -21,6 +21,16 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
{ {
private bool isDisposed; private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="size">The <see cref="Size"/> of the frame.</param>
internal ImageFrame(Configuration configuration, Size size)
: this(configuration, size.Width, size.Height, new ImageFrameMetadata())
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class. /// Initializes a new instance of the <see cref="ImageFrame{TPixel}" /> class.
/// </summary> /// </summary>

55
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <para> /// <para>
/// 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. /// Doing so will result in non-idempotent results.
/// </para> /// </para>
internal sealed class EuclideanPixelMap<TPixel> : IDisposable internal sealed class EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private Rgba32[] rgbaPalette; private Rgba32[] rgbaPalette;
private int transparentIndex;
/// <summary> /// <summary>
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls. /// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
@ -34,12 +36,24 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param> /// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
: this(configuration, palette, -1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int transparentIndex = -1)
{ {
this.configuration = configuration; this.configuration = configuration;
this.Palette = palette; this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length]; this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator); this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
this.transparentIndex = transparentIndex;
} }
/// <summary> /// <summary>
@ -91,16 +105,43 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
this.cache.Clear(); this.cache.Clear();
} }
/// <summary>
/// Allows setting the transparent index after construction.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index) => this.transparentIndex = index;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{ {
// Loop through the palette and find the nearest match. // Loop through the palette and find the nearest match.
int index = 0; int index = 0;
float leastDistance = float.MaxValue; 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++) for (int i = 0; i < this.rgbaPalette.Length; i++)
{ {
Rgba32 candidate = this.rgbaPalette[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 it's an exact match, exit the loop
if (distance == 0) if (distance == 0)
@ -130,12 +171,12 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="b">The second point.</param> /// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns> /// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [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; float deltaR = a.R - b.R;
int deltaG = a.G - b.G; float deltaG = a.G - b.G;
int deltaB = a.B - b.B; float deltaB = a.B - b.B;
int deltaA = a.A - b.A; float deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
} }

15
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
public class PaletteQuantizer : IQuantizer public class PaletteQuantizer : IQuantizer
{ {
private readonly ReadOnlyMemory<Color> colorPalette; private readonly ReadOnlyMemory<Color> colorPalette;
private readonly int transparentIndex;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class. /// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
@ -27,12 +28,24 @@ public class PaletteQuantizer : IQuantizer
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param> /// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options) public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options)
: this(palette, options, -1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
internal PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options, int transparentIndex)
{ {
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.colorPalette = palette; this.colorPalette = palette;
this.Options = options; this.Options = options;
this.transparentIndex = transparentIndex;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -52,6 +65,6 @@ public class PaletteQuantizer : IQuantizer
// Always use the palette length over options since the palette cannot be reduced. // Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length]; TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(this.colorPalette.Span, palette.AsSpan()); Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette); return new PaletteQuantizer<TPixel>(configuration, options, palette, this.transparentIndex);
} }
} }

17
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -25,18 +25,23 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct. /// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param> /// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="palette">The palette to use.</param> /// <param name="palette">The palette to use.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> palette) public PaletteQuantizer(
Configuration configuration,
QuantizerOptions options,
ReadOnlyMemory<TPixel> palette,
int transparentIndex)
{ {
Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette); this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, transparentIndex);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -59,6 +64,12 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
{ {
} }
/// <summary>
/// Allows setting the transparent index after construction.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match) public readonly byte GetQuantizedColor(TPixel color, out TPixel match)

2
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -38,7 +38,7 @@ public class GifEncoderTests
[Theory] [Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, true)]
public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, bool limitAllocationBuffer) public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, bool limitAllocationBuffer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {

Loading…
Cancel
Save