diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 585f87b3e8..4c881ec3f2 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -7,7 +7,6 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -54,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The pixel sampling strategy for global quantization. /// - private IPixelSamplingStrategy pixelSamplingStrategy; + private readonly IPixelSamplingStrategy pixelSamplingStrategy; /// /// Initializes a new instance of the class. @@ -150,8 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Gif // 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 pixelMapHasValue = false; + PaletteQuantizer paletteFrameQuantizer = default; + bool quantizerInitialized = false; for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; @@ -166,22 +165,18 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - if (!pixelMapHasValue) + if (!quantizerInitialized) { - pixelMapHasValue = true; - pixelMap = new EuclideanPixelMap(this.configuration, quantized.Palette); + quantizerInitialized = true; + paletteFrameQuantizer = new PaletteQuantizer(this.configuration, this.quantizer.Options, quantized.Palette); } - using var paletteFrameQuantizer = new PaletteQuantizer(this.configuration, this.quantizer.Options, pixelMap, true); using IndexedImageFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.WriteImageData(paletteQuantized, stream); } } - if (pixelMapHasValue) - { - pixelMap.Dispose(); - } + paletteFrameQuantizer.Dispose(); } private void EncodeLocal(Image image, IndexedImageFrame quantized, Stream stream) @@ -310,7 +305,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - ratio = (byte)(((1 / vr) * 64) - 15); + ratio = (byte)((1 / vr * 64) - 15); } } } @@ -354,7 +349,7 @@ namespace SixLabors.ImageSharp.Formats.Gif return; } - for (var i = 0; i < metadata.Comments.Count; i++) + for (int i = 0; i < metadata.Comments.Count; i++) { string comment = metadata.Comments[i]; this.buffer[0] = GifConstants.ExtensionIntroducer; diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 0311c40be4..b82ce71bbd 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -18,20 +18,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// This class is not threadsafe and should not be accessed in parallel. /// Doing so will result in non-idempotent results. /// - internal readonly struct EuclideanPixelMap : IDisposable + internal sealed class EuclideanPixelMap : IDisposable where TPixel : unmanaged, IPixel { - private readonly Rgba32[] rgbaPalette; + private Rgba32[] rgbaPalette; private readonly ColorDistanceCache cache; + private readonly Configuration configuration; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// The configuration. /// The color palette to map from. - [MethodImpl(InliningOptions.ShortMethod)] public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) { + this.configuration = configuration; this.Palette = palette; this.rgbaPalette = new Rgba32[palette.Length]; this.cache = new ColorDistanceCache(configuration.MemoryAllocator); @@ -46,6 +47,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { [MethodImpl(InliningOptions.ShortMethod)] get; + + [MethodImpl(InliningOptions.ShortMethod)] + private set; } /// @@ -72,6 +76,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return index; } + /// + /// Clears the map, resetting it to use the given palette. + /// + /// The color palette to map from. + public void Clear(ReadOnlyMemory palette) + { + this.Palette = palette; + this.rgbaPalette = new Rgba32[palette.Length]; + PixelOperations.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); + this.cache.Clear(); + } + [MethodImpl(InliningOptions.ShortMethod)] private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) { @@ -177,6 +193,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return match > -1; } + /// + /// Clears the cache resetting each entry to empty. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Clear() => this.table.GetSpan().Fill(-1); + [MethodImpl(InliningOptions.ShortMethod)] private static int GetPaletteIndex(int r, int g, int b, int a) => (r << ((IndexBits << 1) + IndexAlphaBits)) diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index 10b26337f4..311a8aa2e0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private IMemoryOwner paletteOwner; private ReadOnlyMemory palette; private EuclideanPixelMap pixelMap; - private bool pixelMapHasValue; private readonly bool isDithering; private bool isDisposed; @@ -48,7 +47,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.octree = new Octree(this.bitDepth); this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); this.pixelMap = default; - this.pixelMapHasValue = false; this.palette = default; this.isDithering = !(this.Options.Dither is null); this.isDisposed = false; @@ -112,15 +110,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.octree.Palletize(paletteSpan, max, ref paletteIndex); ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length); - // When called by QuantizerUtilities.BuildPalette this prevents - // mutiple instances of the map being created but not disposed. - if (this.pixelMapHasValue) + // When called multiple times by QuantizerUtilities.BuildPalette + // this prevents memory churn caused by reallocation. + if (this.pixelMap is null) { - this.pixelMap.Dispose(); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); + } + else + { + this.pixelMap.Clear(result); } - this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - this.pixelMapHasValue = true; this.palette = result; } @@ -153,9 +153,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (!this.isDisposed) { this.isDisposed = true; - this.paletteOwner.Dispose(); + this.paletteOwner?.Dispose(); this.paletteOwner = null; - this.pixelMap.Dispose(); + this.pixelMap?.Dispose(); + this.pixelMap = null; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index a83c760c20..4f73f4ac81 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -58,9 +58,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization var palette = new TPixel[length]; Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); - - var pixelMap = new EuclideanPixelMap(configuration, palette); - return new PaletteQuantizer(configuration, options, pixelMap, false); + return new PaletteQuantizer(configuration, options, palette); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs index 9329bdfebe..284f4fa701 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs @@ -16,32 +16,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal struct PaletteQuantizer : IQuantizer where TPixel : unmanaged, IPixel { - private readonly EuclideanPixelMap pixelMap; - private readonly bool leaveMap; + private EuclideanPixelMap pixelMap; /// /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. /// The quantizer options defining quantization rules. - /// The pixel map for looking up color matches from a predefined palette. - /// - /// to leave the pixel map undisposed after disposing the object; otherwise, . - /// + /// The palette to use. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteQuantizer( - Configuration configuration, - QuantizerOptions options, - EuclideanPixelMap pixelMap, - bool leaveMap) + public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory palette) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); this.Configuration = configuration; this.Options = options; - this.pixelMap = pixelMap; - this.leaveMap = leaveMap; + this.pixelMap = new EuclideanPixelMap(configuration, palette); } /// @@ -72,10 +63,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public void Dispose() { - if (!this.leaveMap) - { - this.pixelMap.Dispose(); - } + this.pixelMap?.Dispose(); + this.pixelMap = null; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index b6f4be4949..bf4a5ca41e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -72,7 +72,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private int maxColors; private readonly Box[] colorCube; private EuclideanPixelMap pixelMap; - private bool pixelMapHasValue; private readonly bool isDithering; private bool isDisposed; @@ -97,7 +96,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.colorCube = new Box[this.maxColors]; this.isDisposed = false; this.pixelMap = default; - this.pixelMapHasValue = false; this.palette = default; this.isDithering = this.isDithering = !(this.Options.Dither is null); } @@ -147,15 +145,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlyMemory result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length); if (this.isDithering) { - // When called by QuantizerUtilities.BuildPalette this prevents - // mutiple instances of the map being created but not disposed. - if (this.pixelMapHasValue) + // When called multiple times by QuantizerUtilities.BuildPalette + // this prevents memory churn caused by reallocation. + if (this.pixelMap is null) { - this.pixelMap.Dispose(); + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); + } + else + { + this.pixelMap.Clear(result); } - - this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - this.pixelMapHasValue = true; } this.palette = result; @@ -201,7 +200,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.momentsOwner = null; this.tagsOwner = null; this.paletteOwner = null; - this.pixelMap.Dispose(); + this.pixelMap?.Dispose(); + this.pixelMap = null; } } @@ -215,16 +215,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The index. [MethodImpl(InliningOptions.ShortMethod)] private static int GetPaletteIndex(int r, int g, int b, int a) - { - return (r << ((IndexBits * 2) + IndexAlphaBits)) - + (r << (IndexBits + IndexAlphaBits + 1)) - + (g << (IndexBits + IndexAlphaBits)) - + (r << (IndexBits * 2)) - + (r << (IndexBits + 1)) - + (g << IndexBits) - + ((r + g + b) << IndexAlphaBits) - + r + g + b + a; - } + => (r << ((IndexBits * 2) + IndexAlphaBits)) + + (r << (IndexBits + IndexAlphaBits + 1)) + + (g << (IndexBits + IndexAlphaBits)) + + (r << (IndexBits * 2)) + + (r << (IndexBits + 1)) + + (g << IndexBits) + + ((r + g + b) << IndexAlphaBits) + + r + g + b + a; /// /// Computes sum over a box of any given statistic. @@ -233,24 +231,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The moment. /// The result. private static Moment Volume(ref Box cube, ReadOnlySpan moments) - { - return moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] - - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] - - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] - + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] - - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] - + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] - + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] - - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] - + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] - + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] - - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] - - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; - } + => moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; /// /// Computes part of Volume(cube, moment) that doesn't depend on RMax, GMax, BMax, or AMax (depending on direction). @@ -835,7 +831,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public int Volume; /// - public readonly override bool Equals(object obj) + public override readonly bool Equals(object obj) => obj is Box box && this.Equals(box); @@ -852,7 +848,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization && this.Volume == other.Volume; /// - public readonly override int GetHashCode() + public override readonly int GetHashCode() { HashCode hash = default; hash.Add(this.RMin);