From e535d1d4091b063d106f1b4fb2b19eccde84c472 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 16 Feb 2020 20:55:12 +1100 Subject: [PATCH] Add dither scaling and simplify API. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 23 ++- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 5 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 18 ++- .../Formats/Png/PngEncoderOptionsHelpers.cs | 3 +- .../Processors/Dithering/ErrorDither.cs | 5 +- .../Processors/Dithering/IDither.cs | 4 +- .../Processors/Dithering/OrderedDither.cs | 8 +- .../Dithering/PaletteDitherProcessor.cs | 7 +- .../PaletteDitherProcessor{TPixel}.cs | 18 ++- .../Quantization/FrameQuantizer{TPixel}.cs | 74 ++++----- .../Quantization/IFrameQuantizer{TPixel}.cs | 9 +- .../Processors/Quantization/IQuantizer.cs | 17 +- .../OctreeFrameQuantizer{TPixel}.cs | 25 +-- .../Quantization/OctreeQuantizer.cs | 76 ++------- .../PaletteFrameQuantizer{TPixel}.cs | 11 +- .../Quantization/PaletteQuantizer.cs | 60 +++---- .../Quantization/QuantizerConstants.cs | 23 ++- .../Quantization/QuantizerOptions.cs | 42 +++++ .../Quantization/WebSafePaletteQuantizer.cs | 21 +-- .../Quantization/WernerPaletteQuantizer.cs | 23 +-- .../Quantization/WuFrameQuantizer{TPixel}.cs | 26 +-- .../Processors/Quantization/WuQuantizer.cs | 69 ++------ .../ImageSharp.Benchmarks/Codecs/EncodeGif.cs | 11 +- .../Codecs/EncodeGifMultiple.cs | 7 +- .../Codecs/EncodeIndexedPng.cs | 10 +- .../Formats/Bmp/BmpEncoderTests.cs | 4 +- .../Formats/Gif/GifEncoderTests.cs | 6 +- .../Formats/Png/PngEncoderTests.cs | 2 +- .../Quantization/OctreeQuantizerTests.cs | 50 +++--- .../Quantization/PaletteQuantizerTests.cs | 48 +++--- .../Processors/Quantization/QuantizerTests.cs | 149 ++++++++++++++++-- .../Quantization/WuQuantizerTests.cs | 50 +++--- .../Quantization/QuantizedImageTests.cs | 36 +++-- .../Quantization/WuQuantizerTests.cs | 10 +- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/david.png | Bin 0 -> 27218 bytes 36 files changed, 511 insertions(+), 440 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs create mode 100644 tests/Images/Input/Png/david.png diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 995aee91d5..435fdc4fc6 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; @@ -82,6 +83,7 @@ namespace SixLabors.ImageSharp.Advanced // This is we actually call all the individual methods you need to seed. AotCompileOctreeQuantizer(); AotCompileWuQuantizer(); + AotCompilePaletteQuantizer(); AotCompileDithering(); AotCompilePixelOperations(); @@ -109,7 +111,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileOctreeQuantizer() where TPixel : struct, IPixel { - using (var test = new OctreeFrameQuantizer(Configuration.Default, new OctreeQuantizer(false))) + using (var test = new OctreeFrameQuantizer(Configuration.Default, new OctreeQuantizer().Options)) { test.AotGetPalette(); } @@ -122,7 +124,22 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileWuQuantizer() where TPixel : struct, IPixel { - using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer(false))) + using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer().Options)) + { + var frame = new ImageFrame(Configuration.Default, 1, 1); + test.QuantizeFrame(frame, frame.Bounds()); + test.AotGetPalette(); + } + } + + /// + /// This method pre-seeds the PaletteQuantizer in the AoT compiler for iOS. + /// + /// The pixel format. + private static void AotCompilePaletteQuantizer() + where TPixel : struct, IPixel + { + using (var test = (PaletteFrameQuantizer)new PaletteQuantizer(Array.Empty()).CreateFrameQuantizer(Configuration.Default)) { var frame = new ImageFrame(Configuration.Default, 1, 1); test.QuantizeFrame(frame, frame.Bounds()); @@ -141,7 +158,7 @@ namespace SixLabors.ImageSharp.Advanced TPixel pixel = default; using (var image = new ImageFrame(Configuration.Default, 1, 1)) { - test.Dither(image, default, pixel, pixel, 0, 0, 0); + test.Dither(image, default, pixel, pixel, 0, 0, 0, 0); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index a1c415f76e..2d6b06111d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp @@ -87,7 +88,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; this.writeV4Header = options.SupportTransparency; - this.quantizer = options.Quantizer ?? new OctreeQuantizer(dither: true, maxColors: 256); + this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; } /// @@ -335,7 +336,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : struct, IPixel { - using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration, 256); + using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); using IQuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); ReadOnlySpan quantizedColors = quantized.Palette.Span; diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 8577ab4768..0307f7d94b 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -144,13 +144,10 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - using (IFrameQuantizer paletteFrameQuantizer = - new PaletteFrameQuantizer(this.configuration, this.quantizer.Dither, quantized.Palette)) + using (IFrameQuantizer paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette)) + using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) { - using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) - { - this.WriteImageData(paletteQuantized, stream); - } + this.WriteImageData(paletteQuantized, stream); } } } @@ -171,7 +168,14 @@ namespace SixLabors.ImageSharp.Formats.Gif if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength && frameMetadata.ColorTableLength > 0) { - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, frameMetadata.ColorTableLength)) + var options = new QuantizerOptions + { + Dither = this.quantizer.Options.Dither, + DitherScale = this.quantizer.Options.DitherScale, + MaxColors = frameMetadata.ColorTableLength + }; + + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options)) { quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index dc3d9d3ce6..c29ec578c1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -72,7 +72,8 @@ namespace SixLabors.ImageSharp.Formats.Png // Use the metadata to determine what quantization depth to use if no quantizer has been set. if (options.Quantizer is null) { - options.Quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits)); + var maxColors = ImageMaths.GetColorCountForBitDepth(bits); + options.Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = maxColors }); } // Create quantized frame returning the palette and set the bit depth. diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 91ca4e95ef..92db4638be 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -38,7 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering TPixel transformed, int x, int y, - int bitDepth) + int bitDepth, + float scale) where TPixel : struct, IPixel { // Equal? Break out as there's no error to pass. @@ -48,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } // Calculate the error - Vector4 error = source.ToVector4() - transformed.ToVector4(); + Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale; int offset = this.offset; DenseMatrix matrix = this.matrix; diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index 0d7841884b..dc48b7e6d2 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -28,6 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// The column index. /// The row index. /// The bit depth of the target palette. + /// The dithering scale used to adjust the amount of dither. Range 0..1. /// The pixel format. /// The dithered result for the source pixel. TPixel Dither( @@ -37,7 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering TPixel transformed, int x, int y, - int bitDepth) + int bitDepth, + float scale) where TPixel : struct, IPixel; } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index c3277e3266..2e66ae86ff 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -54,20 +54,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering TPixel transformed, int x, int y, - int bitDepth) + int bitDepth, + float scale) where TPixel : struct, IPixel { - // TODO: Should we consider a pixel format with a larger coror range? Rgba32 rgba = default; source.ToRgba32(ref rgba); Rgba32 attempt; - // Srpead assumes an even colorspace distribution and precision. + // Spread assumes an even colorspace distribution and precision. // Calculated as 0-255/component count. 256 / bitDepth // https://bisqwit.iki.fi/story/howto/dither/jy/ // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm int spread = 256 / bitDepth; - float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX]; + float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale; attempt.R = (byte)(rgba.R + factor).Clamp(byte.MinValue, byte.MaxValue); attempt.G = (byte)(rgba.G + factor).Clamp(byte.MinValue, byte.MaxValue); diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs index c7abb308f3..40949bb284 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs @@ -32,10 +32,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } /// - /// Gets the dithering algorithm. + /// Gets the dithering algorithm to apply to the output image. /// public IDither Dither { get; } + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + public float DitherScale { get; } + /// /// Gets the palette to select substitute colors from. /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index bdcc9e6b89..315ce22e08 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -20,6 +20,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering private readonly int paletteLength; private readonly int bitDepth; private readonly IDither dither; + private readonly float ditherScale; private readonly ReadOnlyMemory sourcePalette; private IMemoryOwner palette; private EuclideanPixelMap pixelMap; @@ -38,6 +39,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering this.paletteLength = definition.Palette.Span.Length; this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(this.paletteLength); this.dither = definition.Dither; + this.ditherScale = definition.DitherScale; this.sourcePalette = definition.Palette; } @@ -58,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering { TPixel sourcePixel = row[x]; this.pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); - this.dither.Dither(source, interest, sourcePixel, transformed, x, y, this.bitDepth); + this.dither.Dither(source, interest, sourcePixel, transformed, x, y, this.bitDepth, this.ditherScale); row[x] = transformed; } } @@ -67,7 +69,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } // Ordered dithering. We are only operating on a single pixel so we can work in parallel. - var ditherOperation = new DitherRowIntervalOperation(source, interest, this.pixelMap, this.dither, this.bitDepth); + var ditherOperation = new DitherRowIntervalOperation( + source, + interest, + this.pixelMap, + this.dither, + this.ditherScale, + this.bitDepth); + ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -114,6 +123,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering private readonly Rectangle bounds; private readonly EuclideanPixelMap pixelMap; private readonly IDither dither; + private readonly float scale; private readonly int bitDepth; [MethodImpl(InliningOptions.ShortMethod)] @@ -122,12 +132,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering Rectangle bounds, EuclideanPixelMap pixelMap, IDither dither, + float scale, int bitDepth) { this.source = source; this.bounds = bounds; this.pixelMap = pixelMap; this.dither = dither; + this.scale = scale; this.bitDepth = bitDepth; } @@ -143,7 +155,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel dithered = dither.Dither(this.source, this.bounds, row[x], transformed, x, y, this.bitDepth); + TPixel dithered = dither.Dither(this.source, this.bounds, row[x], transformed, x, y, this.bitDepth, this.scale); this.pixelMap.GetClosestColor(dithered, out transformed); row[x] = transformed; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs index 1914ed8915..0d3b7de6d6 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs @@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public abstract class FrameQuantizer : IFrameQuantizer where TPixel : struct, IPixel { - /// - /// Flag used to indicate whether a single pass or two passes are needed for quantization. - /// private readonly bool singlePass; - private EuclideanPixelMap pixelMap; private bool isDisposed; @@ -29,57 +25,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The quantizer. + /// The quantizer options defining quantization rules. /// - /// If true, the quantization process only needs to loop through the source pixels once. + /// If , the quantization process only needs to loop through the source pixels once. /// /// /// If you construct this class with a true for , then the code will /// only call the method. /// If two passes are required, the code will also call . /// - protected FrameQuantizer(Configuration configuration, IQuantizer quantizer, bool singlePass) + protected FrameQuantizer(Configuration configuration, QuantizerOptions options, bool singlePass) { - Guard.NotNull(quantizer, nameof(quantizer)); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); this.Configuration = configuration; - this.Dither = quantizer.Dither; - this.DoDither = this.Dither != null; + this.Options = options; + this.IsDitheringQuantizer = options.Dither != null; this.singlePass = singlePass; } - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The diffuser - /// - /// If true, the quantization process only needs to loop through the source pixels once - /// - /// - /// If you construct this class with a true for , then the code will - /// only call the method. - /// If two passes are required, the code will also call . - /// - protected FrameQuantizer(Configuration configuration, IDither diffuser, bool singlePass) - { - this.Configuration = configuration; - this.Dither = diffuser; - this.DoDither = this.Dither != null; - this.singlePass = singlePass; - } - - /// - public IDither Dither { get; } - - /// - public bool DoDither { get; } + /// + public QuantizerOptions Options { get; } /// /// Gets the configuration which allows altering default behaviour or extending the library. /// protected Configuration Configuration { get; } + /// + /// Gets a value indicating whether the frame quantizer utilizes a dithering method. + /// + protected bool IsDitheringQuantizer { get; } + /// public void Dispose() { @@ -109,7 +87,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); Memory output = quantizedFrame.GetWritablePixelMemory(); - if (this.DoDither) + if (this.Options.Dither is null) + { + this.SecondPass(image, interest, output, palette); + } + else { // We clone the image as we don't want to alter the original via error diffusion based dithering. using (ImageFrame clone = image.Clone()) @@ -117,10 +99,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization this.SecondPass(clone, interest, output, palette); } } - else - { - this.SecondPass(image, interest, output, palette); - } return quantizedFrame; } @@ -162,7 +140,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlyMemory palette) { ReadOnlySpan paletteSpan = palette.Span; - if (!this.DoDither) + IDither dither = this.Options.Dither; + + if (dither is null) { var operation = new RowIntervalOperation(source, output, bounds, this, palette); ParallelRowIterator.IterateRows( @@ -179,8 +159,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Span outputSpan = output.Span; int bitDepth = ImageMaths.GetBitsNeededForColorDepth(paletteSpan.Length); - if (this.Dither.DitherType == DitherType.ErrorDiffusion) + if (dither.DitherType == DitherType.ErrorDiffusion) { + float ditherScale = this.Options.DitherScale; int width = bounds.Width; int offsetY = bounds.Top; int offsetX = bounds.Left; @@ -193,7 +174,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { TPixel sourcePixel = row[x]; outputSpan[rowStart + x - offsetX] = this.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); - this.Dither.Dither(source, bounds, sourcePixel, transformed, x, y, bitDepth); + dither.Dither(source, bounds, sourcePixel, transformed, x, y, bitDepth, ditherScale); } } @@ -306,7 +287,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization int width = this.bounds.Width; int offsetY = this.bounds.Top; int offsetX = this.bounds.Left; - IDither dither = this.quantizer.Dither; + IDither dither = this.quantizer.Options.Dither; + float scale = this.quantizer.Options.DitherScale; TPixel transformed = default; for (int y = rows.Min; y < rows.Max; y++) @@ -316,7 +298,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel dithered = dither.Dither(this.source, this.bounds, row[x], transformed, x, y, this.bitDepth); + TPixel dithered = dither.Dither(this.source, this.bounds, row[x], transformed, x, y, this.bitDepth, scale); outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 30d58ab0b1..5913179025 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -15,14 +15,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel { /// - /// Gets a value indicating whether to apply dithering to the output image. + /// Gets the quantizer options defining quantization rules. /// - bool DoDither { get; } - - /// - /// Gets the algorithm to apply to the output image. - /// - IDither Dither { get; } + QuantizerOptions Options { get; } /// /// Quantize an image frame and return the resulting output pixels. diff --git a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs index 7bf58b31f8..2daddf1057 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { @@ -12,27 +11,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public interface IQuantizer { /// - /// Gets the dithering algorithm to apply to the output image. + /// Gets the quantizer options defining quantization rules. /// - IDither Dither { get; } + QuantizerOptions Options { get; } /// - /// Creates the generic frame quantizer + /// Creates the generic frame quantizer. /// /// The to configure internal operations. /// The pixel format. - /// The + /// The . IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel; /// - /// Creates the generic frame quantizer + /// Creates the generic frame quantizer. /// /// The pixel format. /// The to configure internal operations. - /// The maximum number of colors to hold in the color palette. - /// The - IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// The options to create the quantizer with. + /// The . + IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 643507351b..4fecc5702a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -39,30 +39,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The octree quantizer + /// The quantizer options defining quantization rules. /// /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, /// the second pass quantizes a color based on the nodes in the tree /// - public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer) - : this(configuration, quantizer, quantizer.MaxColors) + public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions options) + : base(configuration, options, false) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The octree quantizer. - /// The maximum number of colors to hold in the color palette. - /// - /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, - /// the second pass quantizes a color based on the nodes in the tree - /// - public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer, int maxColors) - : base(configuration, quantizer, false) - { - this.colors = maxColors; + this.colors = this.Options.MaxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); } @@ -95,7 +80,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // Octree only maps the RGB component of a color // so cannot tell the difference between a fully transparent // pixel and a black one. - if (!this.DoDither && !color.Equals(default)) + if (!this.IsDitheringQuantizer && !color.Equals(default)) { var index = (byte)this.octree.GetPaletteIndex(color); match = palette[index]; diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 06578354c0..a5660c43b4 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -2,97 +2,45 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Allows the quantization of images pixels using Octrees. /// - /// - /// By default the quantizer uses dithering and a color palette of a maximum length of 255 - /// /// public class OctreeQuantizer : IQuantizer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// using the default . /// public OctreeQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of colors to hold in the color palette. - public OctreeQuantizer(int maxColors) - : this(GetDiffuser(true), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image. - public OctreeQuantizer(bool dither) - : this(GetDiffuser(dither), QuantizerConstants.MaxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image. - /// The maximum number of colors to hold in the color palette. - public OctreeQuantizer(bool dither, int maxColors) - : this(GetDiffuser(dither), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The dithering algorithm, if any, to apply to the output image. - public OctreeQuantizer(IDither diffuser) - : this(diffuser, QuantizerConstants.MaxColors) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The dithering algorithm, if any, to apply to the output image. - /// The maximum number of colors to hold in the color palette. - public OctreeQuantizer(IDither dither, int maxColors) + /// The quantizer options defining quantization rules. + public OctreeQuantizer(QuantizerOptions options) { - this.Dither = dither; - this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); + Guard.NotNull(options, nameof(options)); + this.Options = options; } /// - public IDither Dither { get; } - - /// - /// Gets the maximum number of colors to hold in the color palette. - /// - public int MaxColors { get; } + public QuantizerOptions Options { get; } - /// /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - => new OctreeFrameQuantizer(configuration, this); + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel - { - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - return new OctreeFrameQuantizer(configuration, this, maxColors); - } - - private static IDither GetDiffuser(bool dither) => dither ? KnownDitherings.FloydSteinberg : null; + => new OctreeFrameQuantizer(configuration, options); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index f60e6d79a7..453c1d5dcc 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -4,7 +4,6 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { @@ -25,13 +24,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The palette quantizer. - /// An array of all colors in the palette. - public PaletteFrameQuantizer(Configuration configuration, IDither diffuser, ReadOnlyMemory colors) - : base(configuration, diffuser, true) => this.palette = colors; + /// The quantizer options defining quantization rules. + /// A containing all colors in the palette. + public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory colors) + : base(configuration, options, true) => this.palette = colors; /// [MethodImpl(InliningOptions.ShortMethod)] protected override ReadOnlyMemory GenerateQuantizedPalette() => this.palette; + + internal ReadOnlyMemory AotGetPalette() => this.GenerateQuantizedPalette(); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index fd2e6052ee..c1198c58f7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -2,80 +2,62 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Allows the quantization of images pixels using color palettes. - /// Override this class to provide your own palette. - /// - /// By default the quantizer uses dithering. - /// /// public class PaletteQuantizer : IQuantizer { /// /// Initializes a new instance of the class. /// - /// The palette. + /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, true) + : this(palette, new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The palette. - /// Whether to apply dithering to the output image - public PaletteQuantizer(ReadOnlyMemory palette, bool dither) - : this(palette, GetDiffuser(dither)) + /// The color palette. + /// The quantizer options defining quantization rules. + public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) { - } + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + Guard.NotNull(options, nameof(options)); - /// - /// Initializes a new instance of the class. - /// - /// The palette. - /// The dithering algorithm, if any, to apply to the output image - public PaletteQuantizer(ReadOnlyMemory palette, IDither dither) - { this.Palette = palette; - this.Dither = dither; + this.Options = options; } - /// - public IDither Dither { get; } - /// - /// Gets the palette. + /// Gets the color palette. /// public ReadOnlyMemory Palette { get; } + /// + public QuantizerOptions Options { get; } + /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - { - var palette = new TPixel[this.Palette.Length]; - Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Dither, palette); - } + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel { - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - int max = Math.Min(maxColors, this.Palette.Length); + Guard.NotNull(options, nameof(options)); - var palette = new TPixel[max]; - Color.ToPixel(configuration, this.Palette.Span.Slice(0, max), palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Dither, palette); - } + int length = Math.Min(this.Palette.Span.Length, options.MaxColors); + var palette = new TPixel[length]; - private static IDither GetDiffuser(bool dither) => dither ? KnownDitherings.FloydSteinberg : null; + Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); + return new PaletteFrameQuantizer(configuration, options, palette); + } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs index d79a91c301..ece3777e0e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs @@ -1,12 +1,14 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Processing.Processors.Dithering; + namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Contains color quantization specific constants. /// - internal static class QuantizerConstants + public static class QuantizerConstants { /// /// The minimum number of colors to use when quantizing an image. @@ -17,5 +19,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The maximum number of colors to use when quantizing an image. /// public const int MaxColors = 256; + + /// + /// The minumim dithering scale used to adjust the amount of dither. + /// + public const float MinDitherScale = 0; + + /// + /// The max dithering scale used to adjust the amount of dither. + /// + public const float MaxDitherScale = 1F; + + /// + /// Gets the default dithering algorithm to use. + /// + public static IDither DefaultDither { get; } = KnownDitherings.FloydSteinberg; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs new file mode 100644 index 0000000000..5c1daf183b --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// Defines options for quantization. + /// + public class QuantizerOptions + { + private float ditherScale = QuantizerConstants.MaxDitherScale; + private int maxColors = QuantizerConstants.MaxColors; + + /// + /// Gets or sets the algorithm to apply to the output image. + /// Defaults to ; set to for no dithering. + /// + public IDither Dither { get; set; } = QuantizerConstants.DefaultDither; + + /// + /// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1. + /// Defaults to . + /// + public float DitherScale + { + get { return this.ditherScale; } + set { this.ditherScale = value.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } + } + + /// + /// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256. + /// Defaults to . + /// + public int MaxColors + { + get { return this.maxColors; } + set { this.maxColors = value.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index ff965e3930..8aa634b9ff 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -14,26 +14,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(true) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// Whether to apply dithering to the output image - public WebSafePaletteQuantizer(bool dither) - : base(Color.WebSafePalette, dither) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error diffusion algorithm, if any, to apply to the output image - public WebSafePaletteQuantizer(IDither diffuser) - : base(Color.WebSafePalette, diffuser) + /// The quantizer options defining quantization rules. + public WebSafePaletteQuantizer(QuantizerOptions options) + : base(Color.WebSafePalette, options) { } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 3b48ddedac..168c837d57 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -1,8 +1,6 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Processing.Processors.Dithering; - namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -15,26 +13,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image - public WernerPaletteQuantizer(bool dither) - : base(Color.WernerPalette, dither) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image - public WernerPaletteQuantizer(IDither diffuser) - : base(Color.WernerPalette, diffuser) + /// The quantizer options defining quantization rules. + public WernerPaletteQuantizer(QuantizerOptions options) + : base(Color.WernerPalette, options) { } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 75b922e347..0a46cd302e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -96,33 +96,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The Wu quantizer + /// The quantizer options defining quantization rules. /// /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram, /// the second pass quantizes a color based on the position in the histogram. /// - public WuFrameQuantizer(Configuration configuration, WuQuantizer quantizer) - : this(configuration, quantizer, quantizer.MaxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The Wu quantizer. - /// The maximum number of colors to hold in the color palette. - /// - /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram, - /// the second pass quantizes a color based on the position in the histogram. - /// - public WuFrameQuantizer(Configuration configuration, WuQuantizer quantizer, int maxColors) - : base(configuration, quantizer, false) + public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) + : base(configuration, options, false) { this.memoryAllocator = this.Configuration.MemoryAllocator; this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.colors = maxColors; + this.colors = this.Options.MaxColors; } /// @@ -185,9 +170,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(InliningOptions.ShortMethod)] protected override byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { - if (!this.DoDither) + if (!this.IsDitheringQuantizer) { - // Expected order r->g->b->a Rgba32 rgba = default; color.ToRgba32(ref rgba); diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 682b6ec64f..b8c54f467e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -2,89 +2,44 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Allows the quantization of images pixels using Xiaolin Wu's Color Quantizer - /// - /// By default the quantizer uses dithering and a color palette of a maximum length of 255 - /// /// public class WuQuantizer : IQuantizer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// using the default . /// public WuQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of colors to hold in the color palette - public WuQuantizer(int maxColors) - : this(GetDiffuser(true), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image - public WuQuantizer(bool dither) - : this(GetDiffuser(dither), QuantizerConstants.MaxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The dithering algorithm, if any, to apply to the output image - public WuQuantizer(IDither diffuser) - : this(diffuser, QuantizerConstants.MaxColors) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The dithering algorithm, if any, to apply to the output image - /// The maximum number of colors to hold in the color palette - public WuQuantizer(IDither dither, int maxColors) + /// The quantizer options defining quantization rules. + public WuQuantizer(QuantizerOptions options) { - this.Dither = dither; - this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); + Guard.NotNull(options, nameof(options)); + this.Options = options; } /// - public IDither Dither { get; } - - /// - /// Gets the maximum number of colors to hold in the color palette. - /// - public int MaxColors { get; } + public QuantizerOptions Options { get; } /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - return new WuFrameQuantizer(configuration, this); - } + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - return new WuFrameQuantizer(configuration, this, maxColors); - } - - private static IDither GetDiffuser(bool dither) => dither ? KnownDitherings.FloydSteinberg : null; + => new WuFrameQuantizer(configuration, options); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 89eb63d629..8983d30409 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.Drawing.Imaging; @@ -6,6 +6,7 @@ using System.IO; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests; using SDImage = System.Drawing.Image; @@ -53,11 +54,15 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public void GifCore() { // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.BayerDither4x4 }) + }; + using (var memoryStream = new MemoryStream()) { this.bmpCore.SaveAsGif(memoryStream, options); } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs index 4d93d89af2..e21fbfc612 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Drawing.Imaging; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Benchmarks.Codecs @@ -23,7 +24,11 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs this.ForEachImageSharpImage((img, ms) => { // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.BayerDither4x4 }) + }; + img.Save(ms, options); return null; }); diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs index 639d1594ee..aedf9cd777 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.IO; @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new OctreeQuantizer(false) }; + var options = new PngEncoder { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } @@ -95,9 +95,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new WuQuantizer(false) }; + var options = new PngEncoder { Quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 55d31b5a38..10be33a97a 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, - Quantizer = new WuQuantizer(256) + Quantizer = new WuQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); @@ -223,7 +223,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, - Quantizer = new OctreeQuantizer(256) + Quantizer = new OctreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index fe1faa5aed..ea1eb700a7 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { // Use the palette quantizer without dithering to ensure results // are consistent - Quantizer = new WebSafePaletteQuantizer(false) + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif var encoder = new GifEncoder { ColorTableMode = GifColorTableMode.Global, - Quantizer = new OctreeQuantizer(false) + Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -141,7 +141,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif var encoder = new GifEncoder { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(frameMetadata.ColorTableLength) + Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) }; image.Save(outStream, encoder); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index f5b06eb6c3..2fa1657e66 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -428,7 +428,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FilterMethod = pngFilterMethod, CompressionLevel = compressionLevel, BitDepth = bitDepth, - Quantizer = new WuQuantizer(paletteSize), + Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }), InterlaceMethod = interlaceMode }; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs index 69a681bb36..bb7921d686 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs @@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization [Fact] public void OctreeQuantizerConstructor() { - var quantizer = new OctreeQuantizer(128); - - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDitherings.FloydSteinberg, quantizer.Dither); - - quantizer = new OctreeQuantizer(false); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Dither); - - quantizer = new OctreeQuantizer(KnownDitherings.Atkinson); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDitherings.Atkinson, quantizer.Dither); - - quantizer = new OctreeQuantizer(KnownDitherings.Atkinson, 128); - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDitherings.Atkinson, quantizer.Dither); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new OctreeQuantizer(expected); + + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = null }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] @@ -38,23 +42,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.FloydSteinberg, frameQuantizer.Dither); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(false); + quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.DoDither); - Assert.Null(frameQuantizer.Dither); + Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(KnownDitherings.Atkinson); + quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Dither); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index a348deb654..3c1fa11ab0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -10,49 +10,55 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization { public class PaletteQuantizerTests { - private static readonly Color[] Rgb = new Color[] { Color.Red, Color.Green, Color.Blue }; + private static readonly Color[] Palette = new Color[] { Color.Red, Color.Green, Color.Blue }; [Fact] public void PaletteQuantizerConstructor() { - var quantizer = new PaletteQuantizer(Rgb); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new PaletteQuantizer(Palette, expected); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDitherings.FloydSteinberg, quantizer.Dither); + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); - quantizer = new PaletteQuantizer(Rgb, false); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Null(quantizer.Dither); + expected = new QuantizerOptions { Dither = null }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); - quantizer = new PaletteQuantizer(Rgb, KnownDitherings.Atkinson); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDitherings.Atkinson, quantizer.Dither); + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] public void PaletteQuantizerCanCreateFrameQuantizer() { - var quantizer = new PaletteQuantizer(Rgb); + var quantizer = new PaletteQuantizer(Palette); IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.FloydSteinberg, frameQuantizer.Dither); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new PaletteQuantizer(Rgb, false); + quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.DoDither); - Assert.Null(frameQuantizer.Dither); + Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new PaletteQuantizer(Rgb, KnownDitherings.Atkinson); + quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Dither); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); } @@ -60,14 +66,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization public void KnownQuantizersWebSafeTests() { IQuantizer quantizer = KnownQuantizers.WebSafe; - Assert.Equal(KnownDitherings.FloydSteinberg, quantizer.Dither); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } [Fact] public void KnownQuantizersWernerTests() { IQuantizer quantizer = KnownQuantizers.Werner; - Assert.Equal(KnownDitherings.FloydSteinberg, quantizer.Dither); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index efad57d5b9..d3e8b034be 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -17,21 +17,128 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization TestImages.Png.Bike }; + private static readonly QuantizerOptions NoDitherOptions = new QuantizerOptions { Dither = null }; + private static readonly QuantizerOptions DiffuserDitherOptions = new QuantizerOptions { Dither = KnownDitherings.FloydSteinberg }; + private static readonly QuantizerOptions OrderedDitherOptions = new QuantizerOptions { Dither = KnownDitherings.BayerDither8x8 }; + + private static readonly QuantizerOptions Diffuser0_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = 0F + }; + + private static readonly QuantizerOptions Diffuser0_25_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .25F + }; + + private static readonly QuantizerOptions Diffuser0_5_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .5F + }; + + private static readonly QuantizerOptions Diffuser0_75_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .75F + }; + + private static readonly QuantizerOptions Ordered0_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.BayerDither8x8, + DitherScale = 0F + }; + + private static readonly QuantizerOptions Ordered0_25_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.BayerDither8x8, + DitherScale = .25F + }; + + private static readonly QuantizerOptions Ordered0_5_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.BayerDither8x8, + DitherScale = .5F + }; + + private static readonly QuantizerOptions Ordered0_75_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.BayerDither8x8, + DitherScale = .75F + }; + public static readonly TheoryData Quantizers = new TheoryData { + // Known uses error diffusion by default. KnownQuantizers.Octree, KnownQuantizers.WebSafe, KnownQuantizers.Werner, KnownQuantizers.Wu, - new OctreeQuantizer(false), - new WebSafePaletteQuantizer(false), - new WernerPaletteQuantizer(false), - new WuQuantizer(false), - new OctreeQuantizer(KnownDitherings.BayerDither8x8), - new WebSafePaletteQuantizer(KnownDitherings.BayerDither8x8), - new WernerPaletteQuantizer(KnownDitherings.BayerDither8x8), - new WuQuantizer(KnownDitherings.BayerDither8x8) + new OctreeQuantizer(NoDitherOptions), + new WebSafePaletteQuantizer(NoDitherOptions), + new WernerPaletteQuantizer(NoDitherOptions), + new WuQuantizer(NoDitherOptions), + new OctreeQuantizer(OrderedDitherOptions), + new WebSafePaletteQuantizer(OrderedDitherOptions), + new WernerPaletteQuantizer(OrderedDitherOptions), + new WuQuantizer(OrderedDitherOptions) + }; + + public static readonly TheoryData DitherScaleQuantizers + = new TheoryData + { + new OctreeQuantizer(Diffuser0_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions), + new WuQuantizer(Diffuser0_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions), + new WuQuantizer(Diffuser0_25_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions), + new WuQuantizer(Diffuser0_5_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions), + new WuQuantizer(Diffuser0_75_ScaleDitherOptions), + + new OctreeQuantizer(DiffuserDitherOptions), + new WebSafePaletteQuantizer(DiffuserDitherOptions), + new WernerPaletteQuantizer(DiffuserDitherOptions), + new WuQuantizer(DiffuserDitherOptions), + + new OctreeQuantizer(Ordered0_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions), + new WuQuantizer(Ordered0_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_25_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions), + new WuQuantizer(Ordered0_25_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_5_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions), + new WuQuantizer(Ordered0_5_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_75_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions), + new WuQuantizer(Ordered0_75_ScaleDitherOptions), + + new OctreeQuantizer(OrderedDitherOptions), + new WebSafePaletteQuantizer(OrderedDitherOptions), + new WernerPaletteQuantizer(OrderedDitherOptions), + new WuQuantizer(OrderedDitherOptions), }; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f); @@ -42,8 +149,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization where TPixel : struct, IPixel { string quantizerName = quantizer.GetType().Name; - string ditherName = quantizer.Dither?.GetType()?.Name ?? "noDither"; - string ditherType = quantizer.Dither?.DitherType.ToString() ?? string.Empty; + string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "noDither"; + string ditherType = quantizer.Options.Dither?.DitherType.ToString() ?? string.Empty; string testOutputDetails = $"{quantizerName}_{ditherName}_{ditherType}"; provider.RunRectangleConstrainedValidatingProcessorTest( @@ -59,8 +166,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization where TPixel : struct, IPixel { string quantizerName = quantizer.GetType().Name; - string ditherName = quantizer.Dither?.GetType()?.Name ?? "noDither"; - string ditherType = quantizer.Dither?.DitherType.ToString() ?? string.Empty; + string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "noDither"; + string ditherType = quantizer.Options.Dither?.DitherType.ToString() ?? string.Empty; string testOutputDetails = $"{quantizerName}_{ditherName}_{ditherType}"; provider.RunValidatingProcessorTest( @@ -69,5 +176,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization testOutputDetails: testOutputDetails, appendPixelTypeToFileName: false); } + + [Theory] + [WithFile(TestImages.Png.David, nameof(DitherScaleQuantizers), PixelTypes.Rgba32)] + public void ApplyQuantizationWithDitheringScale(TestImageProvider provider, IQuantizer quantizer) + where TPixel : struct, IPixel + { + string quantizerName = quantizer.GetType().Name; + string ditherName = quantizer.Options.Dither.GetType().Name; + string ditherType = quantizer.Options.Dither.DitherType.ToString(); + float ditherScale = quantizer.Options.DitherScale; + string testOutputDetails = $"{quantizerName}_{ditherName}_{ditherType}_{ditherScale}"; + + provider.RunValidatingProcessorTest( + x => x.Quantize(quantizer), + comparer: ValidatorComparer, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: false); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs index e352d51f63..eb9d738e9a 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs @@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization [Fact] public void WuQuantizerConstructor() { - var quantizer = new WuQuantizer(128); - - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDitherings.FloydSteinberg, quantizer.Dither); - - quantizer = new WuQuantizer(false); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Dither); - - quantizer = new WuQuantizer(KnownDitherings.Atkinson); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDitherings.Atkinson, quantizer.Dither); - - quantizer = new WuQuantizer(KnownDitherings.Atkinson, 128); - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDitherings.Atkinson, quantizer.Dither); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new WuQuantizer(expected); + + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = null }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] @@ -38,23 +42,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.FloydSteinberg, frameQuantizer.Dither); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new WuQuantizer(false); + quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.DoDither); - Assert.Null(frameQuantizer.Dither); + Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new WuQuantizer(KnownDitherings.Atkinson); + quantizer = new WuQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.DoDither); - Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Dither); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 0b11395a87..42da64fdbd 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -22,29 +22,29 @@ namespace SixLabors.ImageSharp.Tests var octree = new OctreeQuantizer(); var wu = new WuQuantizer(); - Assert.NotNull(werner.Dither); - Assert.NotNull(webSafe.Dither); - Assert.NotNull(octree.Dither); - Assert.NotNull(wu.Dither); + Assert.NotNull(werner.Options.Dither); + Assert.NotNull(webSafe.Options.Dither); + Assert.NotNull(octree.Options.Dither); + Assert.NotNull(wu.Options.Dither); using (IFrameQuantizer quantizer = werner.CreateFrameQuantizer(this.Configuration)) { - Assert.True(quantizer.DoDither); + Assert.NotNull(quantizer.Options.Dither); } using (IFrameQuantizer quantizer = webSafe.CreateFrameQuantizer(this.Configuration)) { - Assert.True(quantizer.DoDither); + Assert.NotNull(quantizer.Options.Dither); } using (IFrameQuantizer quantizer = octree.CreateFrameQuantizer(this.Configuration)) { - Assert.True(quantizer.DoDither); + Assert.NotNull(quantizer.Options.Dither); } using (IFrameQuantizer quantizer = wu.CreateFrameQuantizer(this.Configuration)) { - Assert.True(quantizer.DoDither); + Assert.NotNull(quantizer.Options.Dither); } } @@ -58,9 +58,15 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage()) { - Assert.True(image[0, 0].Equals(default(TPixel))); + Assert.True(image[0, 0].Equals(default)); - var quantizer = new OctreeQuantizer(dither); + var options = new QuantizerOptions(); + if (!dither) + { + options.Dither = null; + } + + var quantizer = new OctreeQuantizer(options); foreach (ImageFrame frame in image.Frames) { @@ -82,9 +88,15 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage()) { - Assert.True(image[0, 0].Equals(default(TPixel))); + Assert.True(image[0, 0].Equals(default)); + + var options = new QuantizerOptions(); + if (!dither) + { + options.Dither = null; + } - var quantizer = new WuQuantizer(dither); + var quantizer = new WuQuantizer(options); foreach (ImageFrame frame in image.Frames) { diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index f0ee576235..6d48660f62 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization public void SinglePixelOpaque() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); using var image = new Image(config, 1, 1, Color.Black); ImageFrame frame = image.Frames.RootFrame; @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization public void SinglePixelTransparent() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); using var image = new Image(config, 1, 1, default(Rgba32)); ImageFrame frame = image.Frames.RootFrame; @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization } Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; @@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization using (Image image = provider.GetImage()) { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); @@ -148,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization } Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); ImageFrame frame = image.Frames.RootFrame; using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 16c570d63d..fb3e974bb1 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Tests public const string LowColorVariance = "Png/low-variance.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string InvalidTextData = "Png/InvalidTextData.png"; + public const string David = "Png/david.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/david.png b/tests/Images/Input/Png/david.png new file mode 100644 index 0000000000000000000000000000000000000000..6cfa88480fbb63b7b4898509f74e98091280ebc1 GIT binary patch literal 27218 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 Us?KBYMnLlOaP`^K?aUVZA1@#-9smFU literal 0 HcmV?d00001