Browse Source

Merge branch 'main' into dependabot/github_actions/codecov/codecov-action-6

pull/3097/head
James Jackson-South 1 month ago
committed by GitHub
parent
commit
b7f3bbb9da
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 4
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  3. 8
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  4. 14
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  5. 2
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  6. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  7. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  8. 6
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  9. 6
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
  10. 42
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  11. 2
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  12. 2
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  13. 13
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  14. 32
      src/ImageSharp/ImageInfo.cs
  15. 198
      src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs
  16. 8
      src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs
  17. 8
      src/ImageSharp/Processing/KnownQuantizers.cs
  18. 16
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
  19. 10
      src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs
  20. 116
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs
  21. 22
      src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs
  22. 354
      src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs
  23. 387
      src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs
  24. 12
      tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs
  25. 302
      tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs
  26. 8
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  27. 2
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  28. 6
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  29. 85
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  30. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  31. 10
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  32. 30
      tests/ImageSharp.Tests/ImageInfoTests.cs
  33. 20
      tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs
  34. 158
      tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs
  35. 26
      tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs
  36. 14
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  37. 1
      tests/ImageSharp.Tests/TestImages.cs
  38. 3
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp
  39. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png
  40. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png
  41. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png
  42. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png
  43. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png
  44. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png
  45. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png
  46. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png
  47. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png
  48. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png
  49. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png
  50. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png
  51. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png
  52. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png
  53. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png
  54. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png
  55. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png
  56. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png
  57. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png
  58. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png
  59. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png
  60. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png
  61. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png
  62. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png
  63. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png
  64. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png
  65. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png
  66. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png
  67. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png
  68. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png
  69. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png
  70. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png
  71. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png
  72. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png
  73. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png
  74. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png
  75. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png
  76. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png
  77. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png
  78. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png
  79. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png
  80. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png
  81. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png
  82. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png
  83. 3
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png
  84. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png
  85. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png
  86. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png
  87. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png
  88. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png
  89. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png
  90. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png
  91. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png
  92. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png
  93. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png
  94. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png
  95. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png
  96. 0
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png
  97. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png
  98. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png
  99. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png
  100. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png

10
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -54,7 +54,7 @@ internal static class AotCompilerTools
/// <remarks>
/// This method doesn't actually do anything but serves an important purpose...
/// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception:
/// "Attempting to JIT compile method... OctreeFrameQuantizer.ConstructPalette... while running in aot-only mode."
/// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode."
/// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT
/// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on
/// iOS so it bombs out.
@ -479,7 +479,7 @@ internal static class AotCompilerTools
private static void AotCompileQuantizers<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
AotCompileQuantizer<TPixel, OctreeQuantizer>();
AotCompileQuantizer<TPixel, HexadecatreeQuantizer>();
AotCompileQuantizer<TPixel, PaletteQuantizer>();
AotCompileQuantizer<TPixel, WebSafePaletteQuantizer>();
AotCompileQuantizer<TPixel, WernerPaletteQuantizer>();
@ -523,10 +523,8 @@ internal static class AotCompilerTools
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}
/// <summary>
@ -551,8 +549,8 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
where TDither : struct, IDither
{
OctreeQuantizer<TPixel> octree = default;
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, default);
HexadecatreeQuantizer<TPixel> hexadecatree = default;
default(TDither).ApplyQuantizationDither<HexadecatreeQuantizer<TPixel>, TPixel>(ref hexadecatree, default, default, default);
PaletteQuantizer<TPixel> palette = default;
default(TDither).ApplyQuantizationDither<PaletteQuantizer<TPixel>, TPixel>(ref palette, default, default, default);

4
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -15,12 +15,12 @@ public interface IRowOperation<TBuffer>
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
public int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
public void Invoke(int y, Span<TBuffer> span);
}

8
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -68,7 +68,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -138,7 +138,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -195,7 +195,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -262,7 +262,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,

14
src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs

@ -752,7 +752,7 @@ internal static partial class SimdUtils
/// Implementation is based on MagicScaler code:
/// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L80-L182
/// </remarks>
internal static unsafe void ByteToNormalizedFloat(
internal static void ByteToNormalizedFloat(
ReadOnlySpan<byte> source,
Span<float> destination)
{
@ -1172,8 +1172,10 @@ internal static partial class SimdUtils
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
// Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load,
// so we need 3 extra pixels of addressable slack beyond the vectorized chunk.
const int bytesPerRgbStride = 24;
nuint count = (uint)source.Length / 8;
nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
@ -1193,10 +1195,10 @@ internal static partial class SimdUtils
}
int sliceCount = (int)(count * 8);
redChannel = redChannel.Slice(sliceCount);
greenChannel = greenChannel.Slice(sliceCount);
blueChannel = blueChannel.Slice(sliceCount);
source = source.Slice(sliceCount);
redChannel = redChannel[sliceCount..];
greenChannel = greenChannel[sliceCount..];
blueChannel = blueChannel[sliceCount..];
source = source[sliceCount..];
}
}
}

2
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
/// </summary>
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
/// <summary>
/// Gets the number of bits per pixel.

2
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -116,7 +116,7 @@ internal sealed class BmpEncoderCore
this.bitsPerPixel = encoder.BitsPerPixel;
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;

2
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -468,7 +468,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
rawColorTable = this.currentLocalColorTable!.GetSpan()[..length];
rawColorTable = this.currentLocalColorTable.GetSpan()[..length];
}
else if (this.globalColorTable != null)
{

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

@ -117,7 +117,7 @@ internal sealed class GifEncoderCore
if (globalQuantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
// Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
int ti = GetTransparentIndex(quantized, frameMetadata);
@ -132,12 +132,12 @@ internal sealed class GifEncoderCore
}
else
{
globalQuantizer = new OctreeQuantizer(options);
globalQuantizer = new HexadecatreeQuantizer(options);
}
}
else
{
globalQuantizer = new OctreeQuantizer(options);
globalQuantizer = new HexadecatreeQuantizer(options);
}
}

6
src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs

@ -114,9 +114,9 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
Span<TPixel> sourceRow = this.pixelBuffer.DangerousGetRowSpan(srcIndex);
PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(rLane, gLane, bLane, sourceRow);
rLane.Slice(paddingStartIndex).Fill(rLane[paddingStartIndex - 1]);
gLane.Slice(paddingStartIndex).Fill(gLane[paddingStartIndex - 1]);
bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]);
rLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(rLane[paddingStartIndex - 1]);
gLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(gLane[paddingStartIndex - 1]);
bLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(bLane[paddingStartIndex - 1]);
// Convert from rgb24 to target pixel type
JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y);

42
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -214,7 +214,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
case PngChunkType.FrameData:
{
if (frameCount >= this.maxFrames)
if (frameCount > this.maxFrames)
{
goto EOF;
}
@ -275,7 +275,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
previousFrameControl = currentFrameControl;
}
if (frameCount >= this.maxFrames)
if (frameCount > this.maxFrames)
{
goto EOF;
}
@ -402,7 +402,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
case PngChunkType.FrameControl:
++frameCount;
if (frameCount >= this.maxFrames)
if (frameCount > this.maxFrames)
{
break;
}
@ -411,8 +411,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
case PngChunkType.FrameData:
if (frameCount >= this.maxFrames)
if (frameCount > this.maxFrames)
{
// Must skip the chunk data even when we've hit maxFrames, because TryReadChunk
// restores the stream position to the start of the fdAT data after CRC validation.
this.SkipChunkDataAndCrc(chunk);
this.SkipRemainingFrameDataChunks(buffer);
break;
}
@ -428,9 +432,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore
InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
// Skip sequence number
this.currentStream.Skip(4);
// Skip data for this and all remaining FrameData chunks belonging to the same frame
// (comparable to how Decode consumes them via ReadScanlines + ReadNextFrameDataChunk).
this.SkipChunkDataAndCrc(chunk);
this.SkipRemainingFrameDataChunks(buffer);
break;
case PngChunkType.Data:
@ -2093,6 +2098,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return 0;
}
/// <summary>
/// Skips any remaining <see cref="PngChunkType.FrameData"/> chunks belonging to the current frame.
/// This mirrors how <see cref="ReadNextFrameDataChunk"/> is used during decoding:
/// consecutive fdAT chunks are consumed until a non-fdAT chunk is encountered,
/// which is stored in <see cref="nextChunk"/> for the next iteration.
/// </summary>
/// <param name="buffer">Temporary buffer.</param>
private void SkipRemainingFrameDataChunks(Span<byte> buffer)
{
while (this.TryReadChunk(buffer, out PngChunk chunk))
{
if (chunk.Type is PngChunkType.FrameData)
{
chunk.Data?.Dispose();
this.SkipChunkDataAndCrc(chunk);
}
else
{
// Not a FrameData chunk; store it so the next TryReadChunk call returns it.
this.nextChunk = chunk;
return;
}
}
}
/// <summary>
/// Reads a chunk from the stream.
/// </summary>

2
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -15,7 +15,7 @@ public class TiffEncoder : QuantizingImageEncoder
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoder"/> class.
/// </summary>
public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree;
public TiffEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
/// <summary>
/// Gets the number of bits per pixel.

2
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -71,7 +71,7 @@ internal sealed class TiffEncoderCore
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.PhotometricInterpretation = encoder.PhotometricInterpretation;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.BitsPerPixel = encoder.BitsPerPixel;
this.HorizontalPredictor = encoder.HorizontalPredictor;

13
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
@ -456,15 +457,21 @@ internal class WebpAnimationDecoder : IDisposable
// The destination frame has already been prepopulated with the pixel data from the previous frame
// so blending will leave the desired result which takes into consideration restoration to the
// background color within the restore area.
PixelBlender<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(
PixelColorBlendingMode.Normal,
PixelAlphaCompositionMode.SrcOver);
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
using IMemoryOwner<Vector4> workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate<Vector4>(restoreArea.Width * 3);
Span<Vector4> workingBuffer = workingBufferOwner.GetSpan();
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer);
}
return;

32
src/ImageSharp/ImageInfo.cs

@ -63,8 +63,12 @@ public class ImageInfo
public int Height => this.Size.Height;
/// <summary>
/// Gets the number of frames in the image.
/// Gets the number of frame metadata entries available for the image.
/// </summary>
/// <remarks>
/// This value is the same as <see cref="FrameMetadataCollection"/> count and may be <c>0</c> when frame
/// metadata was not populated by the decoder.
/// </remarks>
public int FrameCount => this.FrameMetadataCollection.Count;
/// <summary>
@ -73,8 +77,12 @@ public class ImageInfo
public ImageMetadata Metadata { get; }
/// <summary>
/// Gets the collection of metadata associated with individual image frames.
/// Gets the metadata associated with the decoded image frames, if available.
/// </summary>
/// <remarks>
/// For multi-frame formats, decoders populate one entry per decoded frame. For single-frame formats, this
/// collection is typically empty.
/// </remarks>
public IReadOnlyList<ImageFrameMetadata> FrameMetadataCollection { get; }
/// <summary>
@ -86,4 +94,24 @@ public class ImageInfo
/// Gets the bounds of the image.
/// </summary>
public Rectangle Bounds => new(Point.Empty, this.Size);
/// <summary>
/// Gets the total number of bytes required to store the image pixels in memory.
/// </summary>
/// <remarks>
/// This reports the in-memory size of the pixel data represented by this <see cref="ImageInfo"/>, not the
/// encoded size of the image file. The value is computed from the image dimensions and
/// <see cref="PixelType"/>. When <see cref="FrameMetadataCollection"/> contains decoded frame metadata, the
/// per-frame size is multiplied by that count. Otherwise, the value is the in-memory size of the single
/// image frame represented by this <see cref="ImageInfo"/>.
/// </remarks>
/// <returns>The total number of bytes required to store the image pixels in memory.</returns>
public long GetPixelMemorySize()
{
int count = this.FrameMetadataCollection.Count > 0
? this.FrameMetadataCollection.Count
: 1;
return (long)this.Size.Width * this.Size.Height * (this.PixelType.BitsPerPixel / 8) * count;
}
}

198
src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.PixelFormats;
@ -52,16 +51,53 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
float amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -87,14 +123,48 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
float amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -116,6 +186,27 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<float> amount)
=> this.Blend<TPixel>(configuration, destination, background, source, amount);
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixel> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
=> this.Blend<TPixel>(configuration, destination, background, source, amount, workingBuffer);
/// <summary>
/// Blends 2 rows together
/// </summary>
@ -142,20 +233,89 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends a row against a constant source color.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends a row against a constant source color.
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
@ -165,26 +325,28 @@ public abstract class PixelBlender<TPixel>
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>

8
src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs

@ -12,12 +12,12 @@ namespace SixLabors.ImageSharp.Processing;
public static class QuantizeExtensions
{
/// <summary>
/// Applies quantization to the image using the <see cref="OctreeQuantizer"/>.
/// Applies quantization to the image using the <see cref="HexadecatreeQuantizer"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Quantize(this IImageProcessingContext source) =>
Quantize(source, KnownQuantizers.Octree);
Quantize(source, KnownQuantizers.Hexadecatree);
/// <summary>
/// Applies quantization to the image.
@ -29,7 +29,7 @@ public static class QuantizeExtensions
source.ApplyProcessor(new QuantizeProcessor(quantizer));
/// <summary>
/// Applies quantization to the image using the <see cref="OctreeQuantizer"/>.
/// Applies quantization to the image using the <see cref="HexadecatreeQuantizer"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">
@ -37,7 +37,7 @@ public static class QuantizeExtensions
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) =>
Quantize(source, KnownQuantizers.Octree, rectangle);
Quantize(source, KnownQuantizers.Hexadecatree, rectangle);
/// <summary>
/// Applies quantization to the image.

8
src/ImageSharp/Processing/KnownQuantizers.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -6,14 +6,14 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing;
/// <summary>
/// Contains reusable static instances of known quantizing algorithms
/// Contains reusable static instances of known quantizing algorithms.
/// </summary>
public static class KnownQuantizers
{
/// <summary>
/// Gets the adaptive Octree quantizer. Fast with good quality.
/// Gets the adaptive hexadecatree quantizer. Fast with good quality.
/// </summary>
public static IQuantizer Octree { get; } = new OctreeQuantizer();
public static IQuantizer Hexadecatree { get; } = new HexadecatreeQuantizer();
/// <summary>
/// Gets the Xiaolin Wu's Color Quantizer which generates high quality output.

16
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -145,7 +146,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.Blender,
this.Opacity);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
configuration,
new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
in operation);
@ -161,7 +162,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary>
private readonly struct RowOperation : IRowOperation
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Buffer2D<TPixelBg> background;
private readonly Buffer2D<TPixelFg> foreground;
@ -190,13 +191,20 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.opacity = opacity;
}
/// <inheritdoc/>
public int GetRequiredBufferLength(Rectangle bounds)
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
=> 3 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
public void Invoke(int y, Span<Vector4> span)
{
Span<TPixelBg> background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width);
Span<TPixelFg> foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity, span);
}
}
}

10
src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs

@ -15,14 +15,8 @@ public enum ColorMatchingMode
Coarse,
/// <summary>
/// Enables an exact color match cache for the first 512 unique colors encountered,
/// falling back to coarse matching thereafter.
/// </summary>
Hybrid,
/// <summary>
/// Performs exact color matching without any caching optimizations.
/// This is the slowest but most accurate matching strategy.
/// Performs exact color matching using a bounded exact-match cache with eviction.
/// This preserves exact color matching while accelerating repeated colors.
/// </summary>
Exact
}

116
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs

@ -3,6 +3,8 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -71,32 +73,107 @@ internal sealed class EuclideanPixelMap<TPixel, TCache> : PixelMap<TPixel>
[MethodImpl(InliningOptions.ColdPath)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
ReadOnlySpan<Rgba32> rgbaPalette = this.rgbaPalette;
ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPalette);
int index = 0;
float leastDistance = float.MaxValue;
for (int i = 0; i < this.rgbaPalette.Length; i++)
int leastDistance = int.MaxValue;
int i = 0;
if (Vector128.IsHardwareAccelerated && rgbaPalette.Length >= 4)
{
Rgba32 candidate = this.rgbaPalette[i];
if (candidate.PackedValue == rgba.PackedValue)
{
index = i;
break;
}
// Duplicate the query color so one 128-bit register can be subtracted from
// two packed RGBA candidates at a time after widening.
Vector128<short> pixel = Vector128.Create(
rgba.R,
rgba.G,
rgba.B,
rgba.A,
rgba.R,
rgba.G,
rgba.B,
rgba.A);
float distance = DistanceSquared(rgba, candidate);
if (distance == 0)
int vectorizedLength = rgbaPalette.Length & ~0x03;
for (; i < vectorizedLength; i += 4)
{
index = i;
break;
// Load four packed Rgba32 values (16 bytes) and widen them into two vectors:
// [c0.r, c0.g, c0.b, c0.a, c1.r, ...] and [c2.r, c2.g, c2.b, c2.a, c3.r, ...].
Vector128<byte> packed = Vector128.LoadUnsafe(ref Unsafe.As<Rgba32, byte>(ref Unsafe.Add(ref rgbaPaletteRef, i)));
Vector128<short> lowerDiff = Vector128.WidenLower(packed).AsInt16() - pixel;
Vector128<short> upperDiff = Vector128.WidenUpper(packed).AsInt16() - pixel;
// MultiplyAddAdjacent collapses channel squares into RG + BA partial sums,
// so each pair of int lanes still corresponds to one candidate color.
Vector128<int> lowerPairs = Vector128_.MultiplyAddAdjacent(lowerDiff, lowerDiff);
Vector128<int> upperPairs = Vector128_.MultiplyAddAdjacent(upperDiff, upperDiff);
// Sum the two partials for candidates i and i + 1.
ref int lowerRef = ref Unsafe.As<Vector128<int>, int>(ref lowerPairs);
int distance = lowerRef + Unsafe.Add(ref lowerRef, 1);
if (distance < leastDistance)
{
index = i;
leastDistance = distance;
if (distance == 0)
{
goto Found;
}
}
distance = Unsafe.Add(ref lowerRef, 2) + Unsafe.Add(ref lowerRef, 3);
if (distance < leastDistance)
{
index = i + 1;
leastDistance = distance;
if (distance == 0)
{
goto Found;
}
}
// Sum the two partials for candidates i + 2 and i + 3.
ref int upperRef = ref Unsafe.As<Vector128<int>, int>(ref upperPairs);
distance = upperRef + Unsafe.Add(ref upperRef, 1);
if (distance < leastDistance)
{
index = i + 2;
leastDistance = distance;
if (distance == 0)
{
goto Found;
}
}
distance = Unsafe.Add(ref upperRef, 2) + Unsafe.Add(ref upperRef, 3);
if (distance < leastDistance)
{
index = i + 3;
leastDistance = distance;
if (distance == 0)
{
goto Found;
}
}
}
}
for (; i < rgbaPalette.Length; i++)
{
int distance = DistanceSquared(rgba, Unsafe.Add(ref rgbaPaletteRef, i));
if (distance < leastDistance)
{
index = i;
leastDistance = distance;
if (distance == 0)
{
goto Found;
}
}
}
Found:
// Now I have the index, pop it into the cache for next time
_ = this.cache.TryAdd(rgba, (short)index);
match = Unsafe.Add(ref paletteRef, (uint)index);
@ -111,12 +188,12 @@ internal sealed class EuclideanPixelMap<TPixel, TCache> : PixelMap<TPixel>
/// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static float DistanceSquared(Rgba32 a, Rgba32 b)
private static int DistanceSquared(Rgba32 a, Rgba32 b)
{
float deltaR = a.R - b.R;
float deltaG = a.G - b.G;
float deltaB = a.B - b.B;
float deltaA = a.A - b.A;
int deltaR = a.R - b.R;
int deltaG = a.G - b.G;
int deltaB = a.B - b.B;
int deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}
@ -177,8 +254,7 @@ internal static class PixelMapFactory
ColorMatchingMode colorMatchingMode)
where TPixel : unmanaged, IPixel<TPixel> => colorMatchingMode switch
{
ColorMatchingMode.Hybrid => new EuclideanPixelMap<TPixel, HybridCache>(configuration, palette),
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, NullCache>(configuration, palette),
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, AccurateCache>(configuration, palette),
_ => new EuclideanPixelMap<TPixel, CoarseCache>(configuration, palette),
};
}

22
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs → src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs

@ -6,25 +6,29 @@ using SixLabors.ImageSharp.PixelFormats;
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"/>
/// Quantizes images by grouping colors in an adaptive 16-way tree and reducing those groups into a palette.
/// </summary>
public class OctreeQuantizer : IQuantizer
/// <remarks>
/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha. Fully opaque mid-tone colors
/// use RGB-only routing so more branch resolution is spent on visible color detail, while transparent, dark, and
/// light colors use alpha-aware routing so opacity changes can form their own palette buckets.
/// </remarks>
public class HexadecatreeQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// Initializes a new instance of the <see cref="HexadecatreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public OctreeQuantizer()
public HexadecatreeQuantizer()
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// Initializes a new instance of the <see cref="HexadecatreeQuantizer"/> class.
/// </summary>
/// <param name="options">The quantizer options defining quantization rules.</param>
public OctreeQuantizer(QuantizerOptions options)
/// <param name="options">The quantizer options that control palette size, dithering, and transparency behavior.</param>
public HexadecatreeQuantizer(QuantizerOptions options)
{
Guard.NotNull(options, nameof(options));
this.Options = options;
@ -41,5 +45,5 @@ public class OctreeQuantizer : IQuantizer
/// <inheritdoc />
public IQuantizer<TPixel> CreatePixelSpecificQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : unmanaged, IPixel<TPixel>
=> new OctreeQuantizer<TPixel>(configuration, options);
=> new HexadecatreeQuantizer<TPixel>(configuration, options);
}

354
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs → src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs

@ -12,19 +12,28 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// <summary>
/// Encapsulates methods to calculate the color palette if an image using an Octree pattern.
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
/// Quantizes an image by building an adaptive 16-way color tree and reducing it to the requested palette size.
/// </summary>
/// <remarks>
/// <para>
/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha, giving the tree up to 16 children
/// per node and letting transparency participate directly in palette construction.
/// </para>
/// <para>
/// Fully opaque mid-tone colors use RGB-only routing so more branch resolution is spent on visible color detail.
/// Transparent, dark, and light colors use alpha-aware routing so opacity changes can form distinct palette buckets.
/// </para>
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
// See https://github.com/dotnet/roslyn-analyzers/issues/6151
public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
public struct HexadecatreeQuantizer<TPixel> : IQuantizer<TPixel>
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int maxColors;
private readonly int bitDepth;
private readonly Octree octree;
private readonly Hexadecatree tree;
private readonly IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private PixelMap<TPixel>? pixelMap;
@ -32,19 +41,19 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer{TPixel}"/> struct.
/// Initializes a new instance of the <see cref="HexadecatreeQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="configuration">The configuration that provides memory allocation and pixel conversion services.</param>
/// <param name="options">The quantizer options that control palette size, dithering, and transparency behavior.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public OctreeQuantizer(Configuration configuration, QuantizerOptions options)
public HexadecatreeQuantizer(Configuration configuration, QuantizerOptions options)
{
this.Configuration = configuration;
this.Options = options;
this.maxColors = this.Options.MaxColors;
this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8);
this.octree = new Octree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold);
this.tree = new Hexadecatree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.pixelMap = default;
this.palette = default;
@ -76,23 +85,28 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
/// <inheritdoc/>
public readonly void AddPaletteColors(in Buffer2DRegion<TPixel> pixelRegion)
{
PixelRowDelegate pixelRowDelegate = new(this.octree);
QuantizerUtilities.AddPaletteColors<OctreeQuantizer<TPixel>, TPixel, Rgba32, PixelRowDelegate>(
PixelRowDelegate pixelRowDelegate = new(this.tree);
QuantizerUtilities.AddPaletteColors<HexadecatreeQuantizer<TPixel>, TPixel, Rgba32, PixelRowDelegate>(
ref Unsafe.AsRef(in this),
in pixelRegion,
in pixelRowDelegate);
}
/// <summary>
/// Materializes the final palette from the accumulated tree and prepares the dither lookup map when needed.
/// </summary>
private void ResolvePalette()
{
short paletteIndex = 0;
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
this.octree.Palettize(paletteSpan, ref paletteIndex);
this.tree.Palettize(paletteSpan, ref paletteIndex);
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory[..paletteSpan.Length];
if (this.isDithering)
{
// Dithered colors often no longer land on a color that was seen during palette construction,
// so the quantization pass switches to nearest-palette matching once the palette is finalized.
this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode);
}
@ -108,17 +122,15 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{
// Due to the addition of new colors by dithering that are not part of the original histogram,
// the octree nodes might not match the correct color.
// In this case, we must use the pixel map to get the closest color.
if (this.isDithering)
{
// Dithering introduces adjusted colors that were never inserted into the tree, so tree lookup
// is only reliable for the non-dithered path.
return (byte)this.pixelMap!.GetClosestColor(color, out match);
}
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span);
int index = this.octree.GetPaletteIndex(color);
int index = this.tree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, (nuint)index);
return (byte)index;
}
@ -132,34 +144,43 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
this.paletteOwner.Dispose();
this.pixelMap?.Dispose();
this.pixelMap = null;
this.octree.Dispose();
this.tree.Dispose();
}
}
/// <summary>
/// Forwards source rows into the tree without creating an intermediate buffer.
/// </summary>
private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate<Rgba32>
{
private readonly Octree octree;
private readonly Hexadecatree tree;
public PixelRowDelegate(Octree octree) => this.octree = octree;
/// <summary>
/// Initializes a new instance of the <see cref="PixelRowDelegate"/> struct.
/// </summary>
/// <param name="tree">The destination tree that should accumulate each visited row.</param>
public PixelRowDelegate(Hexadecatree tree) => this.tree = tree;
public void Invoke(ReadOnlySpan<Rgba32> row, int rowIndex) => this.octree.AddColors(row);
/// <inheritdoc/>
public void Invoke(ReadOnlySpan<Rgba32> row, int rowIndex) => this.tree.AddColors(row);
}
/// <summary>
/// A hexadecatree-based color quantization structure used for fast color distance lookups and palette generation.
/// This tree maintains a fixed pool of nodes (capacity 4096) where each node can have up to 16 children, stores
/// color accumulation data, and supports dynamic node allocation and reduction. It offers near-constant-time insertions
/// and lookups while consuming roughly 240 KB for the node pool.
/// Stores the adaptive 16-way partition tree used to accumulate colors and emit palette entries.
/// </summary>
internal sealed class Octree : IDisposable
/// <remarks>
/// The tree uses a fixed node arena for predictable allocation behavior, keeps per-level reducible node lists so
/// deeper buckets can be merged until the palette fits, and caches the previously inserted leaf so repeated colors
/// can be accumulated cheaply.
/// </remarks>
internal sealed class Hexadecatree : IDisposable
{
// The memory allocator.
private readonly MemoryAllocator allocator;
// Pooled buffer for OctreeNodes.
private readonly IMemoryOwner<OctreeNode> nodesOwner;
private readonly IMemoryOwner<Node> nodesOwner;
// Reducible nodes: one per level; we use an integer index; -1 means “no node.”
// One reducible-node head per level.
// Each entry stores a node index, or -1 when that level currently
// has no reducible nodes.
private readonly short[] reducibleNodes;
// Maximum number of allowable colors.
@ -186,13 +207,13 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
private readonly Stack<short> freeIndices = new();
/// <summary>
/// Initializes a new instance of the <see cref="Octree"/> class.
/// Initializes a new instance of the <see cref="Hexadecatree"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="maxColorBits">The maximum number of significant bits in the image.</param>
/// <param name="maxColors">The maximum number of colors to allow in the palette.</param>
/// <param name="transparencyThreshold">The threshold for transparent colors.</param>
public Octree(
/// <param name="configuration">The configuration that provides the backing memory allocator.</param>
/// <param name="maxColorBits">The number of levels to descend before forcing leaves.</param>
/// <param name="maxColors">The maximum number of palette entries the reduced tree may retain.</param>
/// <param name="transparencyThreshold">The alpha threshold below which generated palette entries become fully transparent.</param>
public Hexadecatree(
Configuration configuration,
int maxColorBits,
int maxColors,
@ -207,8 +228,7 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
// Allocate a conservative buffer for nodes.
const int capacity = 4096;
this.allocator = configuration.MemoryAllocator;
this.nodesOwner = this.allocator.Allocate<OctreeNode>(capacity, AllocationOptions.Clean);
this.nodesOwner = configuration.MemoryAllocator.Allocate<Node>(capacity, AllocationOptions.Clean);
// Create the reducible nodes array (one per level 0 .. maxColorBits-1).
this.reducibleNodes = new short[this.maxColorBits];
@ -216,24 +236,24 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
// Reserve index 0 for the root.
this.rootIndex = 0;
ref OctreeNode root = ref this.Nodes[this.rootIndex];
ref Node root = ref this.Nodes[this.rootIndex];
root.Initialize(0, this.maxColorBits, this, this.rootIndex);
}
/// <summary>
/// Gets or sets the number of leaves in the tree.
/// Gets or sets the number of leaf nodes currently representing palette buckets.
/// </summary>
public int Leaves { get; set; }
/// <summary>
/// Gets the full collection of nodes as a span.
/// Gets the underlying node arena.
/// </summary>
internal Span<OctreeNode> Nodes => this.nodesOwner.Memory.Span;
internal Span<Node> Nodes => this.nodesOwner.Memory.Span;
/// <summary>
/// Adds a span of colors to the octree.
/// Adds a row of colors to the tree.
/// </summary>
/// <param name="row">A span of color values to be added.</param>
/// <param name="row">The colors to accumulate.</param>
public void AddColors(ReadOnlySpan<Rgba32> row)
{
for (int x = 0; x < row.Length; x++)
@ -243,12 +263,13 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
/// <summary>
/// Add a color to the Octree.
/// Adds a single color sample to the tree.
/// </summary>
/// <param name="color">The color to add.</param>
/// <param name="color">The color to accumulate.</param>
private void AddColor(Rgba32 color)
{
// Ensure that the tree is not already full.
// Once the node arena is full and there are no recycled slots available, keep collapsing
// reducible leaves until the tree is small enough to make forward progress again.
if (this.nextNode >= this.Nodes.Length && this.freeIndices.Count == 0)
{
while (this.Leaves > this.maxColors)
@ -257,32 +278,32 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
}
// If the color is the same as the previous color, increment the node.
// Otherwise, add a new node.
// Scanlines often contain long runs of the same color. Caching the previous leaf lets those
// repeats skip the tree walk and just bump the accumulated sums in place.
if (this.previousColor.Equals(color))
{
if (this.previousNode == -1)
{
this.previousColor = color;
OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this);
Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this);
}
else
{
OctreeNode.Increment(this.previousNode, color, this);
Node.Increment(this.previousNode, color, this);
}
}
else
{
this.previousColor = color;
OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this);
Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this);
}
}
/// <summary>
/// Construct the palette from the octree.
/// Reduces the tree to the requested palette size and emits the final palette entries.
/// </summary>
/// <param name="palette">The palette to construct.</param>
/// <param name="paletteIndex">The current palette index.</param>
/// <param name="palette">The destination palette span.</param>
/// <param name="paletteIndex">The running palette index.</param>
public void Palettize(Span<TPixel> palette, ref short paletteIndex)
{
while (this.Leaves > this.maxColors)
@ -294,48 +315,45 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
/// <summary>
/// Get the palette index for the passed color.
/// Gets the palette index selected by the tree for the supplied color.
/// </summary>
/// <param name="color">The color to get the palette index for.</param>
/// <returns>The <see cref="int"/>.</returns>
/// <param name="color">The color to resolve.</param>
/// <returns>The palette index represented by the best matching leaf in the reduced tree.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPaletteIndex(TPixel color)
=> this.Nodes[this.rootIndex].GetPaletteIndex(color.ToRgba32(), 0, this);
/// <summary>
/// Track the previous node and color.
/// Records the most recently touched leaf so repeated colors can bypass another descent.
/// </summary>
/// <param name="nodeIndex">The node index.</param>
/// <param name="nodeIndex">The leaf node index.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void TrackPrevious(int nodeIndex)
=> this.previousNode = nodeIndex;
/// <summary>
/// Reduce the depth of the tree.
/// Collapses the deepest currently reducible node into a single leaf.
/// </summary>
private void Reduce()
{
// Find the deepest level containing at least one reducible node
int index = this.maxColorBits - 1;
while ((index > 0) && (this.reducibleNodes[index] == -1))
{
index--;
}
// Reduce the node most recently added to the list at level 'index'
ref OctreeNode node = ref this.Nodes[this.reducibleNodes[index]];
ref Node node = ref this.Nodes[this.reducibleNodes[index]];
this.reducibleNodes[index] = node.NextReducibleIndex;
// Decrement the leaf count after reducing the node
node.Reduce(this);
// And just in case I've reduced the last color to be added, and the next color to
// be added is the same, invalidate the previousNode...
// If the last inserted leaf was merged away, the next repeated color must walk the tree again.
this.previousNode = -1;
}
// Allocate a new OctreeNode from the pooled buffer.
// First check the freeIndices stack.
/// <summary>
/// Allocates a node index from the free list or from the unused tail of the arena.
/// </summary>
/// <returns>The allocated node index, or <c>-1</c> if no node can be allocated.</returns>
internal short AllocateNode()
{
if (this.freeIndices.Count > 0)
@ -354,9 +372,9 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
/// <summary>
/// Free a node index, making it available for re-allocation.
/// Returns a node index to the free list.
/// </summary>
/// <param name="index">The index to free.</param>
/// <param name="index">The node index to recycle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void FreeNode(short index)
{
@ -367,8 +385,11 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
/// <inheritdoc/>
public void Dispose() => this.nodesOwner.Dispose();
/// <summary>
/// Represents one node in the hexadecatree node arena.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct OctreeNode
internal struct Node
{
public bool Leaf;
public int PixelCount;
@ -380,19 +401,21 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
public short NextReducibleIndex;
private InlineArray16<short> children;
/// <summary>
/// Gets the 16 child slots for this node.
/// </summary>
[UnscopedRef]
public Span<short> Children => this.children;
/// <summary>
/// Initialize the <see cref="OctreeNode"/>.
/// Initializes a node either as a leaf or as a reducible interior node.
/// </summary>
/// <param name="level">The level of the node.</param>
/// <param name="colorBits">The number of significant color bits in the image.</param>
/// <param name="octree">The parent octree.</param>
/// <param name="index">The index of the node.</param>
public void Initialize(int level, int colorBits, Octree octree, short index)
/// <param name="level">The depth of the node being initialized.</param>
/// <param name="colorBits">The maximum tree depth.</param>
/// <param name="tree">The owning tree.</param>
/// <param name="index">The node index in the arena.</param>
public void Initialize(int level, int colorBits, Hexadecatree tree, short index)
{
// Construct the new node.
this.Leaf = level == colorBits;
this.Red = 0;
this.Green = 0;
@ -401,76 +424,73 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
this.PixelCount = 0;
this.PaletteIndex = 0;
this.NextReducibleIndex = -1;
// Always clear the Children array.
this.Children.Fill(-1);
if (this.Leaf)
{
octree.Leaves++;
tree.Leaves++;
}
else
{
// Add this node to the reducible nodes list for its level.
this.NextReducibleIndex = octree.reducibleNodes[level];
octree.reducibleNodes[level] = index;
// Track reducible nodes per level so palette reduction can always collapse the deepest
// buckets first without scanning the entire arena.
this.NextReducibleIndex = tree.reducibleNodes[level];
tree.reducibleNodes[level] = index;
}
}
/// <summary>
/// Add a color to the Octree.
/// Descends the tree for the supplied color, allocating nodes as needed until a leaf is reached.
/// </summary>
/// <param name="nodeIndex">The node index.</param>
/// <param name="color">The color to add.</param>
/// <param name="colorBits">The number of significant color bits in the image.</param>
/// <param name="level">The level of the node.</param>
/// <param name="octree">The parent octree.</param>
public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Octree octree)
/// <param name="nodeIndex">The current node index.</param>
/// <param name="color">The color being accumulated.</param>
/// <param name="colorBits">The maximum tree depth.</param>
/// <param name="level">The current depth.</param>
/// <param name="tree">The owning tree.</param>
public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Hexadecatree tree)
{
ref OctreeNode node = ref octree.Nodes[nodeIndex];
ref Node node = ref tree.Nodes[nodeIndex];
if (node.Leaf)
{
Increment(nodeIndex, color, octree);
octree.TrackPrevious(nodeIndex);
Increment(nodeIndex, color, tree);
tree.TrackPrevious(nodeIndex);
return;
}
else
{
int index = GetColorIndex(color, level);
short childIndex;
Span<short> children = node.Children;
childIndex = children[index];
int index = GetColorIndex(color, level);
Span<short> children = node.Children;
short childIndex = children[index];
if (childIndex == -1)
{
childIndex = tree.AllocateNode();
if (childIndex == -1)
{
childIndex = octree.AllocateNode();
if (childIndex == -1)
{
// No room in the tree, so increment the count and return.
Increment(nodeIndex, color, octree);
octree.TrackPrevious(nodeIndex);
return;
}
ref OctreeNode child = ref octree.Nodes[childIndex];
child.Initialize(level + 1, colorBits, octree, childIndex);
children[index] = childIndex;
// If the arena is exhausted and no node can be reclaimed yet, fall back to
// accumulating into the current node instead of failing the insert outright.
Increment(nodeIndex, color, tree);
tree.TrackPrevious(nodeIndex);
return;
}
AddColor(childIndex, color, colorBits, level + 1, octree);
ref Node child = ref tree.Nodes[childIndex];
child.Initialize(level + 1, colorBits, tree, childIndex);
children[index] = childIndex;
}
// Keep descending until we reach the leaf bucket that should accumulate this sample.
AddColor(childIndex, color, colorBits, level + 1, tree);
}
/// <summary>
/// Increment the color components of this node.
/// Adds the supplied color sample to an existing node's running sums.
/// </summary>
/// <param name="nodeIndex">The node index.</param>
/// <param name="color">The color to increment by.</param>
/// <param name="octree">The parent octree.</param>
public static void Increment(int nodeIndex, Rgba32 color, Octree octree)
/// <param name="nodeIndex">The node index to update.</param>
/// <param name="color">The color sample being accumulated.</param>
/// <param name="tree">The owning tree.</param>
public static void Increment(int nodeIndex, Rgba32 color, Hexadecatree tree)
{
ref OctreeNode node = ref octree.Nodes[nodeIndex];
ref Node node = ref tree.Nodes[nodeIndex];
node.PixelCount++;
node.Red += color.R;
node.Green += color.G;
@ -479,10 +499,10 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
/// <summary>
/// Reduce this node by ensuring its children are all reduced (i.e. leaves) and then merging their data.
/// Merges all child nodes into this node and turns it into a leaf.
/// </summary>
/// <param name="octree">The parent octree.</param>
public void Reduce(Octree octree)
/// <param name="tree">The owning tree.</param>
public void Reduce(Hexadecatree tree)
{
// If already a leaf, do nothing.
if (this.Leaf)
@ -492,25 +512,27 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
// Now merge the (presumably reduced) children.
int pixelCount = 0;
int sumRed = 0, sumGreen = 0, sumBlue = 0, sumAlpha = 0;
int sumRed = 0;
int sumGreen = 0;
int sumBlue = 0;
int sumAlpha = 0;
Span<short> children = this.Children;
for (int i = 0; i < children.Length; i++)
{
short childIndex = children[i];
if (childIndex != -1)
{
ref OctreeNode child = ref octree.Nodes[childIndex];
ref Node child = ref tree.Nodes[childIndex];
int pixels = child.PixelCount;
sumRed += child.Red;
sumGreen += child.Green;
sumBlue += child.Blue;
sumAlpha += child.Alpha;
pixelCount += pixels;
// Free the child immediately.
children[i] = -1;
octree.FreeNode(childIndex);
tree.FreeNode(childIndex);
}
}
@ -529,16 +551,16 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
this.Leaf = true;
octree.Leaves++;
tree.Leaves++;
}
/// <summary>
/// Traverse the tree to construct the palette.
/// Traverses the reduced tree and emits one palette color per leaf.
/// </summary>
/// <param name="octree">The parent octree.</param>
/// <param name="palette">The palette to construct.</param>
/// <param name="paletteIndex">The current palette index.</param>
public void ConstructPalette(Octree octree, Span<TPixel> palette, ref short paletteIndex)
/// <param name="tree">The owning tree.</param>
/// <param name="palette">The destination palette span.</param>
/// <param name="paletteIndex">The running palette index.</param>
public void ConstructPalette(Hexadecatree tree, Span<TPixel> palette, ref short paletteIndex)
{
if (this.Leaf)
{
@ -549,13 +571,12 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
Vector4.Zero,
new Vector4(255));
if (vector.W < octree.transparencyThreshold255)
if (vector.W < tree.transparencyThreshold255)
{
vector = Vector4.Zero;
}
palette[paletteIndex] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W));
this.PaletteIndex = paletteIndex++;
}
else
@ -566,19 +587,20 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
int childIndex = children[i];
if (childIndex != -1)
{
octree.Nodes[childIndex].ConstructPalette(octree, palette, ref paletteIndex);
tree.Nodes[childIndex].ConstructPalette(tree, palette, ref paletteIndex);
}
}
}
}
/// <summary>
/// Get the palette index for the passed color.
/// Resolves the palette index represented by this node for the supplied color.
/// </summary>
/// <param name="color">The color to get the palette index for.</param>
/// <param name="level">The level of the node.</param>
/// <param name="octree">The parent octree.</param>
public int GetPaletteIndex(Rgba32 color, int level, Octree octree)
/// <param name="color">The color to resolve.</param>
/// <param name="level">The current tree depth.</param>
/// <param name="tree">The owning tree.</param>
/// <returns>The palette index for the best reachable leaf, or <c>-1</c> if no leaf can be reached.</returns>
public int GetPaletteIndex(Rgba32 color, int level, Hexadecatree tree)
{
if (this.Leaf)
{
@ -590,15 +612,16 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
int childIndex = children[colorIndex];
if (childIndex != -1)
{
return octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree);
return tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree);
}
// After reductions the exact branch can disappear, so fall back to the first reachable descendant leaf.
for (int i = 0; i < children.Length; i++)
{
childIndex = children[i];
if (childIndex != -1)
{
int childPaletteIndex = octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree);
int childPaletteIndex = tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree);
if (childPaletteIndex != -1)
{
return childPaletteIndex;
@ -610,37 +633,35 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
/// <summary>
/// Gets the color index at the given level.
/// Computes the child slot for a color at the supplied tree level.
/// </summary>
/// <param name="color">The color to get the index for.</param>
/// <param name="level">The level to get the index at.</param>
/// <param name="color">The color being routed.</param>
/// <param name="level">The tree depth whose bit plane should be sampled.</param>
/// <returns>The child slot index for the color at the supplied level.</returns>
/// <remarks>
/// For fully opaque mid-tone colors the tree ignores alpha and routes on RGB only, preserving more branch
/// resolution for visible color detail. For transparent, dark, and light colors it includes alpha as the
/// most significant routing bit so opacity changes can form their own branches.
/// </remarks>
public static int GetColorIndex(Rgba32 color, int level)
{
// Determine how many bits to shift based on the current tree level.
// At level 0, shift = 7; as level increases, the shift decreases.
// Sample one bit plane per level, starting at the most significant bit and moving downward.
int shift = 7 - level;
byte mask = (byte)(1 << shift);
// Compute the luminance of the RGB components using the BT.709 standard.
// This gives a measure of brightness for the color.
// Use BT.709 luminance as a cheap brightness estimate for deciding whether alpha carries
// useful information at this level for fully opaque colors.
int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B);
// Define thresholds for determining when to include the alpha bit in the index.
// The thresholds are scaled according to the current level.
// 128 is the midpoint of the 8-bit range (0–255), so shifting it right by 'level'
// produces a threshold that scales with the color cube subdivision.
// Scale the brightness thresholds with depth so deeper levels become stricter about when
// to spend a branch bit on alpha instead of RGB detail.
int darkThreshold = 128 >> level;
// The light threshold is set symmetrically: 255 minus the scaled midpoint.
int lightThreshold = 255 - (128 >> level);
// If the pixel is fully opaque and its brightness falls between the dark and light thresholds,
// ignore the alpha channel to maximize RGB resolution.
// Otherwise (if the pixel is dark, light, or semi-transparent), include the alpha bit
// to preserve any gradient that may be present.
if (color.A == 255 && luminance > darkThreshold && luminance < lightThreshold)
{
// Extract one bit each from R, G, and B channels and combine them into a 3-bit index.
// Fully opaque mid-tone colors route on RGB only, which preserves more visible color
// resolution because alpha would contribute no extra separation here.
int rBits = ((color.R & mask) >> shift) << 2;
int gBits = ((color.G & mask) >> shift) << 1;
int bBits = (color.B & mask) >> shift;
@ -648,7 +669,8 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
}
else
{
// Extract one bit from each channel including alpha (alpha becomes the most significant bit).
// Transparent, dark, and light colors include alpha as the high routing bit so opacity
// changes can form distinct buckets alongside RGB differences.
int aBits = ((color.A & mask) >> shift) << 3;
int rBits = ((color.R & mask) >> shift) << 2;
int gBits = ((color.G & mask) >> shift) << 1;

387
src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs

@ -56,147 +56,6 @@ internal interface IColorIndexCache<T> : IColorIndexCache
public static abstract T Create(MemoryAllocator allocator);
}
/// <summary>
/// A hybrid color distance cache that combines a small, fixed-capacity exact-match dictionary
/// (ExactCache, ~4–5 KB for up to 512 entries) with a coarse lookup table (CoarseCache) for 5,5,5,6 precision.
/// </summary>
/// <remarks>
/// ExactCache provides O(1) lookup for common cases using a simple 256-entry hash-based dictionary, while CoarseCache
/// quantizes RGB channels to 5 bits (yielding 32^3 buckets) and alpha to 6 bits, storing up to 4 alpha entries per bucket
/// (a design chosen based on probability theory to capture most real-world variations) for a total memory footprint of
/// roughly 576 KB. Lookups and insertions are performed in constant time, making the overall design both fast and memory-predictable.
/// </remarks>
internal unsafe struct HybridCache : IColorIndexCache<HybridCache>
{
private CoarseCache coarseCache;
private AccurateCache accurateCache;
public HybridCache(MemoryAllocator allocator)
{
this.accurateCache = AccurateCache.Create(allocator);
this.coarseCache = CoarseCache.Create(allocator);
}
/// <inheritdoc/>
public static HybridCache Create(MemoryAllocator allocator) => new(allocator);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool TryAdd(Rgba32 color, short index)
{
if (this.accurateCache.TryAdd(color, index))
{
return true;
}
return this.coarseCache.TryAdd(color, index);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryGetValue(Rgba32 color, out short value)
{
if (this.accurateCache.TryGetValue(color, out value))
{
return true;
}
return this.coarseCache.TryGetValue(color, out value);
}
/// <inheritdoc/>
public readonly void Clear()
{
this.accurateCache.Clear();
this.coarseCache.Clear();
}
/// <inheritdoc/>
public void Dispose()
{
this.accurateCache.Dispose();
this.coarseCache.Dispose();
}
}
/// <summary>
/// A coarse cache for color distance lookups that uses a fixed-size lookup table.
/// </summary>
/// <remarks>
/// This cache uses a fixed lookup table with 2,097,152 bins, each storing a 2-byte value,
/// resulting in a memory usage of approximately 4 MB. Lookups and insertions are
/// performed in constant time (O(1)) via direct table indexing. This design is optimized for
/// speed while maintaining a predictable, fixed memory footprint.
/// </remarks>
internal unsafe struct CoarseCache : IColorIndexCache<CoarseCache>
{
private const int IndexRBits = 5;
private const int IndexGBits = 5;
private const int IndexBBits = 5;
private const int IndexABits = 6;
private const int IndexRCount = 1 << IndexRBits; // 32 bins for red
private const int IndexGCount = 1 << IndexGBits; // 32 bins for green
private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue
private const int IndexACount = 1 << IndexABits; // 64 bins for alpha
private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 2,097,152 bins
private readonly IMemoryOwner<short> binsOwner;
private readonly short* binsPointer;
private MemoryHandle binsHandle;
private CoarseCache(MemoryAllocator allocator)
{
this.binsOwner = allocator.Allocate<short>(TotalBins);
this.binsOwner.GetSpan().Fill(-1);
this.binsHandle = this.binsOwner.Memory.Pin();
this.binsPointer = (short*)this.binsHandle.Pointer;
}
/// <inheritdoc/>
public static CoarseCache Create(MemoryAllocator allocator) => new(allocator);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryAdd(Rgba32 color, short value)
{
this.binsPointer[GetCoarseIndex(color)] = value;
return true;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryGetValue(Rgba32 color, out short value)
{
value = this.binsPointer[GetCoarseIndex(color)];
return value > -1; // Coarse match found
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetCoarseIndex(Rgba32 color)
{
int rIndex = color.R >> (8 - IndexRBits);
int gIndex = color.G >> (8 - IndexGBits);
int bIndex = color.B >> (8 - IndexBBits);
int aIndex = color.A >> (8 - IndexABits);
return (aIndex * IndexRCount * IndexGCount * IndexBCount) +
(rIndex * IndexGCount * IndexBCount) +
(gIndex * IndexBCount) +
bIndex;
}
/// <inheritdoc/>
public readonly void Clear()
=> this.binsOwner.GetSpan().Fill(-1);
/// <inheritdoc/>
public void Dispose()
{
this.binsHandle.Dispose();
this.binsOwner.Dispose();
}
}
/// <summary>
/// <para>
/// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values,
@ -225,7 +84,7 @@ internal unsafe struct CoarseCache : IColorIndexCache<CoarseCache>
/// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries).
/// </para>
/// </summary>
internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
internal unsafe struct CoarseCache : IColorIndexCache<CoarseCache>
{
// Use 5 bits per channel for R, G, and B: 32 levels each.
// Total buckets = 32^3 = 32768.
@ -236,7 +95,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
private readonly AlphaBucket* buckets;
private MemoryHandle bucketHandle;
private CoarseCacheLite(MemoryAllocator allocator)
private CoarseCache(MemoryAllocator allocator)
{
this.bucketsOwner = allocator.Allocate<AlphaBucket>(BucketCount, AllocationOptions.Clean);
this.bucketHandle = this.bucketsOwner.Memory.Pin();
@ -244,7 +103,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
}
/// <inheritdoc/>
public static CoarseCacheLite Create(MemoryAllocator allocator) => new(allocator);
public static CoarseCache Create(MemoryAllocator allocator) => new(allocator);
/// <inheritdoc/>
public readonly bool TryAdd(Rgba32 color, short paletteIndex)
@ -289,14 +148,11 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
}
[MethodImpl(InliningOptions.ShortMethod)]
private static byte QuantizeAlpha(byte a)
// Quantize to 6 bits: shift right by (8 - 6) = 2 bits.
=> (byte)(a >> 2);
private static byte QuantizeAlpha(byte a) => (byte)(a >> 2);
public struct AlphaEntry
{
// Store the alpha value quantized to 6 bits (0..63)
// Store the alpha value quantized to 6 bits (0..63).
public byte QuantizedAlpha;
public short PaletteIndex;
}
@ -312,7 +168,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
// 2. However, in practice (based on probability theory and typical image data),
// the number of unique alpha values that actually occur for a given quantized RGB
// bucket is usually very small. If you randomly sample 8 values out of 64,
// the probability that these 4 samples are all unique is high if the distribution
// the probability that these samples are all unique is high if the distribution
// of alpha values is skewed or if only a few alpha values are used.
//
// 3. Statistically, for many real-world images, most RGB buckets will have only a couple
@ -377,51 +233,49 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite>
}
/// <summary>
/// A fixed-capacity dictionary with exactly 512 entries mapping a <see cref="uint"/> key
/// to a <see cref="short"/> value.
/// A fixed-size exact-match cache that stores packed RGBA keys with 4-way set associativity.
/// </summary>
/// <remarks>
/// The dictionary is implemented using a fixed array of 512 buckets and an entries array
/// of the same size. The bucket for a key is computed as (key &amp; 0x1FF), and collisions are
/// resolved through a linked chain stored in the <see cref="Entry.Next"/> field.
/// The cache holds 512 total entries split across 128 sets. Entries are evicted within a set
/// using round-robin replacement, but cached values are returned only when the full packed RGBA
/// key matches, preserving exact quantization results with predictable memory usage.
/// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are,
/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are
/// typically very short; in the worst-case, the number of iterations is bounded by 256.
/// on average, O(1) since each lookup probes at most four candidate entries within the selected set.
/// This guarantees highly efficient and predictable performance for small, fixed-size color palettes.
/// </remarks>
internal unsafe struct AccurateCache : IColorIndexCache<AccurateCache>
{
// Buckets array: each bucket holds the index (0-based) into the entries array
// of the first entry in the chain, or -1 if empty.
private readonly IMemoryOwner<short> bucketsOwner;
private MemoryHandle bucketsHandle;
private short* buckets;
public const int Capacity = 512;
private const int Ways = 4;
private const int SetCount = Capacity / Ways;
private const int SetMask = SetCount - 1;
// Entries array: stores up to 256 entries.
private readonly IMemoryOwner<Entry> entriesOwner;
private MemoryHandle entriesHandle;
private Entry* entries;
private readonly IMemoryOwner<uint> keysOwner;
private MemoryHandle keysHandle;
private uint* keys;
public const int Capacity = 512;
private readonly IMemoryOwner<ushort> valuesOwner;
private MemoryHandle valuesHandle;
private ushort* values;
private readonly IMemoryOwner<byte> nextVictimOwner;
private MemoryHandle nextVictimHandle;
private byte* nextVictim;
private AccurateCache(MemoryAllocator allocator)
{
this.Count = 0;
// Allocate exactly 512 indexes for buckets.
this.bucketsOwner = allocator.Allocate<short>(Capacity, AllocationOptions.Clean);
Span<short> bucketSpan = this.bucketsOwner.GetSpan();
bucketSpan.Fill(-1);
this.bucketsHandle = this.bucketsOwner.Memory.Pin();
this.buckets = (short*)this.bucketsHandle.Pointer;
// Allocate exactly 512 entries.
this.entriesOwner = allocator.Allocate<Entry>(Capacity, AllocationOptions.Clean);
this.entriesHandle = this.entriesOwner.Memory.Pin();
this.entries = (Entry*)this.entriesHandle.Pointer;
}
this.keysOwner = allocator.Allocate<uint>(Capacity, AllocationOptions.Clean);
this.keysHandle = this.keysOwner.Memory.Pin();
this.keys = (uint*)this.keysHandle.Pointer;
public int Count { get; private set; }
this.valuesOwner = allocator.Allocate<ushort>(Capacity, AllocationOptions.Clean);
this.valuesHandle = this.valuesOwner.Memory.Pin();
this.values = (ushort*)this.valuesHandle.Pointer;
this.nextVictimOwner = allocator.Allocate<byte>(SetCount, AllocationOptions.Clean);
this.nextVictimHandle = this.nextVictimOwner.Memory.Pin();
this.nextVictim = (byte*)this.nextVictimHandle.Pointer;
}
/// <inheritdoc/>
public static AccurateCache Create(MemoryAllocator allocator) => new(allocator);
@ -430,140 +284,113 @@ internal unsafe struct AccurateCache : IColorIndexCache<AccurateCache>
[MethodImpl(InliningOptions.ShortMethod)]
public bool TryAdd(Rgba32 color, short value)
{
if (this.Count == Capacity)
{
return false; // Dictionary is full.
}
uint key = color.PackedValue;
int set = GetSetIndex(key);
int start = set * Ways;
int empty = -1;
uint* keys = this.keys;
ushort* values = this.values;
ushort storedValue = (ushort)(value + 1);
// The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A
// (with R in the most significant byte and A in the least significant).
// To compute the bucket index:
// 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels.
// 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A).
// 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A),
// which helps to counteract situations where one or more channels have a limited range.
// 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511,
// which corresponds to our fixed bucket count of 512.
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF);
int i = this.buckets[bucket];
// Traverse the collision chain.
Entry* entries = this.entries;
while (i != -1)
for (int i = start; i < start + Ways; i++)
{
Entry e = entries[i];
if (e.Key == key)
ushort candidate = values[i];
if (candidate == 0)
{
// Key already exists; do not overwrite.
return false;
empty = i;
continue;
}
i = e.Next;
if (keys[i] == key)
{
values[i] = storedValue;
return true;
}
}
short index = (short)this.Count;
this.Count++;
int slot = empty >= 0 ? empty : start + this.nextVictim[set];
keys[slot] = key;
values[slot] = storedValue;
// Insert the new entry:
entries[index].Key = key;
entries[index].Value = value;
if (empty < 0)
{
this.nextVictim[set] = (byte)((this.nextVictim[set] + 1) & (Ways - 1));
}
// Link this new entry into the bucket chain.
entries[index].Next = this.buckets[bucket];
this.buckets[bucket] = index;
return true;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool TryGetValue(Rgba32 color, out short value)
public readonly bool TryGetValue(Rgba32 color, out short value)
{
uint key = color.PackedValue;
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF);
int i = this.buckets[bucket];
int start = GetSetIndex(key) * Ways;
// If the bucket is empty, return immediately.
if (i == -1)
{
value = -1;
return false;
}
uint* keys = this.keys;
ushort* values = this.values;
// Traverse the chain.
Entry* entries = this.entries;
do
for (int i = start; i < start + Ways; i++)
{
Entry e = entries[i];
if (e.Key == key)
ushort candidate = values[i];
if (candidate != 0 && keys[i] == key)
{
value = e.Value;
value = (short)(candidate - 1);
return true;
}
i = e.Next;
}
while (i != -1);
value = -1;
return false;
}
/// <summary>
/// Clears the dictionary.
/// Clears the cache.
/// </summary>
public void Clear()
public readonly void Clear()
{
Span<short> bucketSpan = this.bucketsOwner.GetSpan();
bucketSpan.Fill(-1);
this.Count = 0;
this.valuesOwner.GetSpan().Clear();
this.nextVictimOwner.GetSpan().Clear();
}
public void Dispose()
{
this.bucketsHandle.Dispose();
this.bucketsOwner.Dispose();
this.entriesHandle.Dispose();
this.entriesOwner.Dispose();
this.buckets = null;
this.entries = null;
this.keysHandle.Dispose();
this.keysOwner.Dispose();
this.valuesHandle.Dispose();
this.valuesOwner.Dispose();
this.nextVictimHandle.Dispose();
this.nextVictimOwner.Dispose();
this.keys = null;
this.values = null;
this.nextVictim = null;
}
private struct Entry
{
public uint Key; // The key (packed RGBA)
public short Value; // The value; -1 means unused.
public short Next; // Index of the next entry in the chain, or -1 if none.
}
}
/// <summary>
/// Represents a cache that does not store any values.
/// It allows adding colors, but always returns false when trying to retrieve them.
/// </summary>
internal readonly struct NullCache : IColorIndexCache<NullCache>
{
/// <inheritdoc/>
public static NullCache Create(MemoryAllocator allocator) => default;
/// <inheritdoc/>
public bool TryAdd(Rgba32 color, short value) => true;
/// <inheritdoc/>
public bool TryGetValue(Rgba32 color, out short value)
{
value = -1;
return false;
}
/// <inheritdoc/>
public void Clear()
{
}
/// <inheritdoc/>
public void Dispose()
{
}
/// <summary>
/// Maps a packed RGBA key to one of the cache sets used by <see cref="AccurateCache"/>.
/// </summary>
/// <param name="key">The packed <see cref="Rgba32.PackedValue"/> key.</param>
/// <returns>The zero-based set index for the key.</returns>
/// <remarks>
/// <para>
/// The cache is 4-way set-associative, so this hash only needs to choose one of
/// <see cref="SetCount"/> sets before probing up to four candidate entries.
/// </para>
/// <para>
/// <see cref="Rgba32.PackedValue"/> is laid out as <c>R | (G &lt;&lt; 8) | (B &lt;&lt; 16) | (A &lt;&lt; 24)</c>.
/// The XOR-fold mixes neighboring bytes into the low bits, and the final mask selects the
/// set. With the current 128-set layout that makes the selected set effectively depend on
/// the low 7 bits of <c>R ^ G ^ B</c>. Alpha still participates in the later exact key
/// comparison, but not in set selection.
/// </para>
/// <para>
/// Collisions are expected and acceptable here. Correctness comes from the full packed-key
/// comparison during probing; this hash only aims to spread keys cheaply enough that each
/// access touches at most one 4-entry set.
/// </para>
/// </remarks>
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetSetIndex(uint key)
=> (int)(((key >> 16) ^ (key >> 8) ^ key) & SetMask);
}

12
tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs

@ -40,19 +40,19 @@ public class EncodeIndexedPng
this.bmpCore.Dispose();
}
[Benchmark(Baseline = true, Description = "ImageSharp Octree Png")]
public void PngCoreOctree()
[Benchmark(Baseline = true, Description = "ImageSharp Hexadecatree Png")]
public void PngCoreHexadecatree()
{
using MemoryStream memoryStream = new();
PngEncoder options = new() { Quantizer = KnownQuantizers.Octree };
PngEncoder options = new() { Quantizer = KnownQuantizers.Hexadecatree };
this.bmpCore.SaveAsPng(memoryStream, options);
}
[Benchmark(Description = "ImageSharp Octree NoDither Png")]
public void PngCoreOctreeNoDither()
[Benchmark(Description = "ImageSharp Hexadecatree NoDither Png")]
public void PngCoreHexadecatreeNoDither()
{
using MemoryStream memoryStream = new();
PngEncoder options = new() { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) };
PngEncoder options = new() { Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }) };
this.bmpCore.SaveAsPng(memoryStream, options);
}

302
tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs

@ -0,0 +1,302 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Benchmarks.Processing;
[Config(typeof(Config.Standard))]
public class ColorMatchingCaches
{
// IterationSetup forces BenchmarkDotNet to use a single benchmark invocation per iteration.
// Repeated lookups can safely replay a smaller working set because that workload is explicitly
// meant to model steady-state cache hits after warmup.
private const int RepeatedLookupCount = 262_144;
// DitherLike should avoid replaying the same stream across multiple passes because that warms
// the caches in a way real high-churn input usually does not. Make the single pass larger instead.
private const int DitherLikeLookupCount = 1_048_576;
private const int RepeatedPassCount = 16;
private Rgba32[] palette;
private Rgba32[] repeatedSeedColors;
private Rgba32[] repeatedLookups;
private Rgba32[] ditherLookups;
private PixelMap<Rgba32> coarse;
private PixelMap<Rgba32> legacyCoarse;
private PixelMap<Rgba32> exact;
private PixelMap<Rgba32> uncached;
[Params(16, 256)]
public int PaletteSize { get; set; }
[Params(CacheWorkload.Repeated, CacheWorkload.DitherLike)]
public CacheWorkload Workload { get; set; }
[GlobalSetup]
public void Setup()
{
this.palette = CreatePalette(this.PaletteSize);
this.repeatedSeedColors = CreateRepeatedSeedColors(this.palette);
this.repeatedLookups = CreateRepeatedLookups(this.repeatedSeedColors);
this.ditherLookups = CreateDitherLikeLookups();
this.coarse = CreatePixelMap<CoarseCache>(this.palette);
this.legacyCoarse = CreatePixelMap<LegacyCoarseCache>(this.palette);
this.exact = CreatePixelMap<AccurateCache>(this.palette);
this.uncached = CreatePixelMap<UncachedCache>(this.palette);
}
[IterationSetup]
public void ResetCaches()
{
// Each benchmark iteration should start from the same cache state so we measure
// the cache policy itself rather than warm state carried over from a previous iteration.
this.coarse.Clear(this.palette);
this.legacyCoarse.Clear(this.palette);
this.exact.Clear(this.palette);
this.uncached.Clear(this.palette);
if (this.Workload == CacheWorkload.Repeated)
{
// Prime the repeated workload so the benchmark reflects steady-state hit behavior
// instead of mostly measuring the first-wave fill cost.
Prime(this.coarse, this.repeatedSeedColors);
Prime(this.legacyCoarse, this.repeatedSeedColors);
Prime(this.exact, this.repeatedSeedColors);
Prime(this.uncached, this.repeatedSeedColors);
}
}
[GlobalCleanup]
public void Cleanup()
{
this.coarse.Dispose();
this.legacyCoarse.Dispose();
this.exact.Dispose();
this.uncached.Dispose();
}
[Benchmark(Baseline = true, Description = "Coarse")]
public int Coarse() => this.Run(this.coarse);
[Benchmark(Description = "Legacy Coarse")]
public int LegacyCoarse() => this.Run(this.legacyCoarse);
[Benchmark(Description = "Exact Cached")]
public int Exact() => this.Run(this.exact);
[Benchmark(Description = "Exact Uncached")]
public int Uncached() => this.Run(this.uncached);
public enum CacheWorkload
{
// A small working set that is intentionally reused after priming to measure hit-heavy behavior.
Repeated,
// A deterministic high-churn stream intended to resemble dithered lookups where exact repeats are rare.
DitherLike
}
private int Run(PixelMap<Rgba32> map)
{
Rgba32[] lookups = this.Workload == CacheWorkload.Repeated ? this.repeatedLookups : this.ditherLookups;
int passCount = this.Workload == CacheWorkload.Repeated ? RepeatedPassCount : 1;
int checksum = 0;
// Repeated intentionally replays the same lookup stream to measure steady-state hit behavior.
// DitherLike runs as a single larger pass so we do not turn a churn-heavy workload into an
// artificially warmed cache benchmark by replaying the exact same sequence.
for (int pass = 0; pass < passCount; pass++)
{
for (int i = 0; i < lookups.Length; i++)
{
checksum = unchecked((checksum * 31) + map.GetClosestColor(lookups[i], out _));
}
}
return checksum;
}
private static PixelMap<Rgba32> CreatePixelMap<TCache>(Rgba32[] palette)
where TCache : struct, IColorIndexCache<TCache>
=> new EuclideanPixelMap<Rgba32, TCache>(Configuration.Default, palette);
private static void Prime(PixelMap<Rgba32> map, Rgba32[] colors)
{
for (int i = 0; i < colors.Length; i++)
{
map.GetClosestColor(colors[i], out _);
}
}
private static Rgba32[] CreatePalette(int count)
{
Rgba32[] result = new Rgba32[count];
for (int i = 0; i < result.Length; i++)
{
// Use the Knuth/golden-ratio multiplicative hash constant to spread colors across
// RGBA space without clustering into a gradient. That keeps the benchmark from
// accidentally favoring any cache because the palette itself is too regular.
uint value = unchecked((uint)(i + 1) * 2654435761U);
result[i] = new(
(byte)value,
(byte)(value >> 8),
(byte)(value >> 16),
(byte)((value >> 24) | 0x80));
}
return result;
}
private static Rgba32[] CreateRepeatedSeedColors(Rgba32[] palette)
{
// Reuse colors derived from the palette but perturb them slightly so the workload still
// exercises nearest-color matching rather than only exact palette-entry hits.
int count = Math.Min(64, palette.Length * 2);
Rgba32[] result = new Rgba32[count];
for (int i = 0; i < result.Length; i++)
{
Rgba32 source = palette[(i * 17) % palette.Length];
result[i] = new(
(byte)(source.R + ((i * 3) & 0x07)),
(byte)(source.G + ((i * 5) & 0x07)),
(byte)(source.B + ((i * 7) & 0x07)),
source.A);
}
return result;
}
private static Rgba32[] CreateRepeatedLookups(Rgba32[] seedColors)
{
Rgba32[] result = new Rgba32[RepeatedLookupCount];
// Cycle a small seed set to produce a stable, hit-heavy stream after priming.
for (int i = 0; i < result.Length; i++)
{
result[i] = seedColors[i % seedColors.Length];
}
return result;
}
private static Rgba32[] CreateDitherLikeLookups()
{
Rgba32[] result = new Rgba32[DitherLikeLookupCount];
// Generate a deterministic pseudo-image signal with independent channel slopes so nearby
// samples are correlated but exact repeats are uncommon, which is closer to dithered input.
for (int i = 0; i < result.Length; i++)
{
int x = i & 511;
int y = i >> 9;
result[i] = new(
(byte)((x * 17) + (y * 13)),
(byte)((x * 29) + (y * 7)),
(byte)((x * 11) + (y * 23)),
(byte)(255 - ((x * 3) + (y * 5))));
}
return result;
}
/// <summary>
/// Preserves the original direct-mapped coarse cache implementation for side-by-side benchmarks.
/// </summary>
private unsafe struct LegacyCoarseCache : IColorIndexCache<LegacyCoarseCache>
{
private const int IndexRBits = 5;
private const int IndexGBits = 5;
private const int IndexBBits = 5;
private const int IndexABits = 6;
private const int IndexRCount = 1 << IndexRBits;
private const int IndexGCount = 1 << IndexGBits;
private const int IndexBCount = 1 << IndexBBits;
private const int IndexACount = 1 << IndexABits;
private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount;
private readonly IMemoryOwner<short> binsOwner;
private readonly short* binsPointer;
private MemoryHandle binsHandle;
private LegacyCoarseCache(MemoryAllocator allocator)
{
this.binsOwner = allocator.Allocate<short>(TotalBins);
this.binsOwner.GetSpan().Fill(-1);
this.binsHandle = this.binsOwner.Memory.Pin();
this.binsPointer = (short*)this.binsHandle.Pointer;
}
public static LegacyCoarseCache Create(MemoryAllocator allocator) => new(allocator);
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryAdd(Rgba32 color, short value)
{
this.binsPointer[GetCoarseIndex(color)] = value;
return true;
}
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryGetValue(Rgba32 color, out short value)
{
value = this.binsPointer[GetCoarseIndex(color)];
return value > -1;
}
public readonly void Clear() => this.binsOwner.GetSpan().Fill(-1);
public void Dispose()
{
this.binsHandle.Dispose();
this.binsOwner.Dispose();
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetCoarseIndex(Rgba32 color)
{
int rIndex = color.R >> (8 - IndexRBits);
int gIndex = color.G >> (8 - IndexGBits);
int bIndex = color.B >> (8 - IndexBBits);
int aIndex = color.A >> (8 - IndexABits);
return (aIndex * IndexRCount * IndexGCount * IndexBCount) +
(rIndex * IndexGCount * IndexBCount) +
(gIndex * IndexBCount) +
bIndex;
}
}
/// <summary>
/// Preserves the uncached path for exact-cache comparison benchmarks.
/// </summary>
private readonly struct UncachedCache : IColorIndexCache<UncachedCache>
{
public static UncachedCache Create(MemoryAllocator allocator) => default;
public bool TryAdd(Rgba32 color, short value) => true;
public bool TryGetValue(Rgba32 color, out short value)
{
value = -1;
return false;
}
public void Clear()
{
}
public void Dispose()
{
}
}
}

8
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -292,7 +292,7 @@ public class BmpEncoderTests
[Theory]
[WithFile(Bit32Rgb, PixelTypes.Rgba32)]
public void Encode_8BitColor_WithOctreeQuantizer<TPixel>(TestImageProvider<TPixel> provider)
public void Encode_8BitColor_WithHexadecatreeQuantizer<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
@ -304,7 +304,7 @@ public class BmpEncoderTests
BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Bit8,
Quantizer = new OctreeQuantizer()
Quantizer = new HexadecatreeQuantizer()
};
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false);
@ -385,7 +385,7 @@ public class BmpEncoderTests
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = false,
Quantizer = KnownQuantizers.Octree
Quantizer = KnownQuantizers.Hexadecatree
};
image.SaveAsBmp(reencodedStream, encoder);
reencodedStream.Seek(0, SeekOrigin.Begin);
@ -478,7 +478,7 @@ public class BmpEncoderTests
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency,
Quantizer = quantizer ?? KnownQuantizers.Octree
Quantizer = quantizer ?? KnownQuantizers.Hexadecatree
};
// Does DebugSave & load reference CompareToReferenceInput():

2
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -125,7 +125,7 @@ public class GeneralFormatTests
public static readonly TheoryData<string> QuantizerNames =
new()
{
nameof(KnownQuantizers.Octree),
nameof(KnownQuantizers.Hexadecatree),
nameof(KnownQuantizers.WebSafe),
nameof(KnownQuantizers.Werner),
nameof(KnownQuantizers.Wu)

6
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -115,7 +115,7 @@ public class GifEncoderTests
GifEncoder encoder = new()
{
ColorTableMode = FrameColorTableMode.Global,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null })
Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null })
};
// Always save as we need to compare the encoded output.
@ -124,7 +124,7 @@ public class GifEncoderTests
encoder = new GifEncoder
{
ColorTableMode = FrameColorTableMode.Local,
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }),
Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }),
};
provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local");
@ -191,7 +191,7 @@ public class GifEncoderTests
GifEncoder encoder = new()
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { MaxColors = maxColors })
};
image.Save(outStream, encoder);

85
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -411,6 +411,91 @@ public partial class PngDecoderTests
Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel);
}
[Fact]
public void Identify_AnimatedPng_ReadsFrameCountCorrectly()
{
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(48, imageInfo.FrameMetadataCollection.Count);
}
[Fact]
public void Identify_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly()
{
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(new DecoderOptions { MaxFrames = 40 }, stream);
Assert.NotNull(imageInfo);
Assert.Equal(40, imageInfo.FrameMetadataCollection.Count);
}
[Fact]
public void Load_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly()
{
TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount);
using MemoryStream stream = new(testFile.Bytes, false);
using Image image = Image.Load(new DecoderOptions { MaxFrames = 40 }, stream);
Assert.NotNull(image);
Assert.Equal(40, image.Frames.Count);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(5)]
[InlineData(10)]
[InlineData(100)]
public void Identify_AnimatedPng_FrameCount_MatchesDecode(int frameCount)
{
using Image<Rgba32> image = new(10, 10, Color.Red.ToPixel<Rgba32>());
for (int i = 1; i < frameCount; i++)
{
using ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
image.Frames.AddFrame(frame);
}
using MemoryStream stream = new();
image.Save(stream, new PngEncoder());
stream.Position = 0;
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(frameCount, imageInfo.FrameMetadataCollection.Count);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(5)]
[InlineData(10)]
[InlineData(100)]
public void Decode_AnimatedPng_FrameCount(int frameCount)
{
using Image<Rgba32> image = new(10, 10, Color.Red.ToPixel<Rgba32>());
for (int i = 1; i < frameCount; i++)
{
using ImageFrame<Rgba32> frame = new(Configuration.Default, 10, 10);
image.Frames.AddFrame(frame);
}
using MemoryStream stream = new();
image.Save(stream, new PngEncoder());
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(frameCount, decoded.Frames.Count);
}
[Theory]
[WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)]
public void Decode_MissingDataChunk_ThrowsException<TPixel>(TestImageProvider<TPixel> provider)

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -680,7 +680,7 @@ public partial class PngEncoderTests
PaletteQuantizer quantizer = new(
palette.Select(Color.FromPixel).ToArray(),
new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Hybrid });
new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Exact });
using MemoryStream ms = new();
image.Save(ms, new PngEncoder

10
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -135,9 +135,9 @@ public class WebpEncoderTests
// Alpha thresholding is 64/255F.
GifEncoder gifEncoder = new()
{
Quantizer = new OctreeQuantizer(options)
Quantizer = new HexadecatreeQuantizer(options)
};
provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "octree");
provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "hexadecatree");
gifEncoder = new GifEncoder
{
@ -152,8 +152,8 @@ public class WebpEncoderTests
};
using Image<TPixel> cloned1 = image.Clone();
cloned1.Mutate(c => c.Quantize(new OctreeQuantizer(options)));
provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "octree");
cloned1.Mutate(c => c.Quantize(new HexadecatreeQuantizer(options)));
provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "hexadecatree");
using Image<TPixel> cloned2 = image.Clone();
cloned2.Mutate(c => c.Quantize(new WuQuantizer(options)));
@ -162,7 +162,7 @@ public class WebpEncoderTests
// Now blend the images with a blue background and save as webp.
using Image<Rgba32> background1 = new(image.Width, image.Height, Color.White.ToPixel<Rgba32>());
background1.Mutate(c => c.DrawImage(cloned1, 1));
provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "octree-blended");
provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "hexadecatree-blended");
using Image<Rgba32> background2 = new(image.Width, image.Height, Color.White.ToPixel<Rgba32>());
background2.Mutate(c => c.DrawImage(cloned2, 1));

30
tests/ImageSharp.Tests/ImageInfoTests.cs

@ -54,4 +54,34 @@ public class ImageInfoTests
Assert.Equal(meta, info.Metadata);
Assert.Equal(frameMetadata.Count, info.FrameMetadataCollection.Count);
}
[Fact]
public void GetPixelMemorySize_UsesSingleFrameWhenFrameMetadataIsEmpty()
{
const int width = 10;
const int height = 20;
ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance };
meta.GetPngMetadata();
ImageInfo info = new(new Size(width, height), meta);
Assert.Equal(width * height * 4, info.GetPixelMemorySize());
}
[Fact]
public void GetPixelMemorySize_UsesFrameMetadataCountWhenAvailable()
{
const int width = 10;
const int height = 20;
IReadOnlyList<ImageFrameMetadata> frameMetadata = [new(), new(), new()];
ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance };
meta.GetPngMetadata();
ImageInfo info = new(new Size(width, height), meta, frameMetadata);
Assert.Equal(width * height * 4 * frameMetadata.Count, info.GetPixelMemorySize());
}
}

20
tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs → tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs

@ -8,37 +8,37 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization;
[Trait("Category", "Processors")]
public class OctreeQuantizerTests
public class HexadecatreeQuantizerTests
{
[Fact]
public void OctreeQuantizerConstructor()
public void HexadecatreeQuantizerConstructor()
{
QuantizerOptions expected = new() { MaxColors = 128 };
OctreeQuantizer quantizer = new(expected);
HexadecatreeQuantizer quantizer = new(expected);
Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = null };
quantizer = new OctreeQuantizer(expected);
quantizer = new HexadecatreeQuantizer(expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Null(quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson };
quantizer = new OctreeQuantizer(expected);
quantizer = new HexadecatreeQuantizer(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);
quantizer = new HexadecatreeQuantizer(expected);
Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
}
[Fact]
public void OctreeQuantizerCanCreateFrameQuantizer()
public void HexadecatreeQuantizerCanCreateFrameQuantizer()
{
OctreeQuantizer quantizer = new();
HexadecatreeQuantizer quantizer = new();
IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
@ -46,14 +46,14 @@ public class OctreeQuantizerTests
Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null });
quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null });
frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.Null(frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson });
quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson });
frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);

158
tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -75,4 +76,161 @@ public class PaletteQuantizerTests
IQuantizer quantizer = KnownQuantizers.Werner;
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
}
[Fact]
public void ExactColorMatchingMatchesUncachedAfterCacheOverflow()
{
Rgba32[] palette =
[
new Rgba32(0, 0, 0),
new Rgba32(7, 0, 0)
];
using PixelMap<Rgba32> exact = CreatePixelMap<UncachedCache>(palette);
using PixelMap<Rgba32> cachedExact = CreatePixelMap<AccurateCache>(palette);
for (int i = 0; i < AccurateCache.Capacity; i++)
{
cachedExact.GetClosestColor(CreateOverflowFillerColor(i), out _);
}
Rgba32 first = new(1, 0, 0);
Rgba32 second = new(6, 0, 0);
AssertMatchesUncached(exact, cachedExact, first);
AssertMatchesUncached(exact, cachedExact, second);
}
[Fact]
public void ExactColorMatchingMatchesUncachedAcrossManyProbeBinsAfterRepeatedEviction()
{
Rgba32[] palette = CreateGrayscalePalette(256);
using PixelMap<Rgba32> exact = CreatePixelMap<UncachedCache>(palette);
using PixelMap<Rgba32> cachedExact = CreatePixelMap<AccurateCache>(palette);
for (int i = 0; i < AccurateCache.Capacity * 2; i++)
{
cachedExact.GetClosestColor(CreateEvictionFillerColor(i), out _);
}
for (int i = 0; i < AccurateCache.Capacity; i++)
{
AssertMatchesUncached(exact, cachedExact, CreateEvictionProbeColor(i));
}
}
[Fact]
public void ExactColorMatchingMatchesUncachedForDitherStressColorSequence()
{
Rgba32[] palette = CreateGrayscalePalette(16);
using Image<Rgba32> source = CreateDitherStressImage();
using PixelMap<Rgba32> exact = CreatePixelMap<UncachedCache>(palette);
using PixelMap<Rgba32> cachedExact = CreatePixelMap<AccurateCache>(palette);
for (int y = 0; y < source.Height; y++)
{
for (int x = 0; x < source.Width; x++)
{
AssertMatchesUncached(exact, cachedExact, source[x, y]);
}
}
}
// Split the first 512 integers across R and G so the warmup loop produces 512 distinct exact colors:
// the low 8 bits go into R, and the ninth bit spills into G once R wraps after 255.
// Keeping B fixed and G offset away from zero also avoids accidentally probing the red-axis test colors below.
private static Rgba32 CreateOverflowFillerColor(int i)
=> new((byte)i, (byte)(16 + (i >> 8)), 32);
// Treat i as three packed 5-bit coordinates and expand each coordinate back to an 8-bit channel by
// shifting left by 3. That lands on the lower edge of each 5-bit coarse bucket, giving the test a
// deterministic way to fill many distinct coarse buckets before probing nearby exact colors.
private static Rgba32 CreateEvictionFillerColor(int i)
{
byte r = (byte)((i & 31) << 3);
byte g = (byte)(((i >> 5) & 31) << 3);
byte b = (byte)(((i >> 10) & 31) << 3);
return new(r, g, b);
}
// Reconstruct the same 5-bit RGB bucket coordinates used by CreateEvictionFillerColor, then set the
// low 3 bits in each channel to 0b111. That keeps the probe inside the same coarse bucket while making
// it a different exact color, which is the shape that used to expose coarse-fallback false hits.
private static Rgba32 CreateEvictionProbeColor(int i)
{
byte r = (byte)(((i & 31) << 3) | 0x07);
byte g = (byte)((((i >> 5) & 31) << 3) | 0x07);
byte b = (byte)((((i >> 10) & 31) << 3) | 0x07);
return new(r, g, b);
}
private static PixelMap<Rgba32> CreatePixelMap<TCache>(Rgba32[] palette)
where TCache : struct, IColorIndexCache<TCache>
=> new EuclideanPixelMap<Rgba32, TCache>(Configuration.Default, palette);
private static void AssertMatchesUncached(PixelMap<Rgba32> exact, PixelMap<Rgba32> cachedExact, Rgba32 color)
{
int exactIndex = exact.GetClosestColor(color, out Rgba32 exactMatch);
int cachedIndex = cachedExact.GetClosestColor(color, out Rgba32 cachedMatch);
Assert.Equal(exactIndex, cachedIndex);
Assert.Equal(exactMatch, cachedMatch);
}
private static Rgba32[] CreateGrayscalePalette(int count)
{
Rgba32[] palette = new Rgba32[count];
for (int i = 0; i < count; i++)
{
byte value = count == 1 ? (byte)0 : (byte)((i * 255) / (count - 1));
palette[i] = new Rgba32(value, value, value);
}
return palette;
}
// Generate a deterministic pseudo-image where each channel uses a different x/y slope.
// Neighboring pixels stay correlated, like real image content, but the combined RGB values
// churn heavily enough that exact repeats are rare. That makes this a useful stress input
// for verifying cached exact matching against an uncached baseline under dither-like access.
private static Image<Rgba32> CreateDitherStressImage()
{
Image<Rgba32> image = new(192, 96);
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
image[x, y] = new Rgba32(
(byte)((x * 17) + (y * 13)),
(byte)((x * 29) + (y * 7)),
(byte)((x * 11) + (y * 23)));
}
}
return image;
}
private readonly struct UncachedCache : IColorIndexCache<UncachedCache>
{
public static UncachedCache Create(MemoryAllocator allocator) => default;
public bool TryAdd(Rgba32 color, short value) => true;
public bool TryGetValue(Rgba32 color, out short value)
{
value = -1;
return false;
}
public void Clear()
{
}
public void Dispose()
{
}
}
}

26
tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs

@ -74,15 +74,15 @@ public class QuantizerTests
= new()
{
// Known uses error diffusion by default.
KnownQuantizers.Octree,
KnownQuantizers.Hexadecatree,
KnownQuantizers.WebSafe,
KnownQuantizers.Werner,
KnownQuantizers.Wu,
new OctreeQuantizer(NoDitherOptions),
new HexadecatreeQuantizer(NoDitherOptions),
new WebSafePaletteQuantizer(NoDitherOptions),
new WernerPaletteQuantizer(NoDitherOptions),
new WuQuantizer(NoDitherOptions),
new OctreeQuantizer(OrderedDitherOptions),
new HexadecatreeQuantizer(OrderedDitherOptions),
new WebSafePaletteQuantizer(OrderedDitherOptions),
new WernerPaletteQuantizer(OrderedDitherOptions),
new WuQuantizer(OrderedDitherOptions)
@ -91,52 +91,52 @@ public class QuantizerTests
public static readonly TheoryData<IQuantizer> DitherScaleQuantizers
= new()
{
new OctreeQuantizer(Diffuser0_ScaleDitherOptions),
new HexadecatreeQuantizer(Diffuser0_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions),
new WuQuantizer(Diffuser0_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions),
new HexadecatreeQuantizer(Diffuser0_25_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions),
new WuQuantizer(Diffuser0_25_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions),
new HexadecatreeQuantizer(Diffuser0_5_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions),
new WuQuantizer(Diffuser0_5_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions),
new HexadecatreeQuantizer(Diffuser0_75_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions),
new WuQuantizer(Diffuser0_75_ScaleDitherOptions),
new OctreeQuantizer(DiffuserDitherOptions),
new HexadecatreeQuantizer(DiffuserDitherOptions),
new WebSafePaletteQuantizer(DiffuserDitherOptions),
new WernerPaletteQuantizer(DiffuserDitherOptions),
new WuQuantizer(DiffuserDitherOptions),
new OctreeQuantizer(Ordered0_ScaleDitherOptions),
new HexadecatreeQuantizer(Ordered0_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions),
new WuQuantizer(Ordered0_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_25_ScaleDitherOptions),
new HexadecatreeQuantizer(Ordered0_25_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions),
new WuQuantizer(Ordered0_25_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_5_ScaleDitherOptions),
new HexadecatreeQuantizer(Ordered0_5_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions),
new WuQuantizer(Ordered0_5_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_75_ScaleDitherOptions),
new HexadecatreeQuantizer(Ordered0_75_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions),
new WuQuantizer(Ordered0_75_ScaleDitherOptions),
new OctreeQuantizer(OrderedDitherOptions),
new HexadecatreeQuantizer(OrderedDitherOptions),
new WebSafePaletteQuantizer(OrderedDitherOptions),
new WernerPaletteQuantizer(OrderedDitherOptions),
new WuQuantizer(OrderedDitherOptions),

14
tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

@ -15,12 +15,12 @@ public class QuantizedImageTests
{
WernerPaletteQuantizer werner = new();
WebSafePaletteQuantizer webSafe = new();
OctreeQuantizer octree = new();
HexadecatreeQuantizer hexadecatree = new();
WuQuantizer wu = new();
Assert.NotNull(werner.Options.Dither);
Assert.NotNull(webSafe.Options.Dither);
Assert.NotNull(octree.Options.Dither);
Assert.NotNull(hexadecatree.Options.Dither);
Assert.NotNull(wu.Options.Dither);
using (IQuantizer<Rgba32> quantizer = werner.CreatePixelSpecificQuantizer<Rgba32>(this.Configuration))
@ -33,7 +33,7 @@ public class QuantizedImageTests
Assert.NotNull(quantizer.Options.Dither);
}
using (IQuantizer<Rgba32> quantizer = octree.CreatePixelSpecificQuantizer<Rgba32>(this.Configuration))
using (IQuantizer<Rgba32> quantizer = hexadecatree.CreatePixelSpecificQuantizer<Rgba32>(this.Configuration))
{
Assert.NotNull(quantizer.Options.Dither);
}
@ -47,7 +47,7 @@ public class QuantizedImageTests
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, true)]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, false)]
public void OctreeQuantizerYieldsCorrectTransparentPixel<TPixel>(
public void HexadecatreeQuantizerYieldsCorrectTransparentPixel<TPixel>(
TestImageProvider<TPixel> provider,
bool dither)
where TPixel : unmanaged, IPixel<TPixel>
@ -60,7 +60,7 @@ public class QuantizedImageTests
options.Dither = null;
}
OctreeQuantizer quantizer = new(options);
HexadecatreeQuantizer quantizer = new(options);
foreach (ImageFrame<TPixel> frame in image.Frames)
{
@ -103,8 +103,8 @@ public class QuantizedImageTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
OctreeQuantizer octreeQuantizer = new();
IQuantizer<TPixel> quantizer = octreeQuantizer.CreatePixelSpecificQuantizer<TPixel>(Configuration.Default, new QuantizerOptions { MaxColors = 128 });
HexadecatreeQuantizer hexadecatreeQuantizer = new();
IQuantizer<TPixel> quantizer = hexadecatreeQuantizer.CreatePixelSpecificQuantizer<TPixel>(Configuration.Default, new QuantizerOptions { MaxColors = 128 });
ImageFrame<TPixel> frame = image.Frames[0];
quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -76,6 +76,7 @@ public static class TestImages
public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png";
public const string FrameOffset = "Png/animated/frame-offset.png";
public const string DefaultNotAnimated = "Png/animated/default-not-animated.png";
public const string AnimatedFrameCount = "Png/animated/issue-animated-frame-count.png";
public const string Issue2666 = "Png/issues/Issue_2666.png";
public const string Issue2882 = "Png/issues/Issue_2882.png";

3
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4
size 9270

4
tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1af50619f835b4470afac4553445176c121c3c9fa838dff937dcc56ae37941c3
size 945821
oid sha256:770061fbb29cd20bc700ce3fc57e38a758c632c3e89de51f5fbee3d5d522539e
size 912635

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:27f6e8e195c4431dc7354a379152d3a8664582bc2bb1c8960ebf4088aa6505e2
size 248709

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46b5751dc43e9ad5541913cf851ef1b061aa474a95283c712511531202d7015e
size 239326

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0086044f12a7c58e49733f203af29a8aff2826ea654730274720eada15669254
size 249163

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:85ee8479984aa52f837badbc49085c5448597fbfd987438fe25b58bad475e85f
size 239498

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f
size 216030
oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f
size 215873

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f
size 216030
oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f
size 215873

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e6d91a3ec4f974af675dc360fd5fd623ec8773cdbc88c0a3a6506880838718a
size 226727
oid sha256:5eb87f02c7924b764bbd2c951047b7204c56a0a1a0d6853a0fb3d30a56ed0184
size 226633

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7
size 220192
oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3
size 220099

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7
size 220192
oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3
size 220099

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dbd3189b559941f91dd6e0aa15b34a3e5081477400678c2396c6a66d398876f
size 230883
oid sha256:c4548abed72e4f833b33eed14392206d7232112fc651becb2351fdee27da5bc1
size 230687

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f4df5b1bc2c291ec1cf599580d198b447278412576ab998e099cc21110e82b3d
size 263152
oid sha256:832173c8ca6bd7a8bf417d83b459ccddb541daed1c31539bf596cacea455441d
size 263018

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df63a3d12e2998d5242b64169ac86e3df7ab4be585a80daddc3e3888dfcb7095
size 262298
oid sha256:15a6dc485f0c3fd4c9fbbdb6b50437d58d68210790e37f8aab32e66a864e2746
size 261872

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6eeed563b407940e2a05f068c42b52738e6e1217a1500c9230f7068ca4e9f1e
size 304162

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dc7dc55af4ef0741a66c569876ad8a2df27164a653baa5bae536e6d121b2c11
size 300528

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b65e7903fbfa1ed0682221fdd86c6f0448b3f6a886cae5379720cce881a1f1e
size 305962

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce381c2d261b9b1ca61d8f6e2ff07b992283c327dc6b7cf53c7e5c9317abb7d3
size 316443

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bfc23a95df8a88ac6e2777d67f381e800d23647c162a9a97131a101bbb97143
size 306703

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d3f58a108d933ec9ac0a5271af5b65d0a8ab9d521d54e48312b280cc42d71ac
size 322049

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be
size 269397
oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291
size 268504

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be
size 269397
oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291
size 268504

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f3e9a338a5ae37c88ce0c348e0b655429220da051db3352779c277bb2dcb441
size 270622
oid sha256:b5fa657236e12cbb2a8d2cd747029723a6b3829b475f28626d7647d7b2150918
size 271579

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c
size 284481
oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8
size 284056

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c
size 284481
oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8
size 284056

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:293459538454e07bc9ea1e9df1fa5b0eb986fde7de42f6c25b43e4c8859bd28a
size 285370
oid sha256:61ed5f4d77428be46357609d80a66e884dedbb8c255fdcc71d49eeba0eed2bf2
size 285037

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90a2b7b3872c6eb1f1f039558d9f6ace92891c86951c801da01ad55b055fd670
size 316544
oid sha256:1cc2ef3cb819b5a82e0af32c3ab44aff0206530e291b00bdade58da2ebe4494a
size 308246

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff094e6bafe81e818bcbac69018dcfe29366389dfca0d63d8e05ef42896ffe1d
size 317309
oid sha256:575c8d81152642fa0eec0ea9901d1941fea58b7686cfaac1d01e0bf59f393c4b
size 308330

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee0778aac671365dd0afae06cdcf8f36243bd9815f684b975f83e297bb694e63
size 323979
oid sha256:ba295a5ddb79bc61f0be9a28a636fdcc63055c26c46872d407fe20ff785f11ed
size 310415

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5
size 13160

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9a87ef109c08411ca61d91ddcf010c272303a17abd90b6ba2204eac021055e5
size 13665

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81496d88b42edf4b39ab723d0b5414b56140892f45d30fc2435904b630fa9af5
size 13886

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5
size 13160

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:997e5281abd8cf3a587984ec1b7e31487ec5ddf16326d025124833d536e4ac27
size 13910

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5
size 13160

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89
size 13158

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abfdd1e40c2c1d7fde419bda1da6e534ed989598e790b8ae4de35152a83f77a0
size 13686

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60c28eb1dc3c0416b20cec230917c0e4a70dd2929467bbab796ecbb04fe5a178
size 13886

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89
size 13158

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a523f097bf3b155f3823c5e400190b5d5e0d4470db7136576472c3257db76600
size 13909

3
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89
size 13158

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2bd11fa19fab712b5cd6c2b36d673c7dce904b5032b860d257b00e095e4aadf
size 13432
oid sha256:dd31b6fc59e1f9f88230d57b39362b76cedd0bd94e15904f69071ba3f465e48d
size 13656

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d
size 13158
oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489
size 13160

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4ac8b88b317281738d833fc71f52348d9f4f45ea5a1303dd91fdb8b42be4267
size 13186
oid sha256:dd738ee2a397bb1ee305f03c70e185dea6f67827dc15b9df1966cfe8c0f28040
size 13177

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1305d54f2139d4577490317051d6ce94a7fc8dd45b902d87a30fb04098dd4594
size 13407
oid sha256:2a2df64f89df17428415932c2ef0028d8ad408b5276264d99e6038b70473ebde
size 13417

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d
size 13158
oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489
size 13160

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3fc3a7ace123c330ea06072eb36dd5d65ed9154d4d0f55a828fc542c8a422c1
size 13472
oid sha256:234854be2a3f774a58baf79f20e68c7331b6caff486ab4b1e509a96e2a3d70b9
size 13455

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35757f2e0831cae2fbd3cc11ffaaae855e853ebaa9a1a5564b6568a5e1c442e9
size 16031
oid sha256:ef65ce360293ca5659730747087c15735c15df1143204acb60120a5b68cd7cd4
size 15905

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6679d6d6f7c8b44461956b54654cea71180a2b0d43712d3775e60cbedd90cc82
size 17520
oid sha256:6618f169cf4b585979f8e9261af88fe4a61c3c40b453a159cb643cc062a6a9dc
size 17517

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d
size 13158
oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489
size 13160

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5af5d16f875172d73f8426928fc8edaa4a6cab321a968b6c29fca32d0fba0df5
size 18182
oid sha256:4acf21f23978c83c9872bb2575ab45e4f0bbc86c8610c99479b1469fc12df5f2
size 18112

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png

0
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png → tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8ba00e2948337f77d935d98349958c6a520958671e9ec714ff1bfadfb130e72
size 44622
oid sha256:4ded8db323023a7c7620bba3b2259a549571442fe0a37883c7755ac69ae9d6d5
size 44646

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3802cfe67638a24869d6cc9ace1d94460b4c0c26f2c91b12b95fa8f979de64bb
size 101579
oid sha256:83c8403f5d0e5457721d992c1e6980134e8a65a1f646163a4f091cf34583ca02
size 101417

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf2021eba9edbb2295924f8394472ac0bb237f0c462c39aa32a2074ef15f9acc
size 81771
oid sha256:e5412b892143bb433804c662750a64a1660b2072520db53d76ec6897c636ec50
size 81742

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d11b18946d373b995ecbb449c8c4cfcc7078aad1c8705997bcbf83131acde03
size 102439
oid sha256:a88a48586502de786aca0b36341cf6033fb3ec3ce7924ce1e2819fd14791ffe4
size 102235

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save