Browse Source

Add dither scaling and simplify API.

pull/1114/head
James Jackson-South 6 years ago
parent
commit
e535d1d409
  1. 23
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 5
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  3. 18
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 3
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  5. 5
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  6. 4
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  7. 8
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  8. 7
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs
  9. 18
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  10. 74
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs
  11. 9
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  12. 17
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs
  13. 25
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  14. 76
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  15. 11
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  16. 60
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  17. 23
      src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs
  18. 42
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  19. 21
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  20. 23
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs
  21. 26
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  22. 69
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  23. 11
      tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs
  24. 7
      tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs
  25. 10
      tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs
  26. 4
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  27. 6
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  28. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  29. 50
      tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs
  30. 48
      tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs
  31. 149
      tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs
  32. 50
      tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs
  33. 36
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  34. 10
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  35. 1
      tests/ImageSharp.Tests/TestImages.cs
  36. BIN
      tests/Images/Input/Png/david.png

23
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<TPixel>();
AotCompileWuQuantizer<TPixel>();
AotCompilePaletteQuantizer<TPixel>();
AotCompileDithering<TPixel>();
AotCompilePixelOperations<TPixel>();
@ -109,7 +111,7 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileOctreeQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = new OctreeFrameQuantizer<TPixel>(Configuration.Default, new OctreeQuantizer(false)))
using (var test = new OctreeFrameQuantizer<TPixel>(Configuration.Default, new OctreeQuantizer().Options))
{
test.AotGetPalette();
}
@ -122,7 +124,22 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileWuQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = new WuFrameQuantizer<TPixel>(Configuration.Default, new WuQuantizer(false)))
using (var test = new WuFrameQuantizer<TPixel>(Configuration.Default, new WuQuantizer().Options))
{
var frame = new ImageFrame<TPixel>(Configuration.Default, 1, 1);
test.QuantizeFrame(frame, frame.Bounds());
test.AotGetPalette();
}
}
/// <summary>
/// This method pre-seeds the PaletteQuantizer in the AoT compiler for iOS.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private static void AotCompilePaletteQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = (PaletteFrameQuantizer<TPixel>)new PaletteQuantizer(Array.Empty<Color>()).CreateFrameQuantizer<TPixel>(Configuration.Default))
{
var frame = new ImageFrame<TPixel>(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<TPixel>(Configuration.Default, 1, 1))
{
test.Dither(image, default, pixel, pixel, 0, 0, 0);
test.Dither(image, default, pixel, pixel, 0, 0, 0, 0);
}
}

5
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;
}
/// <summary>
@ -335,7 +336,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : struct, IPixel<TPixel>
{
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, 256);
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using IQuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;

18
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -144,13 +144,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (IFrameQuantizer<TPixel> paletteFrameQuantizer =
new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Dither, quantized.Palette))
using (IFrameQuantizer<TPixel> paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (IQuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
using (IQuantizedFrame<TPixel> 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<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, frameMetadata.ColorTableLength))
var options = new QuantizerOptions
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}

3
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.

5
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<TPixel>
{
// 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<float> matrix = this.matrix;

4
src/ImageSharp/Processing/Processors/Dithering/IDither.cs

@ -28,6 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <param name="x">The column index.</param>
/// <param name="y">The row index.</param>
/// <param name="bitDepth">The bit depth of the target palette.</param>
/// <param name="scale">The dithering scale used to adjust the amount of dither. Range 0..1.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The dithered result for the source pixel.</returns>
TPixel Dither<TPixel>(
@ -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<TPixel>;
}
}

8
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<TPixel>
{
// 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);

7
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs

@ -32,10 +32,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
}
/// <summary>
/// Gets the dithering algorithm.
/// Gets the dithering algorithm to apply to the output image.
/// </summary>
public IDither Dither { get; }
/// <summary>
/// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
/// </summary>
public float DitherScale { get; }
/// <summary>
/// Gets the palette to select substitute colors from.
/// </summary>

18
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<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private EuclideanPixelMap<TPixel> 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<TPixel> 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<TPixel> 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;
}

74
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs

@ -17,11 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public abstract class FrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Flag used to indicate whether a single pass or two passes are needed for quantization.
/// </summary>
private readonly bool singlePass;
private EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
@ -29,57 +25,39 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The quantizer.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="singlePass">
/// If true, the quantization process only needs to loop through the source pixels once.
/// If <see langword="true"/>, the quantization process only needs to loop through the source pixels once.
/// </param>
/// <remarks>
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
/// only call the <see cref="SecondPass(ImageFrame{TPixel}, Rectangle, Memory{byte}, ReadOnlyMemory{TPixel})"/> method.
/// If two passes are required, the code will also call <see cref="FirstPass(ImageFrame{TPixel}, Rectangle)"/>.
/// </remarks>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="diffuser">The diffuser</param>
/// <param name="singlePass">
/// If true, the quantization process only needs to loop through the source pixels once
/// </param>
/// <remarks>
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
/// only call the <see cref="SecondPass(ImageFrame{TPixel}, Rectangle, Memory{byte}, ReadOnlyMemory{TPixel})"/> method.
/// If two passes are required, the code will also call <see cref="FirstPass(ImageFrame{TPixel}, Rectangle)"/>.
/// </remarks>
protected FrameQuantizer(Configuration configuration, IDither diffuser, bool singlePass)
{
this.Configuration = configuration;
this.Dither = diffuser;
this.DoDither = this.Dither != null;
this.singlePass = singlePass;
}
/// <inheritdoc />
public IDither Dither { get; }
/// <inheritdoc />
public bool DoDither { get; }
/// <inheritdoc/>
public QuantizerOptions Options { get; }
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.
/// </summary>
protected Configuration Configuration { get; }
/// <summary>
/// Gets a value indicating whether the frame quantizer utilizes a dithering method.
/// </summary>
protected bool IsDitheringQuantizer { get; }
/// <inheritdoc/>
public void Dispose()
{
@ -109,7 +87,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
Memory<byte> 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<TPixel> 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<TPixel> palette)
{
ReadOnlySpan<TPixel> 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<byte> 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 _);
}
}

9
src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs

@ -15,14 +15,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets a value indicating whether to apply dithering to the output image.
/// Gets the quantizer options defining quantization rules.
/// </summary>
bool DoDither { get; }
/// <summary>
/// Gets the algorithm to apply to the output image.
/// </summary>
IDither Dither { get; }
QuantizerOptions Options { get; }
/// <summary>
/// Quantize an image frame and return the resulting output pixels.

17
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
{
/// <summary>
/// Gets the dithering algorithm to apply to the output image.
/// Gets the quantizer options defining quantization rules.
/// </summary>
IDither Dither { get; }
QuantizerOptions Options { get; }
/// <summary>
/// Creates the generic frame quantizer
/// Creates the generic frame quantizer.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/>.</returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// Creates the generic frame quantizer
/// Creates the generic frame quantizer.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <param name="options">The options to create the quantizer with.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/>.</returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>;
}
}

25
src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs

@ -39,30 +39,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The octree quantizer</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <remarks>
/// 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
/// </remarks>
public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer)
: this(configuration, quantizer, quantizer.MaxColors)
public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions options)
: base(configuration, options, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The octree quantizer.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <remarks>
/// 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
/// </remarks>
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];

76
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
{
/// <summary>
/// Allows the quantization of images pixels using Octrees.
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
/// <para>
/// By default the quantizer uses <see cref="KnownDitherings.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// </para>
/// </summary>
public class OctreeQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public OctreeQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image.</param>
public OctreeQuantizer(bool dither)
: this(GetDiffuser(dither), QuantizerConstants.MaxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(bool dither, int maxColors)
: this(GetDiffuser(dither), maxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The dithering algorithm, if any, to apply to the output image.</param>
public OctreeQuantizer(IDither diffuser)
: this(diffuser, QuantizerConstants.MaxColors)
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="dither">The dithering algorithm, if any, to apply to the output image.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(IDither dither, int maxColors)
/// <param name="options">The quantizer options defining quantization rules.</param>
public OctreeQuantizer(QuantizerOptions options)
{
this.Dither = dither;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
Guard.NotNull(options, nameof(options));
this.Options = options;
}
/// <inheritdoc />
public IDither Dither { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
/// </summary>
public int MaxColors { get; }
public QuantizerOptions Options { get; }
/// <param name="configuration"></param>
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
=> new OctreeFrameQuantizer<TPixel>(configuration, this);
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
return new OctreeFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IDither GetDiffuser(bool dither) => dither ? KnownDitherings.FloydSteinberg : null;
=> new OctreeFrameQuantizer<TPixel>(configuration, options);
}
}

11
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 <see cref="PaletteFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="diffuser">The palette quantizer.</param>
/// <param name="colors">An array of all colors in the palette.</param>
public PaletteFrameQuantizer(Configuration configuration, IDither diffuser, ReadOnlyMemory<TPixel> colors)
: base(configuration, diffuser, true) => this.palette = colors;
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> colors)
: base(configuration, options, true) => this.palette = colors;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
protected override ReadOnlyMemory<TPixel> GenerateQuantizedPalette() => this.palette;
internal ReadOnlyMemory<TPixel> AotGetPalette() => this.GenerateQuantizedPalette();
}
}

60
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
{
/// <summary>
/// Allows the quantization of images pixels using color palettes.
/// Override this class to provide your own palette.
/// <para>
/// By default the quantizer uses <see cref="KnownDitherings.FloydSteinberg"/> dithering.
/// </para>
/// </summary>
public class PaletteQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="palette">The color palette.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette)
: this(palette, true)
: this(palette, new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="dither">Whether to apply dithering to the output image</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, bool dither)
: this(palette, GetDiffuser(dither))
/// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options)
{
}
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="dither">The dithering algorithm, if any, to apply to the output image</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, IDither dither)
{
this.Palette = palette;
this.Dither = dither;
this.Options = options;
}
/// <inheritdoc />
public IDither Dither { get; }
/// <summary>
/// Gets the palette.
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public QuantizerOptions Options { get; }
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
{
var palette = new TPixel[this.Palette.Length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, this.Dither, palette);
}
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel>(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<TPixel>(configuration, options, palette);
}
}
}

23
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
{
/// <summary>
/// Contains color quantization specific constants.
/// </summary>
internal static class QuantizerConstants
public static class QuantizerConstants
{
/// <summary>
/// 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.
/// </summary>
public const int MaxColors = 256;
/// <summary>
/// The minumim dithering scale used to adjust the amount of dither.
/// </summary>
public const float MinDitherScale = 0;
/// <summary>
/// The max dithering scale used to adjust the amount of dither.
/// </summary>
public const float MaxDitherScale = 1F;
/// <summary>
/// Gets the default dithering algorithm to use.
/// </summary>
public static IDither DefaultDither { get; } = KnownDitherings.FloydSteinberg;
}
}
}

42
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
{
/// <summary>
/// Defines options for quantization.
/// </summary>
public class QuantizerOptions
{
private float ditherScale = QuantizerConstants.MaxDitherScale;
private int maxColors = QuantizerConstants.MaxColors;
/// <summary>
/// Gets or sets the algorithm to apply to the output image.
/// Defaults to <see cref="QuantizerConstants.DefaultDither"/>; set to <see langword="null"/> for no dithering.
/// </summary>
public IDither Dither { get; set; } = QuantizerConstants.DefaultDither;
/// <summary>
/// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1.
/// Defaults to <see cref="QuantizerConstants.MaxDitherScale"/>.
/// </summary>
public float DitherScale
{
get { return this.ditherScale; }
set { this.ditherScale = value.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); }
}
/// <summary>
/// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256.
/// Defaults to <see cref="QuantizerConstants.MaxColors"/>.
/// </summary>
public int MaxColors
{
get { return this.maxColors; }
set { this.maxColors = value.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); }
}
}
}

21
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 <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
public WebSafePaletteQuantizer()
: this(true)
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WebSafePaletteQuantizer(bool dither)
: base(Color.WebSafePalette, dither)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WebSafePaletteQuantizer(IDither diffuser)
: base(Color.WebSafePalette, diffuser)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WebSafePaletteQuantizer(QuantizerOptions options)
: base(Color.WebSafePalette, options)
{
}
}
}
}

23
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
{
/// <summary>
@ -15,26 +13,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
public WernerPaletteQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WernerPaletteQuantizer(bool dither)
: base(Color.WernerPalette, dither)
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WernerPaletteQuantizer(IDither diffuser)
: base(Color.WernerPalette, diffuser)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WernerPaletteQuantizer(QuantizerOptions options)
: base(Color.WernerPalette, options)
{
}
}
}
}

26
src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs

@ -96,33 +96,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WuFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The Wu quantizer</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <remarks>
/// 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.
/// </remarks>
public WuFrameQuantizer(Configuration configuration, WuQuantizer quantizer)
: this(configuration, quantizer, quantizer.MaxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The Wu quantizer.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <remarks>
/// 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.
/// </remarks>
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<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = maxColors;
this.colors = this.Options.MaxColors;
}
/// <inheritdoc/>
@ -185,9 +170,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
protected override byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
{
if (!this.DoDither)
if (!this.IsDitheringQuantizer)
{
// Expected order r->g->b->a
Rgba32 rgba = default;
color.ToRgba32(ref rgba);

69
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
{
/// <summary>
/// Allows the quantization of images pixels using Xiaolin Wu's Color Quantizer <see href="http://www.ece.mcmaster.ca/~xwu/cq.c"/>
/// <para>
/// By default the quantizer uses <see cref="KnownDitherings.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// </para>
/// </summary>
public class WuQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public WuQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WuQuantizer(bool dither)
: this(GetDiffuser(dither), QuantizerConstants.MaxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The dithering algorithm, if any, to apply to the output image</param>
public WuQuantizer(IDither diffuser)
: this(diffuser, QuantizerConstants.MaxColors)
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="dither">The dithering algorithm, if any, to apply to the output image</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(IDither dither, int maxColors)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WuQuantizer(QuantizerOptions options)
{
this.Dither = dither;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
Guard.NotNull(options, nameof(options));
this.Options = options;
}
/// <inheritdoc />
public IDither Dither { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
/// </summary>
public int MaxColors { get; }
public QuantizerOptions Options { get; }
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
return new WuFrameQuantizer<TPixel>(configuration, this);
}
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
return new WuFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IDither GetDiffuser(bool dither) => dither ? KnownDitherings.FloydSteinberg : null;
=> new WuFrameQuantizer<TPixel>(configuration, options);
}
}

11
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);
}
}
}
}
}

7
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;
});

10
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);
}
}
}
}
}

4
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);

6
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);

2
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
};

50
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<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(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<Rgba32>(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<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.DoDither);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Dither);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
}
}

48
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<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(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<Rgba32>(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<Rgba32>(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);
}
}
}

149
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<IQuantizer> Quantizers
= new TheoryData<IQuantizer>
{
// 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<IQuantizer> DitherScaleQuantizers
= new TheoryData<IQuantizer>
{
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<TPixel>
{
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<TPixel>
{
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<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer)
where TPixel : struct, IPixel<TPixel>
{
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);
}
}
}

50
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<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(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<Rgba32>(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<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.DoDither);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Dither);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
}
}

36
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<Rgba32> quantizer = werner.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.True(quantizer.DoDither);
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = webSafe.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.True(quantizer.DoDither);
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = octree.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.True(quantizer.DoDither);
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = wu.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.True(quantizer.DoDither);
Assert.NotNull(quantizer.Options.Dither);
}
}
@ -58,9 +58,15 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<TPixel> 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<TPixel> frame in image.Frames)
{
@ -82,9 +88,15 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<TPixel> 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<TPixel> frame in image.Frames)
{

10
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<Rgba32>(config, 1, 1, Color.Black);
ImageFrame<Rgba32> 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<Rgba32>(config, 1, 1, default(Rgba32));
ImageFrame<Rgba32> 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<Rgba32> frame = image.Frames.RootFrame;
@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
using (Image<TPixel> image = provider.GetImage())
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
ImageFrame<TPixel> frame = image.Frames.RootFrame;
using IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(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<Rgba32> frame = image.Frames.RootFrame;
using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config))

1
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";

BIN
tests/Images/Input/Png/david.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Loading…
Cancel
Save