Browse Source

Simplify, fix color mapping, and refactor for performance.

af/qhack03
James Jackson-South 6 years ago
parent
commit
fb4c47413b
  1. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  2. 24
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 19
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  5. 3
      src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
  6. 17
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  7. 9
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  8. 68
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  9. 12
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
  10. 3
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  11. 30
      src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
  12. 26
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  13. 53
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  14. 14
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  15. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  16. 4
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  17. 10
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  18. 18
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  19. 2
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  20. 8
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

6
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -336,10 +336,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette) private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration); using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds()); using QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette; ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32); var color = default(Rgba32);
// TODO: Use bulk conversion here for better perf // TODO: Use bulk conversion here for better perf

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

@ -128,6 +128,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream) private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// 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.
EuclideanPixelMap<TPixel> pixelMap = default;
bool pixelMapSet = false;
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
@ -142,7 +147,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else else
{ {
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette); if (!pixelMapSet)
{
pixelMapSet = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette, quantized.Palette.Span.Length);
}
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); using QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream); this.WriteImageData(paletteQuantized, stream);
} }
@ -214,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan(); Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan); PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--) for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{ {
@ -321,8 +332,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
return; return;
} }
foreach (string comment in metadata.Comments) for (var i = 0; i < metadata.Comments.Count; i++)
{ {
string comment = metadata.Comments[i];
this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel; this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2); stream.Write(this.buffer, 0, 2);
@ -330,7 +342,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Comment will be stored in chunks of 255 bytes, if it exceeds this size. // Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan(); ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0; int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength) for (;
idx <= comment.Length - GifConstants.MaxCommentSubBlockLength;
idx += GifConstants.MaxCommentSubBlockLength)
{ {
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength); WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
} }
@ -443,7 +457,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength); using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
PixelOperations<TPixel>.Instance.ToRgb24Bytes( PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration, this.configuration,
image.Palette, image.Palette.Span,
colorTable.GetSpan(), colorTable.GetSpan(),
pixelCount); pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength); stream.Write(colorTable.Array, 0, colorTableLength);

2
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -555,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png
} }
// Grab the palette and write it to the stream. // Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette; ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256); int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3; int colorTableLength = paletteLength * 3;
bool anyAlpha = false; bool anyAlpha = false;

19
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -95,20 +96,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ReadOnlySpan<TPixel> paletteSpan = destination.Palette;
int offsetY = bounds.Top; int offsetY = bounds.Top;
int offsetX = bounds.Left; int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale; float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
Span<byte> destinationRow = destination.GetPixelRowSpan(y - offsetY); ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = sourceRow[x]; TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
destinationRow[x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
} }
} }
@ -124,16 +124,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
float scale = processor.DitherScale; float scale = processor.DitherScale;
ReadOnlySpan<TPixel> palette = processor.Palette.Span;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = bounds.Left; x < bounds.Right; x++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = row[x]; ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel, palette); TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
row[x] = transformed; sourcePixel = transformed;
} }
} }
} }

3
src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs

@ -32,8 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Returns the color from the dithering palette corresponding to the given color. /// Returns the color from the dithering palette corresponding to the given color.
/// </summary> /// </summary>
/// <param name="color">The color to match.</param> /// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <returns>The <typeparamref name="TPixel"/> match.</returns> /// <returns>The <typeparamref name="TPixel"/> match.</returns>
TPixel GetPaletteColor(TPixel color, ReadOnlySpan<TPixel> palette); TPixel GetPaletteColor(TPixel color);
} }
} }

17
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -224,20 +225,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale; float scale = this.quantizer.Options.DitherScale;
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
Span<byte> destinationRow = this.destination.GetPixelRowSpan(y - offsetY); ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetPixelRowSpan(y - offsetY));
for (int x = this.bounds.Left; x < this.bounds.Right; x++) for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{ {
TPixel dithered = this.dither.Dither(sourceRow[x], x, y, this.bitDepth, scale); TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, paletteSpan, out TPixel _); Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
} }
} }
} }
@ -272,16 +272,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.processor.Palette.Span;
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> row = this.source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
for (int x = this.bounds.Left; x < this.bounds.Right; x++) for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{ {
ref TPixel sourcePixel = ref row[x]; ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale); TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered, paletteSpan); sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
} }
} }
} }

9
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -39,6 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.ditherProcessor = new DitherProcessor( this.ditherProcessor = new DitherProcessor(
this.Configuration, this.Configuration,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
this.paletteMemory.Memory, this.paletteMemory.Memory,
definition.DitherScale); definition.DitherScale);
} }
@ -71,7 +73,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <summary> /// <summary>
/// Used to allow inlining of calls to /// Used to allow inlining of calls to
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel, ReadOnlySpan{TPixel})"/>. /// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary> /// </summary>
private readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel> private readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
{ {
@ -80,11 +82,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public DitherProcessor( public DitherProcessor(
Configuration configuration, Configuration configuration,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette, ReadOnlyMemory<TPixel> palette,
float ditherScale) float ditherScale)
{ {
this.Configuration = configuration; this.Configuration = configuration;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette); this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, palette.Span.Length);
this.Palette = palette; this.Palette = palette;
this.DitherScale = ditherScale; this.DitherScale = ditherScale;
} }
@ -96,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public float DitherScale { get; } public float DitherScale { get; }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public TPixel GetPaletteColor(TPixel color, ReadOnlySpan<TPixel> palette) public TPixel GetPaletteColor(TPixel color)
{ {
this.pixelMap.GetClosestColor(color, out TPixel match); this.pixelMap.GetClosestColor(color, out TPixel match);
return match; return match;

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

@ -2,88 +2,86 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Numerics; 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.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
/// <summary> /// <summary>
/// Gets the closest color to the supplied color based upon the Euclidean distance. /// Gets the closest color to the supplied color based upon the Euclidean distance.
/// TODO: Expose this somehow.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>> internal readonly struct EuclideanPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly Vector4[] vectorCache; private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache; private readonly ConcurrentDictionary<TPixel, int> distanceCache;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int length;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct. /// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary> /// </summary>
/// <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>
/// <param name="length">The length of the color palette.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int length)
{ {
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); this.palette = palette;
this.length = length;
this.Palette = palette; ReadOnlySpan<TPixel> paletteSpan = this.palette.Span.Slice(0, this.length);
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; this.vectorCache = new Vector4[length];
this.vectorCache = new Vector4[paletteSpan.Length];
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
// Use the same rules across all target frameworks.
this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
PixelOperations<TPixel>.Instance.ToVector4(configuration, paletteSpan, this.vectorCache); PixelOperations<TPixel>.Instance.ToVector4(configuration, paletteSpan, this.vectorCache);
} }
/// <inheritdoc/> /// <summary>
public ReadOnlyMemory<TPixel> Palette /// Returns the palette span.
{ /// </summary>
[MethodImpl(InliningOptions.ShortMethod)] /// <returns>The <seealso cref="ReadOnlySpan{TPixel}"/>.</returns>
get; [MethodImpl(InliningOptions.ShortMethod)]
} public ReadOnlySpan<TPixel> GetPaletteSpan() => this.palette.Span.Slice(0, this.length);
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map);
/// <inheritdoc/>
public bool Equals(EuclideanPixelMap<TPixel> other)
=> this.Palette.Equals(other.Palette);
/// <inheritdoc/> /// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// The palette contents must match the one used in the constructor.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match) public int GetClosestColor(TPixel color, out TPixel match)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.GetPaletteSpan());
// Check if the color is in the lookup table // Check if the color is in the lookup table
if (!this.distanceCache.TryGetValue(color, out int index)) if (!this.distanceCache.TryGetValue(color, out int index))
{ {
return this.GetClosestColorSlow(color, paletteSpan, out match); return this.GetClosestColorSlow(color, ref paletteRef, out match);
} }
match = paletteSpan[index]; match = Unsafe.Add(ref paletteRef, index);
return index; return index;
} }
/// <inheritdoc/>
public override int GetHashCode() => this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) private int GetClosestColorSlow(TPixel color, 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;
Vector4 vector = color.ToScaledVector4(); var vector = color.ToVector4();
ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette);
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache); ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < palette.Length; i++) for (int i = 0; i < this.length; i++)
{ {
Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i); Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate); float distance = Vector4.DistanceSquared(vector, candidate);
@ -108,5 +106,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
match = Unsafe.Add(ref paletteRef, index); match = Unsafe.Add(ref paletteRef, index);
return index; return index;
} }
} }
} }

12
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -40,20 +41,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<TPixel> palette = quantizer.BuildPalette(source, interest); ReadOnlySpan<TPixel> palette = quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette); var destination = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
if (quantizer.Options.Dither is null) if (quantizer.Options.Dither is null)
{ {
SecondPass(ref quantizer, source, quantizedFrame, interest); SecondPass(ref quantizer, source, destination, interest);
} }
else else
{ {
// We clone the image as we don't want to alter the original via error diffusion based dithering. // We clone the image as we don't want to alter the original via error diffusion based dithering.
using ImageFrame<TPixel> clone = source.Clone(); using ImageFrame<TPixel> clone = source.Clone();
SecondPass(ref quantizer, clone, quantizedFrame, interest); SecondPass(ref quantizer, clone, destination, interest);
} }
return quantizedFrame; return destination;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
@ -106,7 +107,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
for (int x = this.bounds.Left; x < this.bounds.Right; x++) for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{ {
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _); destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
} }
} }
} }

3
src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs

@ -45,10 +45,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Returns the index and color from the quantized palette corresponding to the given color. /// Returns the index and color from the quantized palette corresponding to the given color.
/// </summary> /// </summary>
/// <param name="color">The color to match.</param> /// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param> /// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns> /// <returns>The <see cref="byte"/> index.</returns>
byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match); byte GetQuantizedColor(TPixel color, out TPixel match);
// TODO: Enable bulk operations. // TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches); // void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);

30
src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs

@ -1,30 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Allows the mapping of input colors to colors within a given palette.
/// TODO: Expose this somehow.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal interface IPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets the color palette containing colors to match.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
int GetClosestColor(TPixel color, out TPixel match);
}
}

26
src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -82,29 +83,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
} }
Span<TPixel> paletteSpan = this.palette.GetSpan(); Span<TPixel> paletteSpan = this.palette.GetSpan();
this.octree.Palletize(paletteSpan, this.colors); int paletteIndex = 0;
this.octree.Palletize(paletteSpan, this.colors, ref paletteIndex);
// TODO: Cannot make method readonly due to this line. // Length of reduced palette + transparency.
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory); paletteSpan = paletteSpan.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors));
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory, paletteSpan.Length);
return paletteSpan; return paletteSpan;
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{ {
// Octree only maps the RGB component of a color // Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent // so cannot tell the difference between a fully transparent
// pixel and a black one. // pixel and a black one.
if (!this.isDithering && !color.Equals(default)) if (this.isDithering || color.Equals(default))
{ {
var index = (byte)this.octree.GetPaletteIndex(color); return (byte)this.pixelMap.GetClosestColor(color, out match);
match = palette[index];
return index;
} }
return (byte)this.pixelMap.GetClosestColor(color, out match); ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan());
var index = (byte)this.octree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, index);
return index;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -223,15 +227,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
/// <param name="palette">The palette to fill.</param> /// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param> /// <param name="colorCount">The maximum number of colors</param>
/// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Palletize(Span<TPixel> palette, int colorCount) public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{ {
while (this.Leaves > colorCount - 1) while (this.Leaves > colorCount - 1)
{ {
this.Reduce(); this.Reduce();
} }
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex); this.root.ConstructPalette(palette, ref paletteIndex);
} }

53
src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -17,54 +18,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel> internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private IMemoryOwner<TPixel> paletteOwner;
private readonly EuclideanPixelMap<TPixel> pixelMap; private readonly EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct. /// Initializes a new instance of the <see cref="PaletteFrameQuantizer{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 behaviour 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="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param> /// <param name="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan<Color> colors) public PaletteFrameQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
{ {
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 = pixelMap;
int maxLength = Math.Min(colors.Length, options.MaxColors);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(maxLength);
Color.ToPixel(configuration, colors, this.paletteOwner.GetSpan());
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, this.paletteOwner.Memory);
this.isDisposed = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="palette">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan<TPixel> palette)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
int maxLength = Math.Min(palette.Length, options.MaxColors);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(maxLength);
palette.CopyTo(this.paletteOwner.GetSpan());
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, this.paletteOwner.Memory);
this.isDisposed = false;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -81,24 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public readonly ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public readonly ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.paletteOwner.GetSpan(); => this.pixelMap.GetPaletteSpan();
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match); => (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
} }
} }
} }

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

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public class PaletteQuantizer : IQuantizer public class PaletteQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
private readonly ReadOnlyMemory<Color> palette; private readonly ReadOnlyMemory<Color> colorPalette;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class. /// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
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.palette = palette; this.colorPalette = palette;
this.Options = options; this.Options = options;
} }
@ -50,7 +50,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
return new PaletteFrameQuantizer<TPixel>(configuration, options, this.palette.Span);
// 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.
int length = Math.Min(this.colorPalette.Length, options.MaxColors);
var palette = new TPixel[length];
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
var pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, length);
return new PaletteFrameQuantizer<TPixel>(configuration, options, pixelMap);
} }
} }
} }

2
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public void Invoke(in RowInterval rows) public void Invoke(in RowInterval rows)
{ {
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan(); ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette; ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
int width = this.bounds.Width; int width = this.bounds.Width;

4
src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs

@ -53,10 +53,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>. /// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary> /// </summary>
public ReadOnlySpan<TPixel> Palette public ReadOnlyMemory<TPixel> Palette
{ {
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
get { return this.palette.GetSpan(); } get { return this.palette.Memory; }
} }
/// <summary> /// <summary>

10
src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -131,13 +132,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
} }
} }
// TODO: Cannot make methods readonly due to this line. paletteSpan = paletteSpan.Slice(0, this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory); this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory, paletteSpan.Length);
return paletteSpan; return paletteSpan;
} }
/// <inheritdoc/> /// <inheritdoc/>
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match) public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{ {
if (this.isDithering) if (this.isDithering)
{ {
@ -154,7 +155,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<byte> tagSpan = this.tag.GetSpan(); ReadOnlySpan<byte> tagSpan = this.tag.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
match = palette[index]; ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan());
match = Unsafe.Add(ref paletteRef, index);
return index; return index;
} }

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

@ -5,6 +5,7 @@ using System.IO;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit; using Xunit;
@ -25,6 +26,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
{ TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio } { TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
}; };
[Theory]
[WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32)]
public void EncodeAllocationCheck<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
GifEncoder encoder = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (Image<TPixel> image = provider.GetImage())
{
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder);
}
}
[Theory] [Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)]

2
tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests
// Transparent pixels are much more likely to be found at the end of a palette // Transparent pixels are much more likely to be found at the end of a palette
int index = -1; int index = -1;
Rgba32 trans = default; Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette; ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--) for (int i = paletteSpan.Length - 1; i >= 0; i--)
{ {
paletteSpan[i].ToRgba32(ref trans); paletteSpan[i].ToRgba32(ref trans);

8
tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length); Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(Color.Black, (Color)result.Palette[0]); Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]); Assert.Equal(0, result.GetPixelSpan()[0]);
} }
@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length); Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(default, result.Palette[0]); Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]); Assert.Equal(0, result.GetPixelSpan()[0]);
} }
@ -92,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
var actualImage = new Image<Rgba32>(1, 256); var actualImage = new Image<Rgba32>(1, 256);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette; ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1; int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++) for (int y = 0; y < actualImage.Height; y++)
{ {
@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length); Assert.Equal(256, result.GetPixelSpan().Length);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette; ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1; int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++) for (int y = 0; y < actualImage.Height; y++)
{ {

Loading…
Cancel
Save