diff --git a/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs b/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs
index 4be3f0079..5b312f4f8 100644
--- a/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs
+++ b/src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs
@@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
}
///
- /// Performs the bulk conversion from into .
+ /// Performs the bulk conversion from into .
///
/// The span to the source colors
/// The span to the destination colors
@@ -435,4 +435,4 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
return this.ToLinearRgb(rgb);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 66a60d533..7d2799503 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -336,8 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette)
where TPixel : unmanaged, IPixel
{
- using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration);
- using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds());
+ using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration);
+ using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
@@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
for (int y = image.Height - 1; y >= 0; y--)
{
- ReadOnlySpan pixelSpan = quantized.GetRowSpan(y);
+ ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y);
stream.Write(pixelSpan);
for (int i = 0; i < this.padding; i++)
diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs
index 978609d7f..53c4c6f3f 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs
@@ -4,6 +4,7 @@
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif
@@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the quantizer for reducing the color count.
/// Defaults to the
///
- public IQuantizer Quantizer { get; set; } = new OctreeQuantizer();
+ public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
///
/// Gets or sets the color table mode: Global or local.
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 58bb29f2e..887540930 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -79,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
- QuantizedFrame quantized;
+ IndexedImageFrame quantized;
using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
// Get the number of bits.
- this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
+ this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
// Write the header.
this.WriteHeader(stream);
@@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
// Clean up.
- quantized?.Dispose();
+ quantized.Dispose();
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}
- private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream)
+ private void EncodeGlobal(Image image, IndexedImageFrame quantized, int transparencyIndex, Stream stream)
where TPixel : unmanaged, IPixel
{
+ // 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 pixelMap = default;
+ bool pixelMapSet = false;
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame frame = image.Frames[i];
@@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
- using (var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette))
- using (QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
+ if (!pixelMapSet)
{
- this.WriteImageData(paletteQuantized, stream);
+ pixelMapSet = true;
+ pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette);
}
+
+ using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, pixelMap);
+ using IndexedImageFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
+ this.WriteImageData(paletteQuantized, stream);
}
}
}
- private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream)
+ private void EncodeLocal(Image image, IndexedImageFrame quantized, Stream stream)
where TPixel : unmanaged, IPixel
{
ImageFrame previousFrame = null;
GifFrameMetadata previousMeta = null;
- foreach (ImageFrame frame in image.Frames)
+ for (int i = 0; i < image.Frames.Count; i++)
{
+ ImageFrame frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
@@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength
};
- using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options))
- {
- quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
- }
+ using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options);
+ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
else
{
- using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration))
- {
- quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
- }
+ using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration);
+ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
- this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
+ this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
- quantized?.Dispose();
+ quantized.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
@@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
///
/// The .
///
- private int GetTransparentIndex(QuantizedFrame quantized)
+ private int GetTransparentIndex(IndexedImageFrame quantized)
where TPixel : unmanaged, IPixel
{
- // 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 length = quantized.Palette.Length;
+ ReadOnlySpan paletteSpan = quantized.Palette.Span;
- using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(length))
- {
- Span rgbaSpan = rgbaBuffer.GetSpan();
- ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
- PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
+ using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length);
+ Span rgbaSpan = rgbaOwner.GetSpan();
+ PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
+ ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
- for (int i = quantized.Palette.Length - 1; i >= 0; i--)
+ for (int i = rgbaSpan.Length - 1; i >= 0; i--)
+ {
+ if (Unsafe.Add(ref rgbaSpanRef, i).Equals(default))
{
- if (Unsafe.Add(ref paletteRef, i).Equals(default))
- {
- index = i;
- }
+ index = i;
}
}
@@ -326,8 +330,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
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[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
@@ -335,7 +340,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan commentSpan = comment.AsSpan();
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);
}
@@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
///
/// The extension to write to the stream.
/// The stream to write to.
- public void WriteExtension(IGifExtension extension, Stream stream)
+ private void WriteExtension(TGifExtension extension, Stream stream)
+ where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
@@ -437,37 +445,33 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// The pixel format.
/// The to encode.
/// The stream to write to.
- private void WriteColorTable(QuantizedFrame image, Stream stream)
+ private void WriteColorTable(IndexedImageFrame image, Stream stream)
where TPixel : unmanaged, IPixel
{
// The maximum number of colors for the bit depth
- int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
- int pixelCount = image.Palette.Length;
+ int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf();
- using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
- {
- PixelOperations.Instance.ToRgb24Bytes(
- this.configuration,
- image.Palette.Span,
- colorTable.GetSpan(),
- pixelCount);
- stream.Write(colorTable.Array, 0, colorTableLength);
- }
+ using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean);
+ PixelOperations.Instance.ToRgb24Bytes(
+ this.configuration,
+ image.Palette.Span,
+ colorTable.GetSpan(),
+ image.Palette.Length);
+
+ stream.Write(colorTable.Array, 0, colorTableLength);
}
///
/// Writes the image pixel data to the stream.
///
/// The pixel format.
- /// The containing indexed pixels.
+ /// The containing indexed pixels.
/// The stream to write to.
- private void WriteImageData(QuantizedFrame image, Stream stream)
+ private void WriteImageData(IndexedImageFrame image, Stream stream)
where TPixel : unmanaged, IPixel
{
- using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
- {
- encoder.Encode(image.GetPixelSpan(), stream);
- }
+ using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
+ encoder.Encode(image.GetPixelBufferSpan(), stream);
}
}
}
diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index eda0c5fb8..056076bf0 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(indexedPixels);
- // TODO: PERF: It looks likt hshift could be calculated once statically.
+ // TODO: PERF: It looks like hshift could be calculated once statically.
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 73833e82b..45e1ffd2d 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -146,7 +146,7 @@ namespace SixLabors.ImageSharp.Formats.Png
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata();
PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
- QuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
+ IndexedImageFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes);
@@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The row span.
/// The quantized pixels. Can be null.
/// The row.
- private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row)
+ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row)
where TPixel : unmanaged, IPixel
{
switch (this.options.ColorType)
@@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.bitDepth < 8)
{
- PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
+ PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
- int stride = this.currentScanline.Length();
- quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
+ quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
@@ -440,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The quantized pixels. Can be null.
/// The row.
/// The
- private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row)
+ private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IndexedImageFrame quantized, int row)
where TPixel : unmanaged, IPixel
{
this.CollectPixelBytes(rowSpan, quantized, row);
@@ -546,59 +545,54 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The pixel format.
/// The containing image data.
/// The quantized frame.
- private void WritePaletteChunk(Stream stream, QuantizedFrame quantized)
+ private void WritePaletteChunk(Stream stream, IndexedImageFrame quantized)
where TPixel : unmanaged, IPixel
{
- if (quantized == null)
+ if (quantized is null)
{
return;
}
// Grab the palette and write it to the stream.
ReadOnlySpan palette = quantized.Palette.Span;
- int paletteLength = Math.Min(palette.Length, 256);
- int colorTableLength = paletteLength * 3;
- bool anyAlpha = false;
+ int paletteLength = palette.Length;
+ int colorTableLength = paletteLength * Unsafe.SizeOf();
+ bool hasAlpha = false;
- using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
- using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
- {
- ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
- ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
- ReadOnlySpan quantizedSpan = quantized.GetPixelSpan();
-
- Rgba32 rgba = default;
-
- for (int i = 0; i < paletteLength; i++)
- {
- if (quantizedSpan.IndexOf((byte)i) > -1)
- {
- int offset = i * 3;
- palette[i].ToRgba32(ref rgba);
+ using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
+ using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
- byte alpha = rgba.A;
+ ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast(colorTable.GetSpan()));
+ ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
- Unsafe.Add(ref colorTableRef, offset) = rgba.R;
- Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
- Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
+ // Bulk convert our palette to RGBA to allow assignment to tables.
+ using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteLength);
+ Span rgbaPaletteSpan = rgbaOwner.GetSpan();
+ PixelOperations.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan);
+ ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan);
- if (alpha > this.options.Threshold)
- {
- alpha = byte.MaxValue;
- }
+ // Loop, assign, and extract alpha values from the palette.
+ for (int i = 0; i < paletteLength; i++)
+ {
+ Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i);
+ byte alpha = rgba.A;
- anyAlpha = anyAlpha || alpha < byte.MaxValue;
- Unsafe.Add(ref alphaTableRef, i) = alpha;
- }
+ Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
+ if (alpha > this.options.Threshold)
+ {
+ alpha = byte.MaxValue;
}
- this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
+ hasAlpha = hasAlpha || alpha < byte.MaxValue;
+ Unsafe.Add(ref alphaTableRef, i) = alpha;
+ }
- // Write the transparency data
- if (anyAlpha)
- {
- this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
- }
+ this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
+
+ // Write the transparency data
+ if (hasAlpha)
+ {
+ this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
}
@@ -783,7 +777,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The image.
/// The quantized pixel data. Can be null.
/// The stream.
- private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream)
+ private void WriteDataChunks(ImageFrame pixels, IndexedImageFrame quantized, Stream stream)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -881,7 +875,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The pixels.
/// The quantized pixels span.
/// The deflate stream.
- private void EncodePixels(ImageFrame pixels, QuantizedFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodePixels(ImageFrame pixels, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
@@ -960,7 +954,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The type of the pixel.
/// The quantized.
/// The deflate stream.
- private void EncodeAdam7IndexedPixels(QuantizedFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
int width = quantized.Width;
@@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png
row += Adam7.RowIncrement[pass])
{
// collect data
- ReadOnlySpan srcRow = quantized.GetRowSpan(row);
+ ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0;
col < width;
col += Adam7.ColumnIncrement[pass])
diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
index 20b8c41c9..3f490ca6f 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
@@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The type of the pixel.
/// The options.
/// The image.
- public static QuantizedFrame CreateQuantizedFrame(
+ public static IndexedImageFrame CreateQuantizedFrame(
PngEncoderOptions options,
Image image)
where TPixel : unmanaged, IPixel
@@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth(
PngEncoderOptions options,
Image image,
- QuantizedFrame quantizedFrame)
+ IndexedImageFrame quantizedFrame)
where TPixel : unmanaged, IPixel
{
byte bitDepth;
diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
index 7a8b4f8bd..16ca4de67 100644
--- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
+++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
@@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Memory
///
public override Span GetSpan()
{
- if (this.Data == null)
+ if (this.Data is null)
{
throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer");
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
index 2649b7fb1..89aca914d 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
@@ -18,12 +18,12 @@ namespace SixLabors.ImageSharp.Memory
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
///
- public int BufferLength { get; }
+ int BufferLength { get; }
///
/// Gets the aggregate number of elements in the group.
///
- public long TotalLength { get; }
+ long TotalLength { get; }
///
/// Gets a value indicating whether the group has been invalidated.
diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
index 7b8e83585..7d30bada6 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
@@ -4,6 +4,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -89,29 +90,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither(
ref TFrameQuantizer quantizer,
- ReadOnlyMemory palette,
ImageFrame source,
- Memory output,
+ IndexedImageFrame destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer
where TPixel : unmanaged, IPixel
{
- Span outputSpan = output.Span;
- ReadOnlySpan paletteSpan = palette.Span;
- int width = bounds.Width;
int offsetY = bounds.Top;
int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
- Span row = source.GetPixelRowSpan(y);
- int rowStart = (y - offsetY) * width;
+ ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
+ ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++)
{
- TPixel sourcePixel = row[x];
- outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed);
+ TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
+ Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
}
}
@@ -119,25 +116,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ApplyPaletteDither(
- Configuration configuration,
- ReadOnlyMemory palette,
+ public void ApplyPaletteDither(
+ in TPaletteDitherImageProcessor processor,
ImageFrame source,
- Rectangle bounds,
- float scale)
+ Rectangle bounds)
+ where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor
where TPixel : unmanaged, IPixel
{
- var pixelMap = new EuclideanPixelMap(palette);
-
+ float scale = processor.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
- Span row = source.GetPixelRowSpan(y);
+ ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = bounds.Left; x < bounds.Right; x++)
{
- TPixel sourcePixel = row[x];
- pixelMap.GetClosestColor(sourcePixel, out TPixel transformed);
+ ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
+ TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
- row[x] = transformed;
+ sourcePixel = transformed;
}
}
}
diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs
index 3d6edc9fa..8f9d82537 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
-using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -19,15 +18,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// The type of frame quantizer.
/// The pixel format.
/// The frame quantizer.
- /// The quantized palette.
/// The source image.
- /// The output target
+ /// The destination quantized frame.
/// The region of interest bounds.
void ApplyQuantizationDither(
ref TFrameQuantizer quantizer,
- ReadOnlyMemory palette,
ImageFrame source,
- Memory output,
+ IndexedImageFrame destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer
where TPixel : unmanaged, IPixel;
@@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels.
///
+ /// The type of palette dithering processor.
/// The pixel format.
- /// The configuration.
- /// The quantized palette.
+ /// The palette dithering processor.
/// The source image.
/// The region of interest bounds.
- /// The dithering scale used to adjust the amount of dither. Range 0..1.
- void ApplyPaletteDither(
- Configuration configuration,
- ReadOnlyMemory palette,
+ void ApplyPaletteDither(
+ in TPaletteDitherImageProcessor processor,
ImageFrame source,
- Rectangle bounds,
- float scale)
+ Rectangle bounds)
+ where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor
where TPixel : unmanaged, IPixel;
}
}
diff --git a/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
new file mode 100644
index 000000000..a8e08fa3f
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
@@ -0,0 +1,38 @@
+// 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.Dithering
+{
+ ///
+ /// Implements an algorithm to alter the pixels of an image via palette dithering.
+ ///
+ /// The pixel format.
+ public interface IPaletteDitherImageProcessor
+ where TPixel : unmanaged, IPixel
+ {
+ ///
+ /// Gets the configuration instance to use when performing operations.
+ ///
+ Configuration Configuration { get; }
+
+ ///
+ /// Gets the dithering palette.
+ ///
+ ReadOnlyMemory Palette { get; }
+
+ ///
+ /// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
+ ///
+ float DitherScale { get; }
+
+ ///
+ /// Returns the color from the dithering palette corresponding to the given color.
+ ///
+ /// The color to match.
+ /// The match.
+ TPixel GetPaletteColor(TPixel color);
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
index d3e710782..f6026a64f 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
///
/// An ordered dithering matrix with equal sides of arbitrary length
///
- public readonly partial struct OrderedDither : IDither
+ public readonly partial struct OrderedDither
{
///
/// Applies order dithering using the 2x2 Bayer dithering matrix.
diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
index 6efb84be9..6862cff00 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
@@ -3,8 +3,8 @@
using System;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -105,23 +105,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither(
ref TFrameQuantizer quantizer,
- ReadOnlyMemory palette,
ImageFrame source,
- Memory output,
+ IndexedImageFrame destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer
where TPixel : unmanaged, IPixel
{
- var ditherOperation = new QuantizeDitherRowIntervalOperation(
+ var ditherOperation = new QuantizeDitherRowOperation(
ref quantizer,
in Unsafe.AsRef(this),
source,
- output,
- bounds,
- palette,
- ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
+ destination,
+ bounds);
- ParallelRowIterator.IterateRowIntervals(
+ ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in ditherOperation);
@@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
///
[MethodImpl(InliningOptions.ShortMethod)]
- public void ApplyPaletteDither(
- Configuration configuration,
- ReadOnlyMemory palette,
+ public void ApplyPaletteDither(
+ in TPaletteDitherImageProcessor processor,
ImageFrame source,
- Rectangle bounds,
- float scale)
+ Rectangle bounds)
+ where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor
where TPixel : unmanaged, IPixel
{
- var ditherOperation = new PaletteDitherRowIntervalOperation(
+ var ditherOperation = new PaletteDitherRowOperation(
+ in processor,
in Unsafe.AsRef(this),
source,
- bounds,
- palette,
- scale,
- ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
+ bounds);
- ParallelRowIterator.IterateRowIntervals(
- configuration,
+ ParallelRowIterator.IterateRows(
+ processor.Configuration,
bounds,
in ditherOperation);
}
@@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
- private readonly struct QuantizeDitherRowIntervalOperation : IRowIntervalOperation
+ private readonly struct QuantizeDitherRowOperation : IRowOperation
where TFrameQuantizer : struct, IFrameQuantizer
where TPixel : unmanaged, IPixel
{
private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither;
private readonly ImageFrame source;
- private readonly Memory output;
+ private readonly IndexedImageFrame destination;
private readonly Rectangle bounds;
- private readonly ReadOnlyMemory palette;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
- public QuantizeDitherRowIntervalOperation(
+ public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer,
in OrderedDither dither,
ImageFrame source,
- Memory output,
- Rectangle bounds,
- ReadOnlyMemory palette,
- int bitDepth)
+ IndexedImageFrame destination,
+ Rectangle bounds)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
- this.output = output;
+ this.destination = destination;
this.bounds = bounds;
- this.palette = palette;
- this.bitDepth = bitDepth;
+ this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
- public void Invoke(in RowInterval rows)
+ public void Invoke(int y)
{
- ReadOnlySpan paletteSpan = this.palette.Span;
- Span outputSpan = this.output.Span;
- int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale;
- for (int y = rows.Min; y < rows.Max; y++)
+ ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
+ ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
+
+ for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
- Span row = this.source.GetPixelRowSpan(y);
- int rowStart = (y - offsetY) * width;
-
- // TODO: This can be a bulk operation.
- for (int x = this.bounds.Left; x < this.bounds.Right; x++)
- {
- TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale);
- outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _);
- }
+ TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
+ Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
}
}
}
- private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation
+ private readonly struct PaletteDitherRowOperation : IRowOperation
+ where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor
where TPixel : unmanaged, IPixel
{
+ private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither;
private readonly ImageFrame source;
private readonly Rectangle bounds;
- private readonly EuclideanPixelMap pixelMap;
private readonly float scale;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
- public PaletteDitherRowIntervalOperation(
+ public PaletteDitherRowOperation(
+ in TPaletteDitherImageProcessor processor,
in OrderedDither dither,
ImageFrame source,
- Rectangle bounds,
- ReadOnlyMemory palette,
- float scale,
- int bitDepth)
+ Rectangle bounds)
{
+ this.processor = processor;
this.dither = dither;
this.source = source;
this.bounds = bounds;
- this.pixelMap = new EuclideanPixelMap(palette);
- this.scale = scale;
- this.bitDepth = bitDepth;
+ this.scale = processor.DitherScale;
+ this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
- public void Invoke(in RowInterval rows)
+ public void Invoke(int y)
{
- for (int y = rows.Min; y < rows.Max; y++)
+ ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
+
+ for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
- Span row = this.source.GetPixelRowSpan(y);
-
- for (int x = this.bounds.Left; x < this.bounds.Right; x++)
- {
- TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale);
- this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
- row[x] = transformed;
- }
+ ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
+ TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
+ sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
}
}
}
diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
index 254847f45..e0dd4eae1 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
@@ -3,7 +3,9 @@
using System;
using System.Buffers;
+using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
@@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
internal sealed class PaletteDitherProcessor : ImageProcessor
where TPixel : unmanaged, IPixel
{
- private readonly int paletteLength;
+ private readonly DitherProcessor ditherProcessor;
private readonly IDither dither;
- private readonly float ditherScale;
- private readonly ReadOnlyMemory sourcePalette;
- private IMemoryOwner palette;
+ private IMemoryOwner paletteOwner;
private bool isDisposed;
///
@@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
- this.paletteLength = definition.Palette.Span.Length;
this.dither = definition.Dither;
- this.ditherScale = definition.DitherScale;
- this.sourcePalette = definition.Palette;
- }
- ///
- protected override void OnFrameApply(ImageFrame source)
- {
- var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
+ ReadOnlySpan sourcePalette = definition.Palette.Span;
+ this.paletteOwner = this.Configuration.MemoryAllocator.Allocate(sourcePalette.Length);
+ Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span);
- this.dither.ApplyPaletteDither(
+ this.ditherProcessor = new DitherProcessor(
this.Configuration,
- this.palette.Memory,
- source,
- interest,
- this.ditherScale);
+ this.paletteOwner.Memory,
+ definition.DitherScale);
}
///
- protected override void BeforeFrameApply(ImageFrame source)
+ protected override void OnFrameApply(ImageFrame source)
{
- // Lazy init palettes:
- if (this.palette is null)
- {
- this.palette = this.Configuration.MemoryAllocator.Allocate(this.paletteLength);
- ReadOnlySpan sourcePalette = this.sourcePalette.Span;
- Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span);
- }
-
- base.BeforeFrameApply(source);
+ var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
+ this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
}
///
@@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
return;
}
+ this.isDisposed = true;
if (disposing)
{
- this.palette?.Dispose();
+ this.paletteOwner.Dispose();
}
- this.palette = null;
-
- this.isDisposed = true;
+ this.paletteOwner = null;
base.Dispose(disposing);
}
+
+ ///
+ /// Used to allow inlining of calls to
+ /// .
+ ///
+ private readonly struct DitherProcessor : IPaletteDitherImageProcessor
+ {
+ private readonly EuclideanPixelMap pixelMap;
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public DitherProcessor(
+ Configuration configuration,
+ ReadOnlyMemory palette,
+ float ditherScale)
+ {
+ this.Configuration = configuration;
+ this.pixelMap = new EuclideanPixelMap(configuration, palette);
+ this.Palette = palette;
+ this.DitherScale = ditherScale;
+ }
+
+ public Configuration Configuration { get; }
+
+ public ReadOnlyMemory Palette { get; }
+
+ public float DitherScale { get; }
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public TPixel GetPaletteColor(TPixel color)
+ {
+ this.pixelMap.GetClosestColor(color, out TPixel match);
+ return match;
+ }
+ }
}
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
index 929a66674..775e0aa23 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
@@ -5,101 +5,100 @@ using System;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
///
- /// Gets the closest color to the supplied color based upon the Eucladean distance.
- /// TODO: Expose this somehow.
+ /// Gets the closest color to the supplied color based upon the Euclidean distance.
///
/// The pixel format.
- internal readonly struct EuclideanPixelMap : IPixelMap, IEquatable>
+ internal readonly struct EuclideanPixelMap
where TPixel : unmanaged, IPixel
{
- private readonly ConcurrentDictionary vectorCache;
+ private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary distanceCache;
///
/// Initializes a new instance of the struct.
///
+ /// The configuration.
/// The color palette to map from.
- public EuclideanPixelMap(ReadOnlyMemory palette)
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette)
{
- Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
-
this.Palette = palette;
- ReadOnlySpan paletteSpan = this.Palette.Span;
- this.vectorCache = new ConcurrentDictionary();
- this.distanceCache = new ConcurrentDictionary();
+ this.vectorCache = new Vector4[palette.Length];
- for (int i = 0; i < paletteSpan.Length; i++)
- {
- this.vectorCache[i] = paletteSpan[i].ToScaledVector4();
- }
+ // Use the same rules across all target frameworks.
+ this.distanceCache = new ConcurrentDictionary(Environment.ProcessorCount, 31);
+ PixelOperations.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
}
- ///
- public ReadOnlyMemory Palette { get; }
-
- ///
- public override bool Equals(object obj)
- => obj is EuclideanPixelMap map && this.Equals(map);
-
- ///
- public bool Equals(EuclideanPixelMap other)
- => this.Palette.Equals(other.Palette);
+ ///
+ /// Gets the color palette of this .
+ /// The palette memory is owned by the palette source that created it.
+ ///
+ public ReadOnlyMemory Palette
+ {
+ [MethodImpl(InliningOptions.ShortMethod)]
+ get;
+ }
- ///
+ ///
+ /// Returns the closest color in the palette and the index of that pixel.
+ /// The palette contents must match the one used in the constructor.
+ ///
+ /// The color to match.
+ /// The matched color.
+ /// The index.
[MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match)
{
- ReadOnlySpan paletteSpan = this.Palette.Span;
+ ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
// 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))
{
- match = paletteSpan[index];
- return index;
+ return this.GetClosestColorSlow(color, ref paletteRef, out match);
}
- return this.GetClosestColorSlow(color, paletteSpan, out match);
+ match = Unsafe.Add(ref paletteRef, index);
+ return index;
}
- ///
- public override int GetHashCode()
- => this.vectorCache.GetHashCode();
-
[MethodImpl(InliningOptions.ShortMethod)]
- private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match)
+ private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
- Vector4 vector = color.ToScaledVector4();
-
- for (int i = 0; i < palette.Length; i++)
+ var vector = color.ToVector4();
+ ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference(this.vectorCache);
+ for (int i = 0; i < this.Palette.Length; i++)
{
- Vector4 candidate = this.vectorCache[i];
+ Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate);
- // Less than... assign.
+ // If it's an exact match, exit the loop
+ if (distance == 0)
+ {
+ index = i;
+ break;
+ }
+
if (distance < leastDistance)
{
+ // Less than... assign.
index = i;
leastDistance = distance;
-
- // And if it's an exact match, exit the loop
- if (distance == 0)
- {
- break;
- }
}
}
// Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index;
- match = palette[index];
+ match = Unsafe.Add(ref paletteRef, index);
return index;
}
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs
similarity index 56%
rename from src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
rename to src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs
index 306e9d36e..4d75042ea 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs
@@ -11,22 +11,40 @@ using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
///
- /// Contains extension methods for frame quantizers.
+ /// Contains utility methods for instances.
///
- public static class FrameQuantizerExtensions
+ public static class FrameQuantizerUtilities
{
+ ///
+ /// Helper method for throwing an exception when a frame quantizer palette has
+ /// been requested but not built yet.
+ ///
+ /// The pixel format.
+ /// The frame quantizer palette.
+ ///
+ /// The palette has not been built via
+ ///
+ public static void CheckPaletteState(in ReadOnlyMemory palette)
+ where TPixel : unmanaged, IPixel
+ {
+ if (palette.Equals(default))
+ {
+ throw new InvalidOperationException("Frame Quantizer palette has not been built.");
+ }
+ }
+
///
/// Quantizes an image frame and return the resulting output pixels.
///
/// The type of frame quantizer.
/// The pixel format.
- /// The frame
+ /// The frame quantizer.
/// The source image frame to quantize.
/// The bounds within the frame to quantize.
///
- /// A representing a quantized version of the source frame pixels.
+ /// A representing a quantized version of the source frame pixels.
///
- public static QuantizedFrame QuantizeFrame(
+ public static IndexedImageFrame QuantizeFrame(
ref TFrameQuantizer quantizer,
ImageFrame source,
Rectangle bounds)
@@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs.
- ReadOnlyMemory palette = quantizer.BuildPalette(source, interest);
- MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
+ quantizer.BuildPalette(source, interest);
- var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette);
- Memory output = quantizedFrame.GetWritablePixelMemory();
+ var destination = new IndexedImageFrame(
+ quantizer.Configuration,
+ interest.Width,
+ interest.Height,
+ quantizer.Palette);
if (quantizer.Options.Dither is null)
{
- SecondPass(ref quantizer, source, interest, output, palette);
+ SecondPass(ref quantizer, source, destination, interest);
}
else
{
// We clone the image as we don't want to alter the original via error diffusion based dithering.
- using (ImageFrame clone = source.Clone())
- {
- SecondPass(ref quantizer, clone, interest, output, palette);
- }
+ using ImageFrame clone = source.Clone();
+ SecondPass(ref quantizer, clone, destination, interest);
}
- return quantizedFrame;
+ return destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass(
ref TFrameQuantizer quantizer,
ImageFrame source,
- Rectangle bounds,
- Memory output,
- ReadOnlyMemory palette)
+ IndexedImageFrame destination,
+ Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer
where TPixel : unmanaged, IPixel
{
@@ -73,7 +90,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null)
{
- var operation = new RowIntervalOperation(quantizer, source, output, bounds, palette);
+ var operation = new RowIntervalOperation(
+ ref quantizer,
+ source,
+ destination,
+ bounds);
+
ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration,
bounds,
@@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return;
}
- dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds);
+ dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation
@@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly TFrameQuantizer quantizer;
private readonly ImageFrame source;
- private readonly Memory output;
+ private readonly IndexedImageFrame destination;
private readonly Rectangle bounds;
- private readonly ReadOnlyMemory palette;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
- in TFrameQuantizer quantizer,
+ ref TFrameQuantizer quantizer,
ImageFrame source,
- Memory output,
- Rectangle bounds,
- ReadOnlyMemory palette)
+ IndexedImageFrame destination,
+ Rectangle bounds)
{
this.quantizer = quantizer;
this.source = source;
- this.output = output;
+ this.destination = destination;
this.bounds = bounds;
- this.palette = palette;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
- ReadOnlySpan paletteSpan = this.palette.Span;
- Span outputSpan = this.output.Span;
- int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++)
{
- Span row = this.source.GetPixelRowSpan(y);
- int rowStart = (y - offsetY) * width;
+ Span sourceRow = this.source.GetPixelRowSpan(y);
+ Span destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY);
- // TODO: This can be a bulk operation.
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
- outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _);
+ destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
}
}
}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
index 5aaae9fa6..cc87715eb 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
@@ -24,33 +24,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
QuantizerOptions Options { get; }
///
- /// Quantizes an image frame and return the resulting output pixels.
+ /// Gets the quantized color palette.
///
- /// The source image frame to quantize.
- /// The bounds within the frame to quantize.
- ///
- /// A representing a quantized version of the source frame pixels.
- ///
- QuantizedFrame QuantizeFrame(
- ImageFrame source,
- Rectangle bounds);
+ ///
+ /// The palette has not been built via .
+ ///
+ ReadOnlyMemory Palette { get; }
///
/// Builds the quantized palette from the given image frame and bounds.
///
/// The source image frame.
/// The region of interest bounds.
- /// The palette.
- ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds);
+ void BuildPalette(ImageFrame source, Rectangle bounds);
+
+ ///
+ /// Quantizes an image frame and return the resulting output pixels.
+ ///
+ /// The source image frame to quantize.
+ /// The bounds within the frame to quantize.
+ ///
+ /// A representing a quantized version of the source frame pixels.
+ ///
+ IndexedImageFrame QuantizeFrame(ImageFrame source, Rectangle bounds);
///
- /// Returns the index and color from the quantized palette corresponding to the give to the given color.
+ /// Returns the index and color from the quantized palette corresponding to the given color.
///
/// The color to match.
- /// The output color palette.
/// The matched color.
/// The index.
- public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match);
+ byte GetQuantizedColor(TPixel color, out TPixel match);
// TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan colors, ReadOnlySpan palette, Span indices, Span matches);
diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
deleted file mode 100644
index b421dce21..000000000
--- a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Allows the mapping of input colors to colors within a given palette.
- /// TODO: Expose this somehow.
- ///
- /// The pixel format.
- internal interface IPixelMap
- where TPixel : unmanaged, IPixel
- {
- ///
- /// Gets the color palette containing colors to match.
- ///
- ReadOnlyMemory Palette { get; }
-
- ///
- /// Returns the closest color in the palette and the index of that pixel.
- ///
- /// The color to match.
- /// The matched color.
- /// The index.
- int GetClosestColor(TPixel color, out TPixel match);
- }
-}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs
new file mode 100644
index 000000000..ac737f452
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Processing.Processors.Quantization
+{
+ ///
+ /// A pixel-specific image frame where each pixel buffer value represents an index in a color palette.
+ ///
+ /// The pixel format.
+ public sealed class IndexedImageFrame : IDisposable
+ where TPixel : unmanaged, IPixel
+ {
+ private IMemoryOwner pixelsOwner;
+ private IMemoryOwner paletteOwner;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The configuration which allows altering default behaviour or extending the library.
+ ///
+ /// The frame width.
+ /// The frame height.
+ /// The color palette.
+ internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette)
+ {
+ Guard.NotNull(configuration, nameof(configuration));
+ Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette));
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+
+ this.Configuration = configuration;
+ this.Width = width;
+ this.Height = height;
+ this.pixelsOwner = configuration.MemoryAllocator.AllocateManagedByteBuffer(width * height);
+
+ // Copy the palette over. We want the lifetime of this frame to be independant of any palette source.
+ this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length);
+ palette.Span.CopyTo(this.paletteOwner.GetSpan());
+ this.Palette = this.paletteOwner.Memory.Slice(0, palette.Length);
+ }
+
+ ///
+ /// Gets the configuration which allows altering default behaviour or extending the library.
+ ///
+ public Configuration Configuration { get; }
+
+ ///
+ /// Gets the width of this .
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height of this .
+ ///
+ public int Height { get; }
+
+ ///
+ /// Gets the color palette of this .
+ ///
+ public ReadOnlyMemory Palette { get; }
+
+ ///
+ /// Gets the pixels of this .
+ ///
+ /// The
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public ReadOnlySpan GetPixelBufferSpan() => this.pixelsOwner.GetSpan(); // TODO: Buffer2D
+
+ ///
+ /// Gets the representation of the pixels as a of contiguous memory
+ /// at row beginning from the the first pixel on that row.
+ ///
+ /// The row index in the pixel buffer.
+ /// The pixel row as a .
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public ReadOnlySpan GetPixelRowSpan(int rowIndex)
+ => this.GetWritablePixelRowSpanUnsafe(rowIndex);
+
+ ///
+ ///
+ /// Gets the representation of the pixels as a of contiguous memory
+ /// at row beginning from the the first pixel on that row.
+ ///
+ ///
+ /// Note: Values written to this span are not sanitized against the palette length.
+ /// Care should be taken during assignment to prevent out-of-bounds errors.
+ ///
+ ///
+ /// The row index in the pixel buffer.
+ /// The pixel row as a .
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public Span GetWritablePixelRowSpanUnsafe(int rowIndex)
+ => this.pixelsOwner.GetSpan().Slice(rowIndex * this.Width, this.Width);
+
+ ///
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.isDisposed = true;
+ this.pixelsOwner.Dispose();
+ this.paletteOwner.Dispose();
+ this.pixelsOwner = null;
+ this.paletteOwner = null;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
index 6cca4dd3c..ce2e406d4 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
@@ -3,7 +3,6 @@
using System;
using System.Buffers;
-using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -21,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public struct OctreeFrameQuantizer : IFrameQuantizer
where TPixel : unmanaged, IPixel
{
- private readonly int colors;
+ private readonly int maxColors;
private readonly Octree octree;
+ private IMemoryOwner paletteOwner;
+ private ReadOnlyMemory palette;
private EuclideanPixelMap pixelMap;
private readonly bool isDithering;
+ private bool isDisposed;
///