diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 0d25210a99..ed5ed42933 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -339,7 +339,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); - ReadOnlySpan quantizedColors = quantized.Palette.Span; + ReadOnlySpan quantizedColors = quantized.Palette; var color = default(Rgba32); // TODO: Use bulk conversion here for better perf diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 978609d7fb..53c4c6f3fd 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 e32910d37b..87317a3ef5 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } // Clean up. - quantized?.Dispose(); + quantized.Dispose(); // TODO: Write extension etc stream.WriteByte(GifConstants.EndIntroducer); @@ -142,11 +142,9 @@ 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())) - { - this.WriteImageData(paletteQuantized, stream); - } + using var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette); + using QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); + this.WriteImageData(paletteQuantized, stream); } } } @@ -156,8 +154,9 @@ namespace SixLabors.ImageSharp.Formats.Gif { 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,17 +172,13 @@ 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()); } } @@ -193,7 +188,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); - quantized?.Dispose(); + quantized.Dispose(); quantized = null; // So next frame can regenerate it previousFrame = frame; previousMeta = frameMetadata; @@ -219,7 +214,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { Span rgbaSpan = rgbaBuffer.GetSpan(); ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan); + PixelOperations.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan); for (int i = quantized.Palette.Length - 1; i >= 0; i--) { @@ -391,7 +386,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; @@ -444,15 +440,13 @@ namespace SixLabors.ImageSharp.Formats.Gif int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; int pixelCount = image.Palette.Length; - 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); + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + image.Palette, + colorTable.GetSpan(), + pixelCount); + stream.Write(colorTable.Array, 0, colorTableLength); } /// @@ -464,10 +458,8 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteImageData(QuantizedFrame 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.GetPixelSpan(), stream); } } } diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index eda0c5fb8c..056076bf01 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 ce624f768a..ed2fe143bb 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -555,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png } // Grab the palette and write it to the stream. - ReadOnlySpan palette = quantized.Palette.Span; + ReadOnlySpan palette = quantized.Palette; int paletteLength = Math.Min(palette.Length, 256); int colorTableLength = paletteLength * 3; bool anyAlpha = false; diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 3d18ef3587..9217d1c3f7 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IFrameQuantizer where TPixel : unmanaged, IPixel { - ReadOnlySpan paletteSpan = destination.Palette.Span; + ReadOnlySpan paletteSpan = destination.Palette; int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 00ee4a7e63..c5b4135f56 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -203,7 +203,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering private readonly ImageFrame source; private readonly QuantizedFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] @@ -219,14 +218,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering this.source = source; this.destination = destination; this.bounds = bounds; - this.palette = destination.Palette; - this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Span.Length); + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length); } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.palette.Span; + ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; float scale = this.quantizer.Options.DitherScale; diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 7a789164fd..615a7238b8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// - /// Gets the closest color to the supplied color based upon the Eucladean distance. + /// Gets the closest color to the supplied color based upon the Euclidean distance. /// TODO: Expose this somehow. /// /// The pixel format. @@ -62,18 +62,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlySpan paletteSpan = 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, paletteSpan, out match); } - return this.GetClosestColorSlow(color, paletteSpan, out match); + match = paletteSpan[index]; + return index; } /// - public override int GetHashCode() - => this.vectorCache.GetHashCode(); + public override int GetHashCode() => this.vectorCache.GetHashCode(); [MethodImpl(InliningOptions.ShortMethod)] private int GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match) @@ -88,17 +87,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Vector4 candidate = this.vectorCache[i]; float distance = Vector4.DistanceSquared(vector, candidate); + if (!(distance < leastDistance)) + { + continue; + } + // Less than... assign. - if (distance < leastDistance) + index = i; + leastDistance = distance; + + // And if it's an exact match, exit the loop + if (distance == 0) { - index = i; - leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance == 0) - { - break; - } + break; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs index a589f7524d..d6d8b98dae 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// 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. /// @@ -37,7 +37,7 @@ 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); + ReadOnlySpan palette = quantizer.BuildPalette(source, interest); MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); @@ -49,10 +49,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization 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, quantizedFrame, interest); - } + using ImageFrame clone = source.Clone(); + SecondPass(ref quantizer, clone, quantizedFrame, interest); } return quantizedFrame; @@ -71,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (dither is null) { - var operation = new RowIntervalOperation(quantizer, source, destination, bounds); + var operation = new RowIntervalOperation(ref quantizer, source, destination, bounds); ParallelRowIterator.IterateRows( quantizer.Configuration, bounds, @@ -91,11 +89,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private readonly ImageFrame source; private readonly QuantizedFrame destination; private readonly Rectangle bounds; - private readonly ReadOnlyMemory palette; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( - in TFrameQuantizer quantizer, + ref TFrameQuantizer quantizer, ImageFrame source, QuantizedFrame destination, Rectangle bounds) @@ -104,13 +101,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.source = source; this.destination = destination; this.bounds = bounds; - this.palette = destination.Palette; } [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(in RowInterval rows) { - ReadOnlySpan paletteSpan = this.palette.Span; + ReadOnlySpan paletteSpan = this.destination.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 57e8fc3f35..506767277d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -38,8 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The source image frame. /// The region of interest bounds. - /// The palette. - ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + /// The palette. + ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds); /// /// Returns the index and color from the quantized palette corresponding to the given color. @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The output color palette. /// The matched color. /// The index. - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match); + byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, 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/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 1b7c9edd64..946ae399bb 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 SixLabors.ImageSharp.Advanced; @@ -22,8 +21,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { private readonly int colors; private readonly Octree octree; + private IMemoryOwner palette; private EuclideanPixelMap pixelMap; private readonly bool isDithering; + private bool isDisposed; /// /// Initializes a new instance of the struct. @@ -41,8 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.colors = this.Options.MaxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); + this.palette = configuration.MemoryAllocator.Allocate(this.colors, AllocationOptions.Clean); this.pixelMap = default; this.isDithering = !(this.Options.Dither is null); + this.isDisposed = false; } /// @@ -53,12 +56,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) { using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); Span bufferSpan = buffer.GetSpan(); @@ -78,15 +81,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } - TPixel[] palette = this.octree.Palletize(this.colors); - this.pixelMap = new EuclideanPixelMap(palette); + Span paletteSpan = this.palette.GetSpan(); + this.octree.Palletize(paletteSpan, this.colors); - return palette; + // TODO: Cannot make method readonly due to this line. + this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + + return paletteSpan; } /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { // Octree only maps the RGB component of a color // so cannot tell the difference between a fully transparent @@ -104,6 +110,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public void Dispose() { + if (!this.isDisposed) + { + this.isDisposed = true; + this.palette.Dispose(); + this.palette = null; + } } /// @@ -116,14 +128,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private static readonly byte[] Mask = new byte[] { - 0b10000000, - 0b1000000, - 0b100000, - 0b10000, - 0b1000, - 0b100, - 0b10, - 0b1 + 0b10000000, 0b1000000, 0b100000, 0b10000, 0b1000, 0b100, 0b10, 0b1 }; /// @@ -216,26 +221,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// + /// The palette to fill. /// The maximum number of colors - /// - /// An with the palletized colors - /// [MethodImpl(InliningOptions.ShortMethod)] - public TPixel[] Palletize(int colorCount) + public void Palletize(Span palette, int colorCount) { while (this.Leaves > colorCount - 1) { this.Reduce(); } - // Now palletize the nodes - var palette = new TPixel[colorCount]; - int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); - - // And return the palette - return palette; } /// @@ -437,12 +434,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The palette /// The current palette index [MethodImpl(InliningOptions.ColdPath)] - public void ConstructPalette(TPixel[] palette, ref int index) + public void ConstructPalette(Span palette, ref int index) { if (this.leaf) { // Set the color of the palette entry - var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255)); + var vector = Vector3.Clamp( + new Vector3(this.red, this.green, this.blue) / this.pixelCount, + Vector3.Zero, + new Vector3(255)); + TPixel pixel = default; pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue)); palette[index] = pixel; @@ -516,8 +517,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization int shift = 7 - level; byte mask = Mask[level]; return ((color.R & mask) >> shift) - | ((color.G & mask) >> (shift - 1)) - | ((color.B & mask) >> (shift - 2)); + | ((color.G & mask) >> (shift - 1)) + | ((color.B & mask) >> (shift - 2)); } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 3200dfab88..7960f728af 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -2,7 +2,9 @@ // 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 @@ -15,8 +17,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal struct PaletteFrameQuantizer : IFrameQuantizer where TPixel : unmanaged, IPixel { - private readonly ReadOnlyMemory palette; + private IMemoryOwner paletteOwner; private readonly EuclideanPixelMap pixelMap; + private bool isDisposed; /// /// Initializes a new instance of the struct. @@ -25,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The quantizer options defining quantization rules. /// A containing all colors in the palette. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory colors) + public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan colors) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); @@ -33,8 +36,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.Configuration = configuration; this.Options = options; - this.palette = colors; - this.pixelMap = new EuclideanPixelMap(colors); + int maxLength = Math.Min(colors.Length, options.MaxColors); + this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); + Color.ToPixel(configuration, colors, this.paletteOwner.GetSpan()); + + this.pixelMap = new EuclideanPixelMap(this.paletteOwner.Memory); + this.isDisposed = false; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The quantizer options defining quantization rules. + /// A containing all colors in the palette. + [MethodImpl(InliningOptions.ShortMethod)] + public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan palette) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); + + this.Configuration = configuration; + this.Options = options; + + int maxLength = Math.Min(palette.Length, options.MaxColors); + this.paletteOwner = configuration.MemoryAllocator.Allocate(maxLength); + palette.CopyTo(this.paletteOwner.GetSpan()); + + this.pixelMap = new EuclideanPixelMap(this.paletteOwner.Memory); + this.isDisposed = false; } /// @@ -45,22 +75,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) - => this.palette; + public readonly ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) + => this.paletteOwner.GetSpan(); /// [MethodImpl(InliningOptions.ShortMethod)] - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) => (byte)this.pixelMap.GetClosestColor(color, out match); /// public void Dispose() { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.paletteOwner.Dispose(); + this.paletteOwner = null; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 07fa6e41e5..75a0f39389 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { + private readonly ReadOnlyMemory palette; + /// /// Initializes a new instance of the class. /// @@ -30,15 +32,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.NotNull(options, nameof(options)); - this.Palette = palette; + this.palette = palette; this.Options = options; } - /// - /// Gets the color palette. - /// - public ReadOnlyMemory Palette { get; } - /// public QuantizerOptions Options { get; } @@ -52,12 +49,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : unmanaged, IPixel { Guard.NotNull(options, nameof(options)); - - int length = Math.Min(this.Palette.Span.Length, options.MaxColors); - var palette = new TPixel[length]; - - Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, options, palette); + return new PaletteFrameQuantizer(configuration, options, this.palette.Span); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 5a0116a03f..04586807e7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public void Invoke(in RowInterval rows) { ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); - ReadOnlySpan paletteSpan = this.quantized.Palette.Span; + ReadOnlySpan paletteSpan = this.quantized.Palette; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; int width = this.bounds.Width; diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 07e89a2b0c..cda0546e4a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -16,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public sealed class QuantizedFrame : IDisposable where TPixel : unmanaged, IPixel { + private IMemoryOwner palette; private IMemoryOwner pixels; private bool isDisposed; @@ -26,15 +27,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The image width. /// The image height. /// The color palette. - internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlyMemory palette) + internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlySpan palette) { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); this.Width = width; this.Height = height; - this.Palette = palette; this.pixels = memoryAllocator.AllocateManagedByteBuffer(width * height, AllocationOptions.Clean); + + this.palette = memoryAllocator.Allocate(palette.Length); + palette.CopyTo(this.palette.GetSpan()); } /// @@ -50,7 +53,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Gets the color palette of this . /// - public ReadOnlyMemory Palette { get; private set; } + public ReadOnlySpan Palette + { + [MethodImpl(InliningOptions.ShortMethod)] + get { return this.palette.GetSpan(); } + } /// /// Gets the pixels of this . @@ -72,15 +79,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public void Dispose() { - if (this.isDisposed) + if (!this.isDisposed) { return; } this.isDisposed = true; this.pixels?.Dispose(); + this.palette?.Dispose(); this.pixels = null; - this.Palette = null; + this.palette = null; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 177f7e8590..5054772582 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -65,30 +65,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; - /// - /// Color moments. - /// private IMemoryOwner moments; - - /// - /// Color space tag. - /// private IMemoryOwner tag; - - /// - /// Maximum allowed color depth - /// + private IMemoryOwner palette; private int colors; - - /// - /// The color cube representing the image palette - /// private readonly Box[] colorCube; - private EuclideanPixelMap pixelMap; - private readonly bool isDithering; - private bool isDisposed; /// @@ -104,10 +87,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.Configuration = configuration; this.Options = options; + this.colors = this.Options.MaxColors; this.memoryAllocator = this.Configuration.MemoryAllocator; this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.colors = this.Options.MaxColors; + this.palette = this.memoryAllocator.Allocate(this.colors, AllocationOptions.Clean); this.colorCube = new Box[this.colors]; this.isDisposed = false; this.pixelMap = default; @@ -122,19 +106,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] - public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) - => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); + public readonly QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds); /// - public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + public ReadOnlySpan BuildPalette(ImageFrame source, Rectangle bounds) { this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); - var palette = new TPixel[this.colors]; ReadOnlySpan momentsSpan = this.moments.GetSpan(); - + Span paletteSpan = this.palette.GetSpan(); for (int k = 0; k < this.colors; k++) { this.Mark(ref this.colorCube[k], (byte)k); @@ -143,17 +126,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (moment.Weight > 0) { - ref TPixel color = ref palette[k]; + ref TPixel color = ref paletteSpan[k]; color.FromScaledVector4(moment.Normalize()); } } - this.pixelMap = new EuclideanPixelMap(palette); - return palette; + // TODO: Cannot make methods readonly due to this line. + this.pixelMap = new EuclideanPixelMap(this.palette.Memory); + return paletteSpan; } /// - public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { if (!this.isDithering) { @@ -177,16 +161,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public void Dispose() { - if (this.isDisposed) + if (!this.isDisposed) { - return; + this.isDisposed = true; + this.moments?.Dispose(); + this.tag?.Dispose(); + this.palette?.Dispose(); + this.moments = null; + this.tag = null; + this.palette = null; } - - this.isDisposed = true; - this.moments?.Dispose(); - this.tag?.Dispose(); - this.moments = null; - this.tag = null; } /// diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 5e91f98eb1..71405890cd 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs @@ -21,6 +21,12 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs private SDImage bmpDrawing; private Image bmpCore; + // Try to get as close to System.Drawing's output as possible + private readonly GifEncoder encoder = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + [GlobalSetup] public void ReadImages() { @@ -53,15 +59,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs [Benchmark(Description = "ImageSharp Gif")] public void GifCore() { - // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder - { - Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) - }; - using (var memoryStream = new MemoryStream()) { - this.bmpCore.SaveAsGif(memoryStream, options); + this.bmpCore.SaveAsGif(memoryStream, this.encoder); } } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index cd93ab0cf8..4085d99439 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests // Transparent pixels are much more likely to be found at the end of a palette int index = -1; Rgba32 trans = default; - ReadOnlySpan paletteSpan = quantized.Palette.Span; + ReadOnlySpan paletteSpan = quantized.Palette; for (int i = paletteSpan.Length - 1; i >= 0; i--) { paletteSpan[i].ToRgba32(ref trans); diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index f3bcd0b955..34888f1dbc 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); + Assert.Equal(Color.Black, (Color)result.Palette[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(1, result.Palette.Length); Assert.Equal(1, result.GetPixelSpan().Length); - Assert.Equal(default, result.Palette.Span[0]); + Assert.Equal(default, result.Palette[0]); Assert.Equal(0, result.GetPixelSpan()[0]); } @@ -92,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization var actualImage = new Image(1, 256); - ReadOnlySpan paletteSpan = result.Palette.Span; + ReadOnlySpan paletteSpan = result.Palette; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) { @@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); - ReadOnlySpan paletteSpan = result.Palette.Span; + ReadOnlySpan paletteSpan = result.Palette; int paletteCount = result.Palette.Length - 1; for (int y = 0; y < actualImage.Height; y++) {