Browse Source

Can now encode gifs with global palette

pull/637/head
James Jackson-South 8 years ago
parent
commit
b208be83e0
  1. 5
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  2. 113
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 5
      src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs
  4. 39
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  5. 29
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 21
      src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs
  7. 25
      src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs
  8. 21
      src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs
  9. 3
      src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs
  10. 18
      src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs
  11. 28
      src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs
  12. 32
      src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs
  13. 6
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

5
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); public IQuantizer Quantizer { get; set; } = new OctreeQuantizer();
/// <summary>
/// Gets or sets the color table mode: Global or local.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>

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

@ -19,6 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
internal sealed class GifEncoderCore internal sealed class GifEncoderCore
{ {
/// <summary>
/// Used for allocating memory during procesing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary> /// <summary>
@ -27,15 +30,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
private readonly byte[] buffer = new byte[20]; private readonly byte[] buffer = new byte[20];
/// <summary> /// <summary>
/// Gets the text encoding used to write comments. /// The text encoding used to write comments.
/// </summary> /// </summary>
private readonly Encoding textEncoding; private readonly Encoding textEncoding;
/// <summary> /// <summary>
/// Gets or sets the quantizer used to generate the color palette. /// The quantizer used to generate the color palette.
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private readonly IQuantizer quantizer;
/// <summary>
/// The color table mode: Global or local.
/// </summary>
private readonly GifColorTableMode colorTableMode;
/// <summary> /// <summary>
/// A flag indicating whether to ingore the metadata when writing the image. /// A flag indicating whether to ingore the metadata when writing the image.
/// </summary> /// </summary>
@ -56,6 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer; this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
this.ignoreMetadata = options.IgnoreMetadata; this.ignoreMetadata = options.IgnoreMetadata;
} }
@ -72,28 +81,80 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
// Quantize the image returning a palette. // Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); QuantizedFrame<TPixel> quantized =
this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
// Get the number of bits. // Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
int index = this.GetTransparentIndex(quantized);
// Write the header. // Write the header.
this.WriteHeader(stream); this.WriteHeader(stream);
// Write the LSD. We'll use local color tables for now. // Write the LSD.
this.WriteLogicalScreenDescriptor(image, stream, index); int index = this.GetTransparentIndex(quantized);
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream);
if (useGlobalTable)
{
this.WriteColorTable(quantized, stream);
}
// Write the first frame. // Write the comments.
this.WriteComments(image.MetaData, stream); this.WriteComments(image.MetaData, stream);
// Write additional frames. // Write application extension to allow additional frames.
if (image.Frames.Count > 1) if (image.Frames.Count > 1)
{ {
this.WriteApplicationExtension(stream, image.MetaData.RepeatCount); this.WriteApplicationExtension(stream, image.MetaData.RepeatCount);
} }
if (useGlobalTable)
{
this.EncodeGlobal(image, quantized, index, stream);
}
else
{
this.EncodeLocal(image, quantized, stream);
}
// Clean up.
quantized?.Dispose();
quantized = null;
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
var palleteQuantizer = new PaletteQuantizer(this.quantizer.Diffuser);
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream);
if (i == 0)
{
this.WriteImageData(quantized, stream);
}
else
{
using (QuantizedFrame<TPixel> paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
{
this.WriteImageData(paletteQuantized, stream);
}
}
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
if (quantized == null) if (quantized == null)
@ -101,16 +162,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame); quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
} }
this.WriteGraphicalControlExtension(frame.MetaData, stream, this.GetTransparentIndex(quantized)); this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, stream); this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream); this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized = null; // So next frame can regenerate it quantized = null; // So next frame can regenerate it
} }
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
} }
/// <summary> /// <summary>
@ -159,12 +218,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to encode.</param> /// <param name="image">The image to encode.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param> /// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, Stream stream, int transparencyIndex) /// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, int transparencyIndex, bool useGlobalTable, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(false, this.bitDepth - 1, false, this.bitDepth - 1); byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
var descriptor = new GifLogicalScreenDescriptor( var descriptor = new GifLogicalScreenDescriptor(
width: (ushort)image.Width, width: (ushort)image.Width,
@ -243,9 +303,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the graphics control extension to the stream. /// Writes the graphics control extension to the stream.
/// </summary> /// </summary>
/// <param name="metaData">The metadata of the image or frame.</param> /// <param name="metaData">The metadata of the image or frame.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param> /// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, Stream stream, int transparencyIndex) /// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, int transparencyIndex, Stream stream)
{ {
byte packedValue = GifGraphicControlExtension.GetPackedValue( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metaData.DisposalMethod, disposalMethod: metaData.DisposalMethod,
@ -253,8 +313,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
var extension = new GifGraphicControlExtension( var extension = new GifGraphicControlExtension(
packed: packedValue, packed: packedValue,
transparencyIndex: unchecked((byte)transparencyIndex), delayTime: (ushort)metaData.FrameDelay,
delayTime: (ushort)metaData.FrameDelay); transparencyIndex: unchecked((byte)transparencyIndex));
this.WriteExtension(extension, stream); this.WriteExtension(extension, stream);
} }
@ -281,15 +341,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </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 be encoded.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, Stream stream) private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
byte packedValue = GifImageDescriptor.GetPackedValue( byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: true, localColorTableFlag: hasColorTable,
interfaceFlag: false, interfaceFlag: false,
sortFlag: false, sortFlag: false,
localColorTableSize: (byte)this.bitDepth); // Note: we subtract 1 from the colorTableSize writing localColorTableSize: (byte)(this.bitDepth - 1)); // Note: we subtract 1 from the colorTableSize writing
var descriptor = new GifImageDescriptor( var descriptor = new GifImageDescriptor(
left: 0, left: 0,
@ -342,9 +403,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream) private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (var encoder = new LzwEncoder(this.memoryAllocator, image.Pixels, (byte)this.bitDepth)) using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{ {
encoder.Encode(stream); encoder.Encode(image.GetPixelSpan(), stream);
} }
} }
} }

5
src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs

@ -25,5 +25,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets the quantizer used to generate the color palette. /// Gets the quantizer used to generate the color palette.
/// </summary> /// </summary>
IQuantizer Quantizer { get; } IQuantizer Quantizer { get; }
/// <summary>
/// Gets the color table mode: Global or local.
/// </summary>
GifColorTableMode ColorTableMode { get; }
} }
} }

39
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -58,11 +58,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private const int MaxMaxCode = 1 << MaxBits; private const int MaxMaxCode = 1 << MaxBits;
/// <summary>
/// The working pixel array.
/// </summary>
private readonly byte[] pixelArray;
/// <summary> /// <summary>
/// The initial code size. /// The initial code size.
/// </summary> /// </summary>
@ -83,6 +78,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private readonly byte[] accumulators = new byte[256]; private readonly byte[] accumulators = new byte[256];
/// <summary>
/// For dynamic table sizing
/// </summary>
private readonly int hsize = HashSize;
/// <summary> /// <summary>
/// The current position within the pixelArray. /// The current position within the pixelArray.
/// </summary> /// </summary>
@ -98,11 +98,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private int maxCode; private int maxCode;
/// <summary>
/// For dynamic table sizing
/// </summary>
private int hsize = HashSize;
/// <summary> /// <summary>
/// First unused entry /// First unused entry
/// </summary> /// </summary>
@ -169,13 +164,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Initializes a new instance of the <see cref="LzwEncoder"/> class. /// Initializes a new instance of the <see cref="LzwEncoder"/> class.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param> /// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
/// <param name="indexedPixels">The array of indexed pixels.</param>
/// <param name="colorDepth">The color depth in bits.</param> /// <param name="colorDepth">The color depth in bits.</param>
public LzwEncoder(MemoryAllocator memoryAllocator, byte[] indexedPixels, int colorDepth) public LzwEncoder(MemoryAllocator memoryAllocator, int colorDepth)
{ {
this.pixelArray = indexedPixels;
this.initialCodeSize = Math.Max(2, colorDepth); this.initialCodeSize = Math.Max(2, colorDepth);
this.hashTable = memoryAllocator.Allocate<int>(HashSize, true); this.hashTable = memoryAllocator.Allocate<int>(HashSize, true);
this.codeTable = memoryAllocator.Allocate<int>(HashSize, true); this.codeTable = memoryAllocator.Allocate<int>(HashSize, true);
} }
@ -183,8 +175,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Encodes and compresses the indexed pixels to the stream. /// Encodes and compresses the indexed pixels to the stream.
/// </summary> /// </summary>
/// <param name="indexedPixels">The span of indexed pixels.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
public void Encode(Stream stream) public void Encode(Span<byte> indexedPixels, Stream stream)
{ {
// Write "initial code size" byte // Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize); stream.WriteByte((byte)this.initialCodeSize);
@ -192,7 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.position = 0; this.position = 0;
// Compress and write the pixel data // Compress and write the pixel data
this.Compress(this.initialCodeSize + 1, stream); this.Compress(indexedPixels, this.initialCodeSize + 1, stream);
// Write block terminator // Write block terminator
stream.WriteByte(GifConstants.Terminator); stream.WriteByte(GifConstants.Terminator);
@ -252,9 +245,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Compress the packets to the stream. /// Compress the packets to the stream.
/// </summary> /// </summary>
/// <param name="indexedPixels">The span of indexed pixels.</param>
/// <param name="intialBits">The initial bits.</param> /// <param name="intialBits">The initial bits.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void Compress(int intialBits, Stream stream) private void Compress(Span<byte> indexedPixels, int intialBits, Stream stream)
{ {
int fcode; int fcode;
int c; int c;
@ -276,7 +270,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.accumulatorCount = 0; // clear packet this.accumulatorCount = 0; // clear packet
ent = this.NextPixel(); ent = this.NextPixel(indexedPixels);
// TODO: PERF: It looks likt hshift could be calculated once statically. // TODO: PERF: It looks likt hshift could be calculated once statically.
hshift = 0; hshift = 0;
@ -296,9 +290,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.GetSpan()); ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.GetSpan());
ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.GetSpan()); ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.GetSpan());
while (this.position < this.pixelArray.Length) while (this.position < indexedPixels.Length)
{ {
c = this.NextPixel(); c = this.NextPixel(indexedPixels);
fcode = (c << MaxBits) + ent; fcode = (c << MaxBits) + ent;
int i = (c << hshift) ^ ent /* = 0 */; int i = (c << hshift) ^ ent /* = 0 */;
@ -373,13 +367,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Reads the next pixel from the image. /// Reads the next pixel from the image.
/// </summary> /// </summary>
/// <param name="indexedPixels">The span of indexed pixels.</param>
/// <returns> /// <returns>
/// The <see cref="int"/> /// The <see cref="int"/>
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private int NextPixel() private int NextPixel(Span<byte> indexedPixels)
{ {
return this.pixelArray[this.position++] & 0xff; return indexedPixels[this.position++] & 0xff;
} }
/// <summary> /// <summary>

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

@ -86,11 +86,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
private readonly bool writeGamma; private readonly bool writeGamma;
/// <summary>
/// Contains the raw pixel data from an indexed image.
/// </summary>
private byte[] palettePixelData;
/// <summary> /// <summary>
/// The image width. /// The image width.
/// </summary> /// </summary>
@ -188,11 +183,12 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
QuantizedFrame<TPixel> quantized = null; QuantizedFrame<TPixel> quantized = null;
ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette) if (this.pngColorType == PngColorType.Palette)
{ {
// Create quantized frame returning the palette and set the bit depth. // Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
this.palettePixelData = quantized.Pixels; quantizedPixelsSpan = quantized.GetPixelSpan();
byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
@ -233,9 +229,11 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePhysicalChunk(stream, image); this.WritePhysicalChunk(stream, image);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
this.WriteDataChunks(image.Frames.RootFrame, stream); this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream);
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
quantized?.Dispose();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -384,9 +382,10 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The row span.</param> /// <param name="rowSpan">The row span.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns> /// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, int row) private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, ReadOnlySpan<byte> quantizedPixelsSpan, int row)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
switch (this.pngColorType) switch (this.pngColorType)
@ -394,7 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Png
case PngColorType.Palette: case PngColorType.Palette:
int stride = this.rawScanline.Length(); int stride = this.rawScanline.Length();
this.palettePixelData.AsSpan(row * stride, stride).CopyTo(this.rawScanline.GetSpan()); quantizedPixelsSpan.Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
break; break;
case PngColorType.Grayscale: case PngColorType.Grayscale:
@ -555,10 +554,11 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
Span<byte> colorTableSpan = colorTable.GetSpan(); Span<byte> colorTableSpan = colorTable.GetSpan();
Span<byte> alphaTableSpan = alphaTable.GetSpan(); Span<byte> alphaTableSpan = alphaTable.GetSpan();
Span<byte> quantizedSpan = quantized.GetPixelSpan();
for (byte i = 0; i < pixelCount; i++) for (byte i = 0; i < pixelCount; i++)
{ {
if (quantized.Pixels.Contains(i)) if (quantizedSpan.IndexOf(i) > -1)
{ {
int offset = i * 3; int offset = i * 3;
palette[i].ToRgba32(ref rgba); palette[i].ToRgba32(ref rgba);
@ -571,10 +571,10 @@ namespace SixLabors.ImageSharp.Formats.Png
if (alpha > this.threshold) if (alpha > this.threshold)
{ {
alpha = 255; alpha = byte.MaxValue;
} }
anyAlpha = anyAlpha || alpha < 255; anyAlpha = anyAlpha || alpha < byte.MaxValue;
alphaTableSpan[i] = alpha; alphaTableSpan[i] = alpha;
} }
} }
@ -635,8 +635,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param> /// <param name="pixels">The image.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, Stream stream) private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
this.bytesPerScanline = this.width * this.bytesPerPixel; this.bytesPerScanline = this.width * this.bytesPerPixel;
@ -688,7 +689,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
for (int y = 0; y < this.height; y++) for (int y = 0; y < this.height; y++)
{ {
IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), y); IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y);
deflateStream.Write(r.Array, 0, resultLength); deflateStream.Write(r.Array, 0, resultLength);
IManagedByteBuffer temp = this.rawScanline; IManagedByteBuffer temp = this.rawScanline;

21
src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs

@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
/// <remarks> /// <remarks>
/// If you construct this class with a true value for singlePass, then the code will, when quantizing your image, /// If you construct this class with a true value for singlePass, then the code will, when quantizing your image,
/// only call the <see cref="FirstPass(ImageFrame{TPixel}, int, int)"/> methods. /// only call the <see cref="FirstPass(ImageFrame{TPixel}, int, int)"/> methods.
/// If two passes are required, the code will also call <see cref="SecondPass(ImageFrame{TPixel}, byte[], int, int)"/> /// If two passes are required, the code will also call <see cref="SecondPass(ImageFrame{TPixel}, Span{byte}, int, int)"/>
/// and then 'QuantizeImage'. /// and then 'QuantizeImage'.
/// </remarks> /// </remarks>
protected FrameQuantizerBase(IQuantizer quantizer, bool singlePass) protected FrameQuantizerBase(IQuantizer quantizer, bool singlePass)
@ -58,7 +58,6 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
// Get the size of the source image // Get the size of the source image
int height = image.Height; int height = image.Height;
int width = image.Width; int width = image.Width;
byte[] quantizedPixels = new byte[width * height];
// Call the FirstPass function if not a single pass algorithm. // Call the FirstPass function if not a single pass algorithm.
// For something like an Octree quantizer, this will run through // For something like an Octree quantizer, this will run through
@ -69,22 +68,22 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
} }
// Collect the palette. Required before the second pass runs. // Collect the palette. Required before the second pass runs.
TPixel[] colorPalette = this.GetPalette(); var quantizedFrame = new QuantizedFrame<TPixel>(image.MemoryAllocator, width, height, this.GetPalette());
if (this.Dither) if (this.Dither)
{ {
// We clone the image as we don't want to alter the original. // We clone the image as we don't want to alter the original.
using (ImageFrame<TPixel> clone = image.Clone()) using (ImageFrame<TPixel> clone = image.Clone())
{ {
this.SecondPass(clone, quantizedPixels, width, height); this.SecondPass(clone, quantizedFrame.GetPixelSpan(), width, height);
} }
} }
else else
{ {
this.SecondPass(image, quantizedPixels, width, height); this.SecondPass(image, quantizedFrame.GetPixelSpan(), width, height);
} }
return new QuantizedFrame<TPixel>(width, height, colorPalette, quantizedPixels); return quantizedFrame;
} }
/// <summary> /// <summary>
@ -104,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
/// <param name="output">The output pixel array</param> /// <param name="output">The output pixel array</param>
/// <param name="width">The width in pixels of the image</param> /// <param name="width">The width in pixels of the image</param>
/// <param name="height">The height in pixels of the image</param> /// <param name="height">The height in pixels of the image</param>
protected abstract void SecondPass(ImageFrame<TPixel> source, byte[] output, int width, int height); protected abstract void SecondPass(ImageFrame<TPixel> source, Span<byte> output, int width, int height);
/// <summary> /// <summary>
/// Retrieve the palette for the quantized image. /// Retrieve the palette for the quantized image.
@ -131,7 +130,13 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
return cache[pixel]; return cache[pixel];
} }
// Not found - loop through the palette and find the nearest match. return this.GetClosestPixelSlow(pixel, colorPalette, cache);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private byte GetClosestPixelSlow(TPixel pixel, TPixel[] colorPalette, Dictionary<TPixel, byte> cache)
{
// Loop through the palette and find the nearest match.
byte colorIndex = 0; byte colorIndex = 0;
float leastDistance = int.MaxValue; float leastDistance = int.MaxValue;
var vector = pixel.ToVector4(); var vector = pixel.ToVector4();

25
src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void SecondPass(ImageFrame<TPixel> source, byte[] output, int width, int height) protected override void SecondPass(ImageFrame<TPixel> source, Span<byte> output, int width, int height)
{ {
// Load up the values for the first pixel. We can use these to speed up the second // Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color. // pass of the algorithm by avoiding transforming rows of identical color.
@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
{ {
this.palette[i].ToRgba32(ref trans); this.palette[i].ToRgba32(ref trans);
if (trans.Equals(default(Rgba32))) if (trans.Equals(default))
{ {
index = i; index = i;
} }
@ -185,7 +185,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
} }
pixel.ToRgba32(ref rgba); pixel.ToRgba32(ref rgba);
if (rgba.Equals(default(Rgba32))) if (rgba.Equals(default))
{ {
return this.transparentIndex; return this.transparentIndex;
} }
@ -255,7 +255,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
this.Leaves = 0; this.Leaves = 0;
this.reducibleNodes = new OctreeNode[9]; this.reducibleNodes = new OctreeNode[9];
this.root = new OctreeNode(0, this.maxColorBits, this); this.root = new OctreeNode(0, this.maxColorBits, this);
this.previousColor = default(TPixel); this.previousColor = default;
this.previousNode = null; this.previousNode = null;
} }
@ -476,9 +476,9 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
int shift = 7 - level; int shift = 7 - level;
pixel.ToRgba32(ref rgba); pixel.ToRgba32(ref rgba);
int index = ((rgba.B & Mask[level]) >> (shift - 2)) | int index = ((rgba.B & Mask[level]) >> (shift - 2))
((rgba.G & Mask[level]) >> (shift - 1)) | | ((rgba.G & Mask[level]) >> (shift - 1))
((rgba.R & Mask[level]) >> shift); | ((rgba.R & Mask[level]) >> shift);
OctreeNode child = this.children[index]; OctreeNode child = this.children[index];
@ -551,10 +551,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
// Loop through children looking for leaves // Loop through children looking for leaves
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
{ {
if (this.children[i] != null) this.children[i]?.ConstructPalette(palette, ref index);
{
this.children[i].ConstructPalette(palette, ref index);
}
} }
} }
} }
@ -577,9 +574,9 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
int shift = 7 - level; int shift = 7 - level;
pixel.ToRgba32(ref rgba); pixel.ToRgba32(ref rgba);
int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2)) | int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2))
((rgba.G & Mask[level]) >> (shift - 1)) | | ((rgba.G & Mask[level]) >> (shift - 1))
((rgba.R & Mask[level]) >> shift); | ((rgba.R & Mask[level]) >> shift);
if (this.children[pixelIndex] != null) if (this.children[pixelIndex] != null)
{ {

21
src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs

@ -24,22 +24,23 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
private readonly Dictionary<TPixel, byte> colorMap = new Dictionary<TPixel, byte>(); private readonly Dictionary<TPixel, byte> colorMap = new Dictionary<TPixel, byte>();
/// <summary> /// <summary>
/// List of all colors in the palette /// List of all colors in the palette.
/// </summary> /// </summary>
private readonly TPixel[] colors; private readonly TPixel[] colors;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> class. /// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> class.
/// </summary> /// </summary>
/// <param name="quantizer">The palette quantizer</param> /// <param name="quantizer">The palette quantizer.</param>
public PaletteFrameQuantizer(PaletteQuantizer quantizer) /// <param name="colors">An array of all colors in the palette.</param>
public PaletteFrameQuantizer(PaletteQuantizer quantizer, TPixel[] colors)
: base(quantizer, true) : base(quantizer, true)
{ {
this.colors = quantizer.GetPalette<TPixel>(); this.colors = colors;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void SecondPass(ImageFrame<TPixel> source, byte[] output, int width, int height) protected override void SecondPass(ImageFrame<TPixel> source, Span<byte> output, int width, int height)
{ {
// Load up the values for the first pixel. We can use these to speed up the second // Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color. // pass of the algorithm by avoiding transforming rows of identical color.
@ -88,10 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override TPixel[] GetPalette() protected override TPixel[] GetPalette() => this.colors;
{
return this.colors;
}
/// <summary> /// <summary>
/// Process the pixel in the second pass of the algorithm /// Process the pixel in the second pass of the algorithm
@ -101,9 +99,6 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
/// The quantized value /// The quantized value
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte QuantizePixel(TPixel pixel) private byte QuantizePixel(TPixel pixel) => this.GetClosestPixel(pixel, this.GetPalette(), this.colorMap);
{
return this.GetClosestPixel(pixel, this.GetPalette(), this.colorMap);
}
} }
} }

3
src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs

@ -251,7 +251,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void SecondPass(ImageFrame<TPixel> source, byte[] output, int width, int height) protected override void SecondPass(ImageFrame<TPixel> source, Span<byte> output, int width, int height)
{ {
// Load up the values for the first pixel. We can use these to speed up the second // Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color. // pass of the algorithm by avoiding transforming rows of identical color.
@ -464,6 +464,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
/// <summary> /// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator) private void Get3DMoments(MemoryAllocator memoryAllocator)
{ {
Span<long> vwtSpan = this.vwt.GetSpan(); Span<long> vwtSpan = this.vwt.GetSpan();

18
src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Dithering; using SixLabors.ImageSharp.Processing.Dithering;
using SixLabors.ImageSharp.Processing.Dithering.ErrorDiffusion; using SixLabors.ImageSharp.Processing.Dithering.ErrorDiffusion;
@ -46,19 +47,20 @@ namespace SixLabors.ImageSharp.Processing.Quantization
/// <inheritdoc /> /// <inheritdoc />
public IErrorDiffuser Diffuser { get; } public IErrorDiffuser Diffuser { get; }
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
=> this.CreateFrameQuantizer(() => NamedColors<TPixel>.WebSafePalette);
/// <summary> /// <summary>
/// Gets the palette to use to quantize the image. /// Gets the palette to use to quantize the image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="T:TPixel[]"/></returns> /// <param name="paletteFunction">The method to return the palette.</param>
public virtual TPixel[] GetPalette<TPixel>() /// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
where TPixel : struct, IPixel<TPixel> public virtual IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Func<TPixel[]> paletteFunction)
=> NamedColors<TPixel>.WebSafePalette;
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> new PaletteFrameQuantizer<TPixel>(this); => new PaletteFrameQuantizer<TPixel>(this, paletteFunction.Invoke());
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
} }

28
src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs

@ -36,22 +36,24 @@ namespace SixLabors.ImageSharp.Processing.Quantization.Processors
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{ {
IFrameQuantizer<TPixel> executor = this.Quantizer.CreateFrameQuantizer<TPixel>(); IFrameQuantizer<TPixel> executor = this.Quantizer.CreateFrameQuantizer<TPixel>();
QuantizedFrame<TPixel> quantized = executor.QuantizeFrame(source); using (QuantizedFrame<TPixel> quantized = executor.QuantizeFrame(source))
int paletteCount = quantized.Palette.Length - 1;
// Not parallel to remove "quantized" closure allocation.
// We can operate directly on the source here as we've already read it to get the
// quantized result
for (int y = 0; y < source.Height; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); int paletteCount = quantized.Palette.Length - 1;
int yy = y * source.Width;
for (int x = 0; x < source.Width; x++) // Not parallel to remove "quantized" closure allocation.
// We can operate directly on the source here as we've already read it to get the
// quantized result
for (int y = 0; y < source.Height; y++)
{ {
int i = x + yy; Span<TPixel> row = source.GetPixelRowSpan(y);
TPixel color = quantized.Palette[Math.Min(paletteCount, quantized.Pixels[i])]; ReadOnlySpan<byte> quantizedPixelSpan = quantized.GetPixelSpan();
row[x] = color; int yy = y * source.Width;
for (int x = 0; x < source.Width; x++)
{
int i = x + yy;
row[x] = quantized.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])];
}
} }
} }
} }

32
src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs

@ -3,39 +3,36 @@
using System; using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
// TODO: Consider pooling the TPixel palette also. For Rgba48+ this would end up on th LOH if 256 colors.
namespace SixLabors.ImageSharp.Processing.Quantization namespace SixLabors.ImageSharp.Processing.Quantization
{ {
/// <summary> /// <summary>
/// Represents a quantized image frame where the pixels indexed by a color palette. /// Represents a quantized image frame where the pixels indexed by a color palette.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
public class QuantizedFrame<TPixel> public class QuantizedFrame<TPixel> : IDisposable
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private IBuffer<byte> pixels;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="QuantizedFrame{TPixel}"/> class. /// Initializes a new instance of the <see cref="QuantizedFrame{TPixel}"/> class.
/// </summary> /// </summary>
/// <param name="memoryAllocator">Used to allocated memory for image processing operations.</param>
/// <param name="width">The image width.</param> /// <param name="width">The image width.</param>
/// <param name="height">The image height.</param> /// <param name="height">The image height.</param>
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
/// <param name="pixels">The quantized pixels.</param> public QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, TPixel[] palette)
public QuantizedFrame(int width, int height, TPixel[] palette, byte[] pixels)
{ {
Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height)); Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.NotNull(palette, nameof(palette));
Guard.NotNull(pixels, nameof(pixels));
if (pixels.Length != width * height)
{
throw new ArgumentException($"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels));
}
this.Width = width; this.Width = width;
this.Height = height; this.Height = height;
this.Palette = palette; this.Palette = palette;
this.Pixels = pixels; this.pixels = memoryAllocator.AllocateCleanManagedByteBuffer(width * height);
} }
/// <summary> /// <summary>
@ -51,11 +48,20 @@ namespace SixLabors.ImageSharp.Processing.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 TPixel[] Palette { get; } public TPixel[] Palette { get; private set; }
/// <summary> /// <summary>
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>. /// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary> /// </summary>
public byte[] Pixels { get; } /// <returns>The <see cref="Span{T}"/></returns>
public Span<byte> GetPixelSpan() => this.pixels.GetSpan();
/// <inheritdoc/>
public void Dispose()
{
this.pixels?.Dispose();
this.pixels = null;
this.Palette = null;
}
} }
} }

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

@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Tests
QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame); QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
int index = this.GetTransparentIndex(quantized); int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.Pixels[0]); Assert.Equal(index, quantized.GetPixelSpan()[0]);
} }
} }
} }
@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Tests
QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame); QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
int index = this.GetTransparentIndex(quantized); int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.Pixels[0]); Assert.Equal(index, quantized.GetPixelSpan()[0]);
} }
} }
} }
@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests
QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame); QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
int index = this.GetTransparentIndex(quantized); int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.Pixels[0]); Assert.Equal(index, quantized.GetPixelSpan()[0]);
} }
} }
} }

Loading…
Cancel
Save