diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index a07928b04..07a70ad96 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); + /// + /// Gets or sets the color table mode: Global or local. + /// + public GifColorTableMode ColorTableMode { get; set; } + /// public void Encode(Image image, Stream stream) where TPixel : struct, IPixel diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index f84b13f5f..baed04260 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -19,6 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Gif /// internal sealed class GifEncoderCore { + /// + /// Used for allocating memory during procesing operations. + /// private readonly MemoryAllocator memoryAllocator; /// @@ -27,15 +30,20 @@ namespace SixLabors.ImageSharp.Formats.Gif private readonly byte[] buffer = new byte[20]; /// - /// Gets the text encoding used to write comments. + /// The text encoding used to write comments. /// private readonly Encoding textEncoding; /// - /// Gets or sets the quantizer used to generate the color palette. + /// The quantizer used to generate the color palette. /// private readonly IQuantizer quantizer; + /// + /// The color table mode: Global or local. + /// + private readonly GifColorTableMode colorTableMode; + /// /// A flag indicating whether to ingore the metadata when writing the image. /// @@ -56,6 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.memoryAllocator = memoryAllocator; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.quantizer = options.Quantizer; + this.colorTableMode = options.ColorTableMode; this.ignoreMetadata = options.IgnoreMetadata; } @@ -72,28 +81,80 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(stream, nameof(stream)); // Quantize the image returning a palette. - QuantizedFrame quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); + QuantizedFrame quantized = + this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); // Get the number of bits. this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); - int index = this.GetTransparentIndex(quantized); - // Write the header. this.WriteHeader(stream); - // Write the LSD. We'll use local color tables for now. - this.WriteLogicalScreenDescriptor(image, stream, index); + // Write the LSD. + int index = this.GetTransparentIndex(quantized); + bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); + this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream); + + if (useGlobalTable) + { + this.WriteColorTable(quantized, stream); + } - // Write the first frame. + // Write the comments. this.WriteComments(image.MetaData, stream); - // Write additional frames. + // Write application extension to allow additional frames. if (image.Frames.Count > 1) { this.WriteApplicationExtension(stream, image.MetaData.RepeatCount); } + if (useGlobalTable) + { + this.EncodeGlobal(image, quantized, index, stream); + } + else + { + this.EncodeLocal(image, quantized, stream); + } + + // Clean up. + quantized?.Dispose(); + quantized = null; + + // TODO: Write extension etc + stream.WriteByte(GifConstants.EndIntroducer); + } + + private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream) + where TPixel : struct, IPixel + { + var palleteQuantizer = new PaletteQuantizer(this.quantizer.Diffuser); + + for (int i = 0; i < image.Frames.Count; i++) + { + ImageFrame frame = image.Frames[i]; + + this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream); + this.WriteImageDescriptor(frame, false, stream); + + if (i == 0) + { + this.WriteImageData(quantized, stream); + } + else + { + using (QuantizedFrame paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame)) + { + this.WriteImageData(paletteQuantized, stream); + } + } + } + } + + private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream) + where TPixel : struct, IPixel + { foreach (ImageFrame frame in image.Frames) { if (quantized == null) @@ -101,16 +162,14 @@ namespace SixLabors.ImageSharp.Formats.Gif quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(frame); } - this.WriteGraphicalControlExtension(frame.MetaData, stream, this.GetTransparentIndex(quantized)); - this.WriteImageDescriptor(frame, stream); + this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream); + this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); + quantized?.Dispose(); quantized = null; // So next frame can regenerate it } - - // TODO: Write extension etc - stream.WriteByte(GifConstants.EndIntroducer); } /// @@ -159,12 +218,13 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The pixel format. /// The image to encode. - /// The stream to write to. /// The transparency index to set the default background index to. - private void WriteLogicalScreenDescriptor(Image image, Stream stream, int transparencyIndex) + /// Whether to use a global or local color table. + /// The stream to write to. + private void WriteLogicalScreenDescriptor(Image image, int transparencyIndex, bool useGlobalTable, Stream stream) where TPixel : struct, IPixel { - byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(false, this.bitDepth - 1, false, this.bitDepth - 1); + byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); var descriptor = new GifLogicalScreenDescriptor( width: (ushort)image.Width, @@ -243,9 +303,9 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Writes the graphics control extension to the stream. /// /// The metadata of the image or frame. - /// The stream to write to. /// The index of the color in the color palette to make transparent. - private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, Stream stream, int transparencyIndex) + /// The stream to write to. + private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, int transparencyIndex, Stream stream) { byte packedValue = GifGraphicControlExtension.GetPackedValue( disposalMethod: metaData.DisposalMethod, @@ -253,8 +313,8 @@ namespace SixLabors.ImageSharp.Formats.Gif var extension = new GifGraphicControlExtension( packed: packedValue, - transparencyIndex: unchecked((byte)transparencyIndex), - delayTime: (ushort)metaData.FrameDelay); + delayTime: (ushort)metaData.FrameDelay, + transparencyIndex: unchecked((byte)transparencyIndex)); this.WriteExtension(extension, stream); } @@ -281,15 +341,16 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The pixel format. /// The to be encoded. + /// Whether to use the global color table. /// The stream to write to. - private void WriteImageDescriptor(ImageFrame image, Stream stream) + private void WriteImageDescriptor(ImageFrame image, bool hasColorTable, Stream stream) where TPixel : struct, IPixel { byte packedValue = GifImageDescriptor.GetPackedValue( - localColorTableFlag: true, + localColorTableFlag: hasColorTable, interfaceFlag: false, sortFlag: false, - localColorTableSize: (byte)this.bitDepth); // Note: we subtract 1 from the colorTableSize writing + localColorTableSize: (byte)(this.bitDepth - 1)); // Note: we subtract 1 from the colorTableSize writing var descriptor = new GifImageDescriptor( left: 0, @@ -342,9 +403,9 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteImageData(QuantizedFrame image, Stream stream) where TPixel : struct, IPixel { - using (var encoder = new LzwEncoder(this.memoryAllocator, image.Pixels, (byte)this.bitDepth)) + using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) { - encoder.Encode(stream); + encoder.Encode(image.GetPixelSpan(), stream); } } } diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs index 44dd19db6..30e476e7e 100644 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs @@ -25,5 +25,10 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Gets the quantizer used to generate the color palette. /// IQuantizer Quantizer { get; } + + /// + /// Gets the color table mode: Global or local. + /// + GifColorTableMode ColorTableMode { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index de9de5e15..2ec569781 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -58,11 +58,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private const int MaxMaxCode = 1 << MaxBits; - /// - /// The working pixel array. - /// - private readonly byte[] pixelArray; - /// /// The initial code size. /// @@ -83,6 +78,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private readonly byte[] accumulators = new byte[256]; + /// + /// For dynamic table sizing + /// + private readonly int hsize = HashSize; + /// /// The current position within the pixelArray. /// @@ -98,11 +98,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private int maxCode; - /// - /// For dynamic table sizing - /// - private int hsize = HashSize; - /// /// First unused entry /// @@ -169,13 +164,10 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Initializes a new instance of the class. /// /// The to use for buffer allocations. - /// The array of indexed pixels. /// The color depth in bits. - public LzwEncoder(MemoryAllocator memoryAllocator, byte[] indexedPixels, int colorDepth) + public LzwEncoder(MemoryAllocator memoryAllocator, int colorDepth) { - this.pixelArray = indexedPixels; this.initialCodeSize = Math.Max(2, colorDepth); - this.hashTable = memoryAllocator.Allocate(HashSize, true); this.codeTable = memoryAllocator.Allocate(HashSize, true); } @@ -183,8 +175,9 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Encodes and compresses the indexed pixels to the stream. /// + /// The span of indexed pixels. /// The stream to write to. - public void Encode(Stream stream) + public void Encode(Span indexedPixels, Stream stream) { // Write "initial code size" byte stream.WriteByte((byte)this.initialCodeSize); @@ -192,7 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.position = 0; // Compress and write the pixel data - this.Compress(this.initialCodeSize + 1, stream); + this.Compress(indexedPixels, this.initialCodeSize + 1, stream); // Write block terminator stream.WriteByte(GifConstants.Terminator); @@ -252,9 +245,10 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Compress the packets to the stream. /// + /// The span of indexed pixels. /// The initial bits. /// The stream to write to. - private void Compress(int intialBits, Stream stream) + private void Compress(Span indexedPixels, int intialBits, Stream stream) { int fcode; int c; @@ -276,7 +270,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.accumulatorCount = 0; // clear packet - ent = this.NextPixel(); + ent = this.NextPixel(indexedPixels); // TODO: PERF: It looks likt hshift could be calculated once statically. hshift = 0; @@ -296,9 +290,9 @@ namespace SixLabors.ImageSharp.Formats.Gif ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.GetSpan()); ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.GetSpan()); - while (this.position < this.pixelArray.Length) + while (this.position < indexedPixels.Length) { - c = this.NextPixel(); + c = this.NextPixel(indexedPixels); fcode = (c << MaxBits) + ent; int i = (c << hshift) ^ ent /* = 0 */; @@ -373,13 +367,14 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Reads the next pixel from the image. /// + /// The span of indexed pixels. /// /// The /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int NextPixel() + private int NextPixel(Span indexedPixels) { - return this.pixelArray[this.position++] & 0xff; + return indexedPixels[this.position++] & 0xff; } /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 45e8669d6..1b3e84b85 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -86,11 +86,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// private readonly bool writeGamma; - /// - /// Contains the raw pixel data from an indexed image. - /// - private byte[] palettePixelData; - /// /// The image width. /// @@ -188,11 +183,12 @@ namespace SixLabors.ImageSharp.Formats.Png stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); QuantizedFrame quantized = null; + ReadOnlySpan quantizedPixelsSpan = default; if (this.pngColorType == PngColorType.Palette) { // Create quantized frame returning the palette and set the bit depth. quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); - this.palettePixelData = quantized.Pixels; + quantizedPixelsSpan = quantized.GetPixelSpan(); byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk @@ -233,9 +229,11 @@ namespace SixLabors.ImageSharp.Formats.Png this.WritePhysicalChunk(stream, image); this.WriteGammaChunk(stream); - this.WriteDataChunks(image.Frames.RootFrame, stream); + this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream); this.WriteEndChunk(stream); stream.Flush(); + + quantized?.Dispose(); } /// @@ -384,9 +382,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The pixel format. /// The row span. + /// The span of quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, ReadOnlySpan quantizedPixelsSpan, int row) where TPixel : struct, IPixel { switch (this.pngColorType) @@ -394,7 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Png case PngColorType.Palette: int stride = this.rawScanline.Length(); - this.palettePixelData.AsSpan(row * stride, stride).CopyTo(this.rawScanline.GetSpan()); + quantizedPixelsSpan.Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan()); break; case PngColorType.Grayscale: @@ -555,10 +554,11 @@ namespace SixLabors.ImageSharp.Formats.Png { Span colorTableSpan = colorTable.GetSpan(); Span alphaTableSpan = alphaTable.GetSpan(); + Span quantizedSpan = quantized.GetPixelSpan(); for (byte i = 0; i < pixelCount; i++) { - if (quantized.Pixels.Contains(i)) + if (quantizedSpan.IndexOf(i) > -1) { int offset = i * 3; palette[i].ToRgba32(ref rgba); @@ -571,10 +571,10 @@ namespace SixLabors.ImageSharp.Formats.Png if (alpha > this.threshold) { - alpha = 255; + alpha = byte.MaxValue; } - anyAlpha = anyAlpha || alpha < 255; + anyAlpha = anyAlpha || alpha < byte.MaxValue; alphaTableSpan[i] = alpha; } } @@ -635,8 +635,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The pixel format. /// The image. + /// The span of quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, Stream stream) + private void WriteDataChunks(ImageFrame pixels, ReadOnlySpan quantizedPixelsSpan, Stream stream) where TPixel : struct, IPixel { this.bytesPerScanline = this.width * this.bytesPerPixel; @@ -688,7 +689,7 @@ namespace SixLabors.ImageSharp.Formats.Png { for (int y = 0; y < this.height; y++) { - IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)pixels.GetPixelRowSpan(y), y); + IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y); deflateStream.Write(r.Array, 0, resultLength); IManagedByteBuffer temp = this.rawScanline; diff --git a/src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs b/src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs index bf0d80b07..0c21f6e5e 100644 --- a/src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs +++ b/src/ImageSharp/Processing/Quantization/FrameQuantizers/FrameQuantizerBase{TPixel}.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers /// /// If you construct this class with a true value for singlePass, then the code will, when quantizing your image, /// only call the methods. - /// If two passes are required, the code will also call + /// If two passes are required, the code will also call /// and then 'QuantizeImage'. /// protected FrameQuantizerBase(IQuantizer quantizer, bool singlePass) @@ -58,7 +58,6 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers // Get the size of the source image int height = image.Height; int width = image.Width; - byte[] quantizedPixels = new byte[width * height]; // Call the FirstPass function if not a single pass algorithm. // For something like an Octree quantizer, this will run through @@ -69,22 +68,22 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers } // Collect the palette. Required before the second pass runs. - TPixel[] colorPalette = this.GetPalette(); + var quantizedFrame = new QuantizedFrame(image.MemoryAllocator, width, height, this.GetPalette()); if (this.Dither) { // We clone the image as we don't want to alter the original. using (ImageFrame clone = image.Clone()) { - this.SecondPass(clone, quantizedPixels, width, height); + this.SecondPass(clone, quantizedFrame.GetPixelSpan(), width, height); } } else { - this.SecondPass(image, quantizedPixels, width, height); + this.SecondPass(image, quantizedFrame.GetPixelSpan(), width, height); } - return new QuantizedFrame(width, height, colorPalette, quantizedPixels); + return quantizedFrame; } /// @@ -104,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers /// The output pixel array /// The width in pixels of the image /// The height in pixels of the image - protected abstract void SecondPass(ImageFrame source, byte[] output, int width, int height); + protected abstract void SecondPass(ImageFrame source, Span output, int width, int height); /// /// Retrieve the palette for the quantized image. @@ -131,7 +130,13 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers return cache[pixel]; } - // Not found - loop through the palette and find the nearest match. + return this.GetClosestPixelSlow(pixel, colorPalette, cache); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private byte GetClosestPixelSlow(TPixel pixel, TPixel[] colorPalette, Dictionary cache) + { + // Loop through the palette and find the nearest match. byte colorIndex = 0; float leastDistance = int.MaxValue; var vector = pixel.ToVector4(); diff --git a/src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs index e32022254..99519031d 100644 --- a/src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Quantization/FrameQuantizers/OctreeFrameQuantizer{TPixel}.cs @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers } /// - protected override void SecondPass(ImageFrame source, byte[] output, int width, int height) + protected override void SecondPass(ImageFrame source, Span output, int width, int height) { // Load up the values for the first pixel. We can use these to speed up the second // pass of the algorithm by avoiding transforming rows of identical color. @@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers { this.palette[i].ToRgba32(ref trans); - if (trans.Equals(default(Rgba32))) + if (trans.Equals(default)) { index = i; } @@ -185,7 +185,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers } pixel.ToRgba32(ref rgba); - if (rgba.Equals(default(Rgba32))) + if (rgba.Equals(default)) { return this.transparentIndex; } @@ -255,7 +255,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers this.Leaves = 0; this.reducibleNodes = new OctreeNode[9]; this.root = new OctreeNode(0, this.maxColorBits, this); - this.previousColor = default(TPixel); + this.previousColor = default; this.previousNode = null; } @@ -476,9 +476,9 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers int shift = 7 - level; pixel.ToRgba32(ref rgba); - int index = ((rgba.B & Mask[level]) >> (shift - 2)) | - ((rgba.G & Mask[level]) >> (shift - 1)) | - ((rgba.R & Mask[level]) >> shift); + int index = ((rgba.B & Mask[level]) >> (shift - 2)) + | ((rgba.G & Mask[level]) >> (shift - 1)) + | ((rgba.R & Mask[level]) >> shift); OctreeNode child = this.children[index]; @@ -551,10 +551,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers // Loop through children looking for leaves for (int i = 0; i < 8; i++) { - if (this.children[i] != null) - { - this.children[i].ConstructPalette(palette, ref index); - } + this.children[i]?.ConstructPalette(palette, ref index); } } } @@ -577,9 +574,9 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers int shift = 7 - level; pixel.ToRgba32(ref rgba); - int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2)) | - ((rgba.G & Mask[level]) >> (shift - 1)) | - ((rgba.R & Mask[level]) >> shift); + int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2)) + | ((rgba.G & Mask[level]) >> (shift - 1)) + | ((rgba.R & Mask[level]) >> shift); if (this.children[pixelIndex] != null) { diff --git a/src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs index 34cb7eb16..14f4b1c39 100644 --- a/src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs @@ -24,22 +24,23 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers private readonly Dictionary colorMap = new Dictionary(); /// - /// List of all colors in the palette + /// List of all colors in the palette. /// private readonly TPixel[] colors; /// /// Initializes a new instance of the class. /// - /// The palette quantizer - public PaletteFrameQuantizer(PaletteQuantizer quantizer) + /// The palette quantizer. + /// An array of all colors in the palette. + public PaletteFrameQuantizer(PaletteQuantizer quantizer, TPixel[] colors) : base(quantizer, true) { - this.colors = quantizer.GetPalette(); + this.colors = colors; } /// - protected override void SecondPass(ImageFrame source, byte[] output, int width, int height) + protected override void SecondPass(ImageFrame source, Span output, int width, int height) { // Load up the values for the first pixel. We can use these to speed up the second // pass of the algorithm by avoiding transforming rows of identical color. @@ -88,10 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override TPixel[] GetPalette() - { - return this.colors; - } + protected override TPixel[] GetPalette() => this.colors; /// /// Process the pixel in the second pass of the algorithm @@ -101,9 +99,6 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers /// The quantized value /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte QuantizePixel(TPixel pixel) - { - return this.GetClosestPixel(pixel, this.GetPalette(), this.colorMap); - } + private byte QuantizePixel(TPixel pixel) => this.GetClosestPixel(pixel, this.GetPalette(), this.colorMap); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs index 78c4bfbf8..154263959 100644 --- a/src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Quantization/FrameQuantizers/WuFrameQuantizer{TPixel}.cs @@ -251,7 +251,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers } /// - protected override void SecondPass(ImageFrame source, byte[] output, int width, int height) + protected override void SecondPass(ImageFrame source, Span output, int width, int height) { // Load up the values for the first pixel. We can use these to speed up the second // pass of the algorithm by avoiding transforming rows of identical color. @@ -464,6 +464,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers /// /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// + /// The memory allocator used for allocating buffers. private void Get3DMoments(MemoryAllocator memoryAllocator) { Span vwtSpan = this.vwt.GetSpan(); diff --git a/src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs index 8f790dfc9..85cc8334f 100644 --- a/src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Quantization/PaletteQuantizer.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Dithering; using SixLabors.ImageSharp.Processing.Dithering.ErrorDiffusion; @@ -46,19 +47,20 @@ namespace SixLabors.ImageSharp.Processing.Quantization /// public IErrorDiffuser Diffuser { get; } + /// + public IFrameQuantizer CreateFrameQuantizer() + where TPixel : struct, IPixel + => this.CreateFrameQuantizer(() => NamedColors.WebSafePalette); + /// /// Gets the palette to use to quantize the image. /// /// The pixel format. - /// The - public virtual TPixel[] GetPalette() - where TPixel : struct, IPixel - => NamedColors.WebSafePalette; - - /// - public IFrameQuantizer CreateFrameQuantizer() + /// The method to return the palette. + /// The + public virtual IFrameQuantizer CreateFrameQuantizer(Func paletteFunction) where TPixel : struct, IPixel - => new PaletteFrameQuantizer(this); + => new PaletteFrameQuantizer(this, paletteFunction.Invoke()); private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; } diff --git a/src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs b/src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs index 951e47127..5b20805b0 100644 --- a/src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs +++ b/src/ImageSharp/Processing/Quantization/Processors/QuantizeProcessor.cs @@ -36,22 +36,24 @@ namespace SixLabors.ImageSharp.Processing.Quantization.Processors protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { IFrameQuantizer executor = this.Quantizer.CreateFrameQuantizer(); - QuantizedFrame quantized = executor.QuantizeFrame(source); - int paletteCount = quantized.Palette.Length - 1; - - // Not parallel to remove "quantized" closure allocation. - // We can operate directly on the source here as we've already read it to get the - // quantized result - for (int y = 0; y < source.Height; y++) + using (QuantizedFrame quantized = executor.QuantizeFrame(source)) { - Span row = source.GetPixelRowSpan(y); - int yy = y * source.Width; + int paletteCount = quantized.Palette.Length - 1; - for (int x = 0; x < source.Width; x++) + // Not parallel to remove "quantized" closure allocation. + // We can operate directly on the source here as we've already read it to get the + // quantized result + for (int y = 0; y < source.Height; y++) { - int i = x + yy; - TPixel color = quantized.Palette[Math.Min(paletteCount, quantized.Pixels[i])]; - row[x] = color; + Span row = source.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = quantized.GetPixelSpan(); + int yy = y * source.Width; + + for (int x = 0; x < source.Width; x++) + { + int i = x + yy; + row[x] = quantized.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])]; + } } } } diff --git a/src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs index ac87e1c7c..6699c76f4 100644 --- a/src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Quantization/QuantizedFrame{TPixel}.cs @@ -3,39 +3,36 @@ using System; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Memory; +// TODO: Consider pooling the TPixel palette also. For Rgba48+ this would end up on th LOH if 256 colors. namespace SixLabors.ImageSharp.Processing.Quantization { /// /// Represents a quantized image frame where the pixels indexed by a color palette. /// /// The pixel format. - public class QuantizedFrame + public class QuantizedFrame : IDisposable where TPixel : struct, IPixel { + private IBuffer pixels; + /// /// Initializes a new instance of the class. /// + /// Used to allocated memory for image processing operations. /// The image width. /// The image height. /// The color palette. - /// The quantized pixels. - public QuantizedFrame(int width, int height, TPixel[] palette, byte[] pixels) + public QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, TPixel[] palette) { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - Guard.NotNull(palette, nameof(palette)); - Guard.NotNull(pixels, nameof(pixels)); - - if (pixels.Length != width * height) - { - throw new ArgumentException($"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels)); - } this.Width = width; this.Height = height; this.Palette = palette; - this.Pixels = pixels; + this.pixels = memoryAllocator.AllocateCleanManagedByteBuffer(width * height); } /// @@ -51,11 +48,20 @@ namespace SixLabors.ImageSharp.Processing.Quantization /// /// Gets the color palette of this . /// - public TPixel[] Palette { get; } + public TPixel[] Palette { get; private set; } /// /// Gets the pixels of this . /// - public byte[] Pixels { get; } + /// The + public Span GetPixelSpan() => this.pixels.GetSpan(); + + /// + public void Dispose() + { + this.pixels?.Dispose(); + this.pixels = null; + this.Palette = null; + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 91ba160ab..91b331639 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Tests QuantizedFrame quantized = quantizer.CreateFrameQuantizer().QuantizeFrame(frame); int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.Pixels[0]); + Assert.Equal(index, quantized.GetPixelSpan()[0]); } } } @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Tests QuantizedFrame quantized = quantizer.CreateFrameQuantizer().QuantizeFrame(frame); int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.Pixels[0]); + Assert.Equal(index, quantized.GetPixelSpan()[0]); } } } @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Tests QuantizedFrame quantized = quantizer.CreateFrameQuantizer().QuantizeFrame(frame); int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.Pixels[0]); + Assert.Equal(index, quantized.GetPixelSpan()[0]); } } }