Browse Source

Fix all known quantizing issues

pull/2894/head
James Jackson-South 12 months ago
parent
commit
4e21188efe
  1. 29
      src/ImageSharp/Common/InlineArray.cs
  2. 38
      src/ImageSharp/Common/InlineArray.tt
  3. 3
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  4. 1
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  5. 3
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  6. 39
      src/ImageSharp/Formats/EncodingUtilities.cs
  7. 141
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  8. 93
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  9. 3
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  10. 12
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  11. 6
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  12. 8
      src/ImageSharp/Formats/IFormatMetadata.cs
  13. 4
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  14. 1
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  15. 3
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  16. 12
      src/ImageSharp/Formats/Png/PngEncoder.cs
  17. 246
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  18. 3
      src/ImageSharp/Formats/Png/PngMetadata.cs
  19. 2
      src/ImageSharp/Formats/TransparentColorMode.cs
  20. 2
      src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
  21. 14
      src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs
  22. 110
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  23. 2
      src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
  24. 14
      src/ImageSharp/ImageSharp.csproj
  25. 4
      src/ImageSharp/IndexedImageFrame{TPixel}.cs
  26. 6
      src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
  27. 4
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  28. 8
      src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
  29. 7
      src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs
  30. 521
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  31. 12
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs
  32. 766
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs
  33. 23
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  34. 55
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  35. 19
      src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs
  36. 11
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  37. 38
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  38. 71
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  39. 15
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs
  40. 11
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  41. 46
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  42. 4
      tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs
  43. 16
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  44. 2
      tests/ImageSharp.Tests/Image/ImageFrameTests.cs
  45. 2
      tests/ImageSharp.Tests/Image/ImageTests.cs
  46. 4
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  47. 2
      tests/ImageSharp.Tests/TestImages.cs
  48. 36
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  49. 2
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp
  50. 2
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp
  51. 4
      tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png
  52. 4
      tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png
  53. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png
  54. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png
  55. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png
  56. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png
  57. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png
  58. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png
  59. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png
  60. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png
  61. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png
  62. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png
  63. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png
  64. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png
  65. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png
  66. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png
  67. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png
  68. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png
  69. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png
  70. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png
  71. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png
  72. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png
  73. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png
  74. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png
  75. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png
  76. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png
  77. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png
  78. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png
  79. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png
  80. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png
  81. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png
  82. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png
  83. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png
  84. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png
  85. 4
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png
  86. 4
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png
  87. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png
  88. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png
  89. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png
  90. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png
  91. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png
  92. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png
  93. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png
  94. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png
  95. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png
  96. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png
  97. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png
  98. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png
  99. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_OrderedDither.png
  100. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png

29
src/ImageSharp/Common/InlineArray.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// <auto-generated />
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp;
/// <summary>
/// Represents a safe, fixed sized buffer of 4 elements.
/// </summary>
[InlineArray(4)]
internal struct InlineArray4<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 16 elements.
/// </summary>
[InlineArray(16)]
internal struct InlineArray16<T>
{
private T t;
}

38
src/ImageSharp/Common/InlineArray.tt

@ -0,0 +1,38 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// <auto-generated />
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
private static int[] Lengths = new int[] {4, 16 };
void GenerateInlineArrays()
{
foreach (int length in Lengths)
{
#>
/// <summary>
/// Represents a safe, fixed sized buffer of <#=length#> elements.
/// </summary>
[InlineArray(<#=length#>)]
internal struct InlineArray<#=length#><T>
{
private T t;
}
<#+
}
}
#>

3
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -158,6 +158,5 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
}

1
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -126,6 +126,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>

3
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -152,8 +152,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

39
src/ImageSharp/Formats/EncodingUtilities.cs

@ -3,6 +3,7 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -24,13 +25,29 @@ internal static class EncodingUtilities
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> frame, Color color)
where TPixel : unmanaged, IPixel<TPixel>
=> ClearTransparentPixels(frame.Configuration, frame.PixelBuffer, color);
/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="buffer">The <see cref="Buffer2D{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClearTransparentPixels<TPixel>(
Configuration configuration,
Buffer2D<TPixel> buffer,
Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> buffer = clone.PixelBuffer.GetRegion();
ClearTransparentPixels(clone.Configuration, ref buffer, color);
Buffer2DRegion<TPixel> region = buffer.GetRegion();
ClearTransparentPixels(configuration, in region, color);
}
/// <summary>
@ -39,29 +56,27 @@ internal static class EncodingUtilities
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="clone">The cloned <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
/// <param name="region">The <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(
Configuration configuration,
ref Buffer2DRegion<TPixel> clone,
in Buffer2DRegion<TPixel> region,
Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(clone.Width);
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(region.Width);
Span<Vector4> vectorsSpan = vectors.GetSpan();
Vector4 replacement = color.ToScaledVector4();
for (int y = 0; y < clone.Height; y++)
for (int y = 0; y < region.Height; y++)
{
Span<TPixel> span = clone.DangerousGetRowSpan(y);
Span<TPixel> span = region.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
ClearTransparentPixelRow(vectorsSpan, replacement);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
}
}
private static void ClearTransparentPixelRow(
Span<Vector4> vectorsSpan,
Vector4 replacement)
private static void ClearTransparentPixelRow(Span<Vector4> vectorsSpan, Vector4 replacement)
{
if (Vector128.IsHardwareAccelerated)
{

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

@ -89,6 +89,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// </summary>
private GifMetadata? gifMetadata;
/// <summary>
/// The background color used to fill the frame.
/// </summary>
private Color backgroundColor;
/// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary>
@ -108,9 +113,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
uint frameCount = 0;
Image<TPixel>? image = null;
ImageFrame<TPixel>? previousFrame = null;
FrameDisposalMode? previousDisposalMode = null;
bool globalColorTableUsed = false;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
TPixel backgroundPixel = this.backgroundColor.ToPixel<TPixel>();
// Loop though the respective gif parts and read the data.
int nextFlag = stream.ReadByte();
@ -123,7 +132,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
this.ReadFrame(stream, ref image, ref previousFrame);
globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMode, backgroundPixel);
// Reset per-frame state.
this.imageDescriptor = default;
@ -158,6 +167,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
@ -179,6 +195,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
uint frameCount = 0;
ImageFrameMetadata? previousFrame = null;
List<ImageFrameMetadata> framesMetadata = [];
bool globalColorTableUsed = false;
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@ -194,7 +212,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
globalColorTableUsed |= this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
// Reset per-frame state.
this.imageDescriptor = default;
@ -229,6 +247,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
break;
}
}
// We cannot always trust the global GIF palette has actually been used.
// https://github.com/SixLabors/ImageSharp/issues/2866
if (!globalColorTableUsed)
{
this.gifMetadata.ColorTableMode = FrameColorTableMode.Local;
}
}
finally
{
@ -416,7 +441,15 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="backgroundPixel">The background color pixel.</param>
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref FrameDisposalMode? previousDisposalMode,
TPixel backgroundPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ReadImageDescriptor(stream);
@ -438,10 +471,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable);
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundPixel);
// Skip any remaining blocks
SkipBlock(stream);
return !hasLocalColorTable;
}
/// <summary>
@ -451,46 +486,36 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="previousDisposalMode">The previous frame disposal mode.</param>
/// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="backgroundPixel">The background color pixel.</param>
private void ReadFrameColors<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ReadOnlySpan<Rgb24> colorTable)
ref FrameDisposalMode? previousDisposalMode,
ReadOnlySpan<Rgb24> colorTable,
TPixel backgroundPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
GifImageDescriptor descriptor = this.imageDescriptor;
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
ImageFrame<TPixel>? prevFrame = null;
ImageFrame<TPixel>? currentFrame = null;
ImageFrame<TPixel> imageFrame;
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame;
if (previousFrame is null)
{
if (!transFlag)
{
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
image = new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
}
image = transFlag
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
imageFrame = image.Frames.RootFrame;
currentFrame = image.Frames.RootFrame;
}
else
{
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToPrevious)
{
prevFrame = previousFrame;
}
// We create a clone of the frame and add it.
// We will overpaint the difference of pixels on the current frame to create a complete image.
// This ensures that we have enough pixel data to process without distortion. #2450
@ -498,9 +523,19 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.SetFrameMetadata(currentFrame.Metadata);
imageFrame = currentFrame;
if (previousDisposalMode == FrameDisposalMode.RestoreToBackground)
{
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag);
}
}
Rectangle interest = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
previousFrame = currentFrame;
previousDisposalMode = disposalMethod;
this.RestoreToBackground(imageFrame);
if (disposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = interest;
}
if (colorTable.Length == 0)
@ -568,7 +603,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// #403 The left + width value can be larger than the image width
int maxX = Math.Min(descriptorRight, imageWidth);
Span<TPixel> row = imageFrame.PixelBuffer.DangerousGetRowSpan(writeY);
Span<TPixel> row = currentFrame.PixelBuffer.DangerousGetRowSpan(writeY);
// Take the descriptorLeft..maxX slice of the row, so the loop can be simplified.
row = row[descriptorLeft..maxX];
@ -599,19 +634,6 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
}
}
if (prevFrame != null)
{
previousFrame = prevFrame;
return;
}
previousFrame = currentFrame ?? image.Frames.RootFrame;
if (this.graphicsControlExtension.DisposalMethod == FrameDisposalMode.RestoreToBackground)
{
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
}
}
/// <summary>
@ -620,7 +642,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="frameMetadata">The collection of frame metadata.</param>
/// <param name="previousFrame">The previous frame metadata.</param>
private void ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
/// <returns>Whether the frame has a global color table.</returns>
private bool ReadFrameMetadata(BufferedReadStream stream, List<ImageFrameMetadata> frameMetadata, ref ImageFrameMetadata? previousFrame)
{
this.ReadImageDescriptor(stream);
@ -632,6 +655,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
else
{
this.currentLocalColorTable = null;
this.currentLocalColorTableSize = 0;
}
// Skip the frame indices. Pixels length + mincode size.
// The gif format does not tell us the length of the compressed data beforehand.
@ -649,6 +677,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// Skip any remaining blocks
SkipBlock(stream);
return !this.imageDescriptor.LocalColorTableFlag;
}
/// <summary>
@ -656,7 +686,9 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The frame.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame)
/// <param name="background">The background color.</param>
/// <param name="transparent">Whether the background is transparent.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> frame, TPixel background, bool transparent)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.restoreArea is null)
@ -666,7 +698,14 @@ internal sealed class GifDecoderCore : ImageDecoderCore
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
if (transparent)
{
pixelRegion.Clear();
}
else
{
pixelRegion.Fill(background);
}
this.restoreArea = null;
}
@ -775,7 +814,19 @@ internal sealed class GifDecoderCore : ImageDecoderCore
}
}
this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
// If the global color table is present, we can set the background color
// otherwise we default to transparent to match browser behavior.
ReadOnlyMemory<Color>? table = this.gifMetadata.GlobalColorTable;
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
if (table is not null && index < table.Value.Length)
{
this.backgroundColor = table.Value.Span[index];
this.gifMetadata.BackgroundColorIndex = index;
}
else
{
this.backgroundColor = Color.Transparent;
}
}
private unsafe struct ScratchBuffer

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

@ -39,6 +39,11 @@ internal sealed class GifEncoderCore
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// The fallback quantizer to use when no quantizer is provided.
/// </summary>
private static readonly IQuantizer FallbackQuantizer = KnownQuantizers.Octree;
/// <summary>
/// Whether the quantizer was supplied via options.
/// </summary>
@ -67,6 +72,9 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// The transparent color mode.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <summary>
@ -104,14 +112,18 @@ internal sealed class GifEncoderCore
GifMetadata gifMetadata = image.Metadata.CloneGifMetadata();
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == FrameColorTableMode.Global;
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
bool useGlobalTableForFirstFrame = useGlobalTable;
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
if (frameMetadata.ColorTableMode == FrameColorTableMode.Local)
{
useGlobalTableForFirstFrame = false;
}
// Quantize the first image frame returning a palette.
IndexedImageFrame<TPixel>? quantized = null;
if (this.quantizer is null)
{
// Is this a gif with color information. If so use that, otherwise use octree.
@ -121,21 +133,22 @@ internal sealed class GifEncoderCore
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256)
{
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null });
}
else
{
this.quantizer = KnownQuantizers.Octree;
this.quantizer = FallbackQuantizer;
}
}
else
{
this.quantizer = KnownQuantizers.Octree;
this.quantizer = FallbackQuantizer;
}
}
// Quantize the first frame. Checking to see whether we can clear the transparent pixels
// to allow for a smaller color palette and encoded result.
Color background = Color.Transparent;
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
ImageFrame<TPixel>? clonedFrame = null;
@ -147,24 +160,40 @@ internal sealed class GifEncoderCore
clonedFrame = image.Frames.RootFrame.Clone();
GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
if (frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground)
{
background = this.backgroundColor ?? Color.Transparent;
}
EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
if (useGlobalTable)
if (useGlobalTableForFirstFrame)
{
frameQuantizer.BuildPalette(configuration, mode, strategy, image);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
if (useGlobalTable)
{
frameQuantizer.BuildPalette(configuration, mode, strategy, image, background);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
else
{
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame, background);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
}
}
else
{
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,
encodingFrame.Bounds,
frameMetadata,
true,
default,
false,
frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1,
background);
}
clonedFrame?.Dispose();
@ -259,8 +288,8 @@ internal sealed class GifEncoderCore
return;
}
PaletteQuantizer<TPixel> paletteQuantizer = default;
bool hasPaletteQuantizer = false;
PaletteQuantizer<TPixel> globalPaletteQuantizer = default;
bool hasGlobalPaletteQuantizer = false;
// Store the first frame as a reference for de-duplication comparison.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
@ -280,14 +309,13 @@ internal sealed class GifEncoderCore
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
if (!useLocal && !hasGlobalPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
globalPaletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette);
hasGlobalPaletteQuantizer = true;
}
this.EncodeAdditionalFrame(
@ -298,7 +326,7 @@ internal sealed class GifEncoderCore
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
globalPaletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
@ -307,9 +335,9 @@ internal sealed class GifEncoderCore
}
finally
{
if (hasPaletteQuantizer)
if (hasGlobalPaletteQuantizer)
{
paletteQuantizer.Dispose();
globalPaletteQuantizer.Dispose();
}
}
}
@ -387,7 +415,8 @@ internal sealed class GifEncoderCore
useLocal,
globalPaletteQuantizer,
difference,
transparencyIndex);
transparencyIndex,
background);
this.WriteGraphicalControlExtension(metadata, stream);
@ -410,7 +439,8 @@ internal sealed class GifEncoderCore
bool useLocal,
PaletteQuantizer<TPixel> globalPaletteQuantizer,
bool hasDuplicates,
int transparencyIndex)
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> quantized;
@ -434,14 +464,14 @@ internal sealed class GifEncoderCore
transparencyIndex = palette.Length;
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex, transparentColor);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
else
{
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
@ -454,7 +484,7 @@ internal sealed class GifEncoderCore
else
{
// Just use the local palette.
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex, transparentColor);
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
@ -462,7 +492,7 @@ internal sealed class GifEncoderCore
else
{
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options);
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
@ -486,7 +516,8 @@ internal sealed class GifEncoderCore
else
{
// Quantize the image using the global palette.
// Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
// Individual frames, though using the shared palette, can use a different transparent index
// to represent transparency.
// A difference was captured but the metadata does not have transparency.
if (hasDuplicates && !metadata.HasTransparency)
@ -496,7 +527,7 @@ internal sealed class GifEncoderCore
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
globalPaletteQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>());
quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds);
}

3
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -129,8 +129,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.LocalColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

12
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -101,7 +101,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
int bpp = this.GlobalColorTable.HasValue
int bpp = this.ColorTableMode == FrameColorTableMode.Global && this.GlobalColorTable.HasValue
? Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.GlobalColorTable.Value.Length), 1, 8)
: 8;
@ -115,15 +115,16 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
{
Color color = this.GlobalColorTable.HasValue && this.GlobalColorTable.Value.Span.Length > this.BackgroundColorIndex
bool global = this.ColorTableMode == FrameColorTableMode.Global;
Color color = global && this.GlobalColorTable.HasValue && this.GlobalColorTable.Value.Span.Length > this.BackgroundColorIndex
? this.GlobalColorTable.Value.Span[this.BackgroundColorIndex]
: Color.Transparent;
return new()
return new FormatConnectingMetadata()
{
AnimateRootFrame = true,
ColorTable = global ? this.GlobalColorTable : null,
BackgroundColor = color,
ColorTable = this.GlobalColorTable,
ColorTableMode = this.ColorTableMode,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = this.RepeatCount,
@ -133,8 +134,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.GlobalColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -14,7 +14,7 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// Converts the metadata to a <see cref="FormatConnectingFrameMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns>
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
/// <summary>
/// This method is called after a process has been applied to the image frame.
@ -22,7 +22,7 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -39,6 +39,6 @@ public interface IFormatFrameMetadata<TSelf> : IFormatFrameMetadata, IDeepClonea
/// <param name="metadata">The <see cref="FormatConnectingFrameMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata);
public static abstract TSelf FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

8
src/ImageSharp/Formats/IFormatMetadata.cs

@ -14,20 +14,20 @@ public interface IFormatMetadata : IDeepCloneable
/// Converts the metadata to a <see cref="PixelTypeInfo"/> instance.
/// </summary>
/// <returns>The pixel type info.</returns>
PixelTypeInfo GetPixelTypeInfo();
public PixelTypeInfo GetPixelTypeInfo();
/// <summary>
/// Converts the metadata to a <see cref="FormatConnectingMetadata"/> instance.
/// </summary>
/// <returns>The <see cref="FormatConnectingMetadata"/>.</returns>
FormatConnectingMetadata ToFormatConnectingMetadata();
public FormatConnectingMetadata ToFormatConnectingMetadata();
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -44,6 +44,6 @@ public interface IFormatMetadata<TSelf> : IFormatMetadata, IDeepCloneable<TSelf>
/// <param name="metadata">The <see cref="FormatConnectingMetadata"/>.</param>
/// <returns>The <typeparamref name="TSelf"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata);
public static abstract TSelf FromFormatConnectingMetadata(FormatConnectingMetadata metadata);
#pragma warning restore CA1000 // Do not declare static members on generic types
}

4
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

@ -13,12 +13,12 @@ public interface IQuantizingImageEncoder
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }
public IQuantizer? Quantizer { get; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
public IPixelSamplingStrategy PixelSamplingStrategy { get; }
}
/// <summary>

1
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -119,6 +119,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>

3
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -152,8 +152,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

12
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
@ -10,16 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// </summary>
public class PngEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
/// </summary>
public PngEncoder()
// Hack. TODO: Investigate means to fix/optimize the Wu quantizer.
// The Wu quantizer does not handle the default sampling strategy well for some larger images.
// It's expensive and the results are not better than the extensive strategy.
=> this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy();
/// <summary>
/// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType" /> values.

246
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -3,8 +3,8 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.IO.Hashing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
@ -119,18 +119,13 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary>
private IQuantizer? quantizer;
/// <summary>
/// Any explicit quantized transparent index provided by the background color.
/// </summary>
private int derivedTransparencyIndex = -1;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
private Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
@ -158,7 +153,6 @@ internal sealed class PngEncoderCore : IDisposable
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.animateRootFrame = encoder.AnimateRootFrame;
}
@ -187,74 +181,92 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel>? clonedFrame = null;
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0;
IndexedImageFrame<TPixel>? quantized = null;
PaletteQuantizer<TPixel>? paletteQuantizer = null;
Buffer2DRegion<TPixel> currentFrameRegion = currentFrame.PixelBuffer.GetRegion();
bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode);
if (clearTransparency)
try
{
currentFrame = clonedFrame = currentFrame.Clone();
EncodingUtilities.ClearTransparentPixels(currentFrame, Color.Transparent);
}
int currentFrameIndex = 0;
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
currentFrame,
currentFrame.Bounds,
null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteCicpChunk(stream, metadata);
this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode);
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
currentFrameRegion = currentFrame.PixelBuffer.GetRegion();
EncodingUtilities.ClearTransparentPixels(this.configuration, in currentFrameRegion, this.backgroundColor.Value);
}
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(
stream,
(uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)),
this.repeatCount ?? pngMetadata.RepeatCount);
}
// Do not move this. We require an accurate bit depth for the header chunk.
quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
image,
currentFrame,
currentFrame.Bounds,
null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
this.WriteCicpChunk(stream, metadata);
this.WriteColorProfileChunk(stream, metadata);
this.WritePaletteChunk(stream, quantized);
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
bool userAnimateRootFrame = this.animateRootFrame == true;
if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
cancellationToken.ThrowIfCancellationRequested();
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
currentFrameIndex++;
}
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(
stream,
(uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)),
this.repeatCount ?? pngMetadata.RepeatCount);
}
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
bool userAnimateRootFrame = this.animateRootFrame == true;
if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
cancellationToken.ThrowIfCancellationRequested();
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false);
currentFrameIndex++;
}
try
{
if (image.Frames.Count > 1)
{
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
currentFrameRegion = currentFrame.PixelBuffer.GetRegion();
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false);
}
else
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
sequenceNumber += this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, true);
}
currentFrameIndex++;
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
ReadOnlyMemory<TPixel> previousPalette = quantized?.Palette.ToArray();
if (!previousPalette.IsEmpty)
{
// Use the previously derived global palette and a shared quantizer to
// quantize the subsequent frames. This allows us to cache the color matching resolution.
paletteQuantizer ??= new(
this.configuration,
this.quantizer!.Options,
previousPalette);
}
// Write following frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
@ -267,13 +279,16 @@ internal sealed class PngEncoderCore : IDisposable
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
currentFrameRegion = currentFrame.PixelBuffer.GetRegion();
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
? this.backgroundColor.Value
: Color.Transparent;
(bool difference, Rectangle bounds) =
@ -296,8 +311,20 @@ internal sealed class PngEncoderCore : IDisposable
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
quantized = this.CreateQuantizedFrame(
this.encoder,
this.colorType,
this.bitDepth,
pngMetadata,
image,
encodingFrame,
bounds,
paletteQuantizer,
default);
Buffer2DRegion<TPixel> encodingFrameRegion = encodingFrame.PixelBuffer.GetRegion(bounds);
sequenceNumber += this.WriteDataChunks(in frameControl, in encodingFrameRegion, quantized, stream, true) + 1;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode;
@ -313,6 +340,7 @@ internal sealed class PngEncoderCore : IDisposable
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
paletteQuantizer?.Dispose();
}
}
@ -328,18 +356,35 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="image">The image.</param>
/// <param name="frame">The current image frame.</param>
/// <param name="bounds">The area of interest within the frame.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
/// <param name="paletteQuantizer">The quantizer containing any previously derived palette.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel>? CreateQuantizedImageAndUpdateBitDepth<TPixel>(
PngMetadata metadata,
Image<TPixel> image,
ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette)
PaletteQuantizer<TPixel>? paletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, bounds, previousPalette);
PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata();
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedFrame(
this.encoder,
this.colorType,
this.bitDepth,
metadata,
image,
frame,
bounds,
paletteQuantizer,
background);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
@ -1105,7 +1150,7 @@ internal sealed class PngEncoderCore : IDisposable
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
/// <param name="isFrame">Is writing fdAT or IDAT.</param>
private uint WriteDataChunks<TPixel>(FrameControl frameControl, Buffer2DRegion<TPixel> frame, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
private uint WriteDataChunks<TPixel>(in FrameControl frameControl, in Buffer2DRegion<TPixel> frame, IndexedImageFrame<TPixel>? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] buffer;
@ -1123,12 +1168,12 @@ internal sealed class PngEncoderCore : IDisposable
}
else
{
this.EncodeAdam7Pixels(frame, deflateStream);
this.EncodeAdam7Pixels(in frame, deflateStream);
}
}
else
{
this.EncodePixels(frame, quantized, deflateStream);
this.EncodePixels(in frame, quantized, deflateStream);
}
}
@ -1196,7 +1241,7 @@ internal sealed class PngEncoderCore : IDisposable
/// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="quantized">The quantized pixels.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(Buffer2DRegion<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
private void EncodePixels<TPixel>(in Buffer2DRegion<TPixel> pixels, IndexedImageFrame<TPixel>? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(pixels.Width);
@ -1222,7 +1267,7 @@ internal sealed class PngEncoderCore : IDisposable
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="pixels">The image frame pixel buffer.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(Buffer2DRegion<TPixel> pixels, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(in Buffer2DRegion<TPixel> pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
for (int pass = 0; pass < 7; pass++)
@ -1258,7 +1303,7 @@ internal sealed class PngEncoderCore : IDisposable
// Encode data
// Note: quantized parameter not used
// Note: row parameter not used
this.CollectAndFilterPixelRow<TPixel>(block, ref filter, ref attempt, null, -1);
this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1);
deflateStream.Write(filter);
this.SwapScanlineBuffers();
@ -1432,6 +1477,7 @@ internal sealed class PngEncoderCore : IDisposable
/// <param name="pngMetadata">The PNG metadata.</param>
/// <param name="use16Bit">if set to <c>true</c> [use16 bit].</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param>
[MemberNotNull(nameof(backgroundColor))]
private void SanitizeAndSetEncoderOptions<TPixel>(
PngEncoder encoder,
PngMetadata pngMetadata,
@ -1473,6 +1519,7 @@ internal sealed class PngEncoderCore : IDisposable
this.interlaceMode = encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod;
this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None;
this.backgroundColor = encoder.BackgroundColor ?? pngMetadata.TransparentColor ?? Color.Transparent;
}
/// <summary>
@ -1483,17 +1530,21 @@ internal sealed class PngEncoderCore : IDisposable
/// <param name="colorType">The color type.</param>
/// <param name="bitDepth">The bits per component.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="frame">The frame to quantize.</param>
/// <param name="image">The image.</param>
/// <param name="frame">The current image frame.</param>
/// <param name="bounds">The frame area of interest.</param>
/// <param name="previousPalette">Any previously derived palette.</param>
/// <param name="paletteQuantizer">The quantizer containing any previously derived palette.</param>
/// <param name="backgroundColor">The background color.</param>
private IndexedImageFrame<TPixel>? CreateQuantizedFrame<TPixel>(
QuantizingImageEncoder encoder,
PngColorType colorType,
byte bitDepth,
PngMetadata metadata,
Image<TPixel> image,
ImageFrame<TPixel> frame,
Rectangle bounds,
ReadOnlyMemory<TPixel>? previousPalette)
PaletteQuantizer<TPixel>? paletteQuantizer,
Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
if (colorType is not PngColorType.Palette)
@ -1501,55 +1552,52 @@ internal sealed class PngEncoderCore : IDisposable
return null;
}
if (previousPalette is not null)
if (paletteQuantizer.HasValue)
{
// Use the previously derived palette created by quantizing the root frame to quantize the current frame.
using PaletteQuantizer<TPixel> paletteQuantizer = new(
this.configuration,
this.quantizer!.Options,
previousPalette.Value,
this.derivedTransparencyIndex);
paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
return paletteQuantizer.QuantizeFrame(frame, bounds);
return paletteQuantizer.Value.QuantizeFrame(frame, bounds);
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer is null)
{
if (metadata.ColorTable is not null)
if (metadata.ColorTable?.Length > 0)
{
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlySpan<Color> palette = metadata.ColorTable.Value.Span;
// Certain operations perform alpha premultiplication, which can cause the color to change so we
// must search for the transparency index in the palette.
// Transparent pixels are much more likely to be found at the end of a palette.
int index = -1;
for (int i = palette.Length - 1; i >= 0; i--)
{
Vector4 instance = palette[i].ToScaledVector4();
if (instance.W == 0f)
{
index = i;
break;
}
}
this.derivedTransparencyIndex = index;
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }, this.derivedTransparencyIndex);
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null });
}
else
{
this.quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
// Don't use transparency threshold for quantization PNG can handle multiple transparent colors.
this.quantizer = new WuQuantizer(new QuantizerOptions { TransparencyThreshold = 0, MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) });
}
}
// Create quantized frame returning the palette and set the bit depth.
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
if (image.Frames.Count > 1)
{
// Encoding animated frames with a global palette requires a transparent pixel in the palette
// since we only encode the delta between frames. To ensure that we have a transparent pixel
// we create a fake frame with a containing only transparent pixels and add it to the palette.
using Buffer2D<TPixel> px = image.Configuration.MemoryAllocator.Allocate2D<TPixel>(Math.Min(256, image.Width), Math.Min(256, image.Height));
TPixel backGroundPixel = backgroundColor.ToPixel<TPixel>();
for (int i = 0; i < px.Height; i++)
{
px.DangerousGetRowSpan(i).Fill(backGroundPixel);
}
frameQuantizer.AddPaletteColors(px.GetRegion());
}
frameQuantizer.BuildPalette(
this.configuration,
encoder.TransparentColorMode,
encoder.PixelSamplingStrategy,
image,
backgroundColor);
return frameQuantizer.QuantizeFrame(frame, bounds);
}

3
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -250,8 +250,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
=> this.ColorTable = null;
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

2
src/ImageSharp/Formats/TransparentColorMode.cs

@ -18,5 +18,5 @@ public enum TransparentColorMode
/// to fully transparent pixels (all components set to zero),
/// which may improve compression.
/// </summary>
Clear = 1,
Clear = 1
}

2
src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs

@ -32,7 +32,7 @@ internal abstract class BitReaderBase : IDisposable
/// <param name="memoryAllocator">Used for allocating memory during reading data from the stream.</param>
protected static IMemoryOwner<byte> ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator)
{
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead);
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean);
Span<byte> dataSpan = data.Memory.Span;
input.Read(dataSpan[..bytesToRead], 0, bytesToRead);

14
src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs

@ -67,14 +67,14 @@ internal class Vp8Decoder : IDisposable
int extraY = extraRows * this.CacheYStride;
int extraUv = extraRows / 2 * this.CacheUvStride;
this.YuvBuffer = memoryAllocator.Allocate<byte>((WebpConstants.Bps * 17) + (WebpConstants.Bps * 9) + extraY);
this.CacheY = memoryAllocator.Allocate<byte>((16 * this.CacheYStride) + extraY);
this.CacheY = memoryAllocator.Allocate<byte>((16 * this.CacheYStride) + extraY, AllocationOptions.Clean);
int cacheUvSize = (16 * this.CacheUvStride) + extraUv;
this.CacheU = memoryAllocator.Allocate<byte>(cacheUvSize);
this.CacheV = memoryAllocator.Allocate<byte>(cacheUvSize);
this.TmpYBuffer = memoryAllocator.Allocate<byte>((int)width);
this.TmpUBuffer = memoryAllocator.Allocate<byte>((int)width);
this.TmpVBuffer = memoryAllocator.Allocate<byte>((int)width);
this.Pixels = memoryAllocator.Allocate<byte>((int)(width * height * 4));
this.CacheU = memoryAllocator.Allocate<byte>(cacheUvSize, AllocationOptions.Clean);
this.CacheV = memoryAllocator.Allocate<byte>(cacheUvSize, AllocationOptions.Clean);
this.TmpYBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
this.TmpUBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
this.TmpVBuffer = memoryAllocator.Allocate<byte>((int)width, AllocationOptions.Clean);
this.Pixels = memoryAllocator.Allocate<byte>((int)(width * height * 4), AllocationOptions.Clean);
#if DEBUG
// Filling those buffers with 205, is only useful for debugging,

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

@ -81,16 +81,29 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="completeDataSize">The size of the image data in bytes.</param>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize)
public Image<TPixel> Decode<TPixel>(
BufferedReadStream stream,
WebpFeatures features,
uint width,
uint height,
uint completeDataSize)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel>? image = null;
ImageFrame<TPixel>? previousFrame = null;
WebpFrameData? prevFrameData = null;
this.metadata = new ImageMetadata();
this.webpMetadata = this.metadata.GetWebpMetadata();
this.webpMetadata.RepeatCount = features.AnimationLoopCount;
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? Color.Transparent
: features.AnimationBackgroundColor!.Value;
this.webpMetadata.BackgroundColor = backgroundColor;
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0;
int remainingBytes = (int)completeDataSize;
@ -101,10 +114,16 @@ internal class WebpAnimationDecoder : IDisposable
switch (chunkType)
{
case WebpChunkType.FrameData:
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? Color.FromPixel(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value;
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, backgroundColor);
uint dataSize = this.ReadFrame(
stream,
ref image,
ref previousFrame,
ref prevFrameData,
width,
height,
backgroundPixel);
remainingBytes -= (int)dataSize;
break;
case WebpChunkType.Xmp:
@ -132,10 +151,18 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="prevFrameData">The previous frame data.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="backgroundColor">The default background color of the canvas in.</param>
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor)
private uint ReadFrame<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ref WebpFrameData? prevFrameData,
uint width,
uint height,
TPixel backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
WebpFrameData frameData = WebpFrameData.Parse(stream);
@ -174,40 +201,51 @@ internal class WebpAnimationDecoder : IDisposable
break;
}
ImageFrame<TPixel>? currentFrame = null;
ImageFrame<TPixel> imageFrame;
ImageFrame<TPixel> currentFrame;
if (previousFrame is null)
{
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata);
SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData);
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor, this.metadata);
imageFrame = image.Frames.RootFrame;
currentFrame = image.Frames.RootFrame;
SetFrameMetadata(currentFrame.Metadata, frameData);
}
else
{
currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
// If the frame is a key frame we do not need to clone the frame or clear it.
bool isKeyFrame = prevFrameData?.DisposalMethod is FrameDisposalMode.RestoreToBackground
&& this.restoreArea == image!.Bounds;
SetFrameMetadata(currentFrame.Metadata, frameData);
if (isKeyFrame)
{
currentFrame = image!.Frames.CreateFrame(backgroundColor);
}
else
{
// This clones the frame and adds it the collection.
currentFrame = image!.Frames.AddFrame(previousFrame);
if (prevFrameData?.DisposalMethod is FrameDisposalMode.RestoreToBackground)
{
this.RestoreToBackground(currentFrame, backgroundColor);
}
}
imageFrame = currentFrame;
SetFrameMetadata(currentFrame.Metadata, frameData);
}
Rectangle regionRectangle = frameData.Bounds;
Rectangle interest = frameData.Bounds;
bool blend = previousFrame != null && frameData.BlendingMethod == FrameBlendMode.Over;
using Buffer2D<TPixel> pixelData = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
DrawDecodedImageFrameOnCanvas(pixelData, currentFrame, interest, blend);
webpInfo?.Dispose();
previousFrame = currentFrame;
prevFrameData = frameData;
if (frameData.DisposalMethod is FrameDisposalMode.RestoreToBackground)
{
this.RestoreToBackground(imageFrame, backgroundColor);
this.restoreArea = interest;
}
using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
bool blend = previousFrame != null && frameData.BlendingMethod == FrameBlendMode.Over;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle;
return (uint)(stream.Position - streamStartPosition);
}
@ -257,31 +295,26 @@ internal class WebpAnimationDecoder : IDisposable
try
{
Buffer2D<TPixel> pixelBufferDecoded = decodedFrame.PixelBuffer;
Buffer2D<TPixel> decodeBuffer = decodedFrame.PixelBuffer;
if (webpInfo.IsLossless)
{
WebpLosslessDecoder losslessDecoder =
new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(decodeBuffer, (int)webpInfo.Width, (int)webpInfo.Height);
}
else
{
WebpLossyDecoder lossyDecoder =
new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
lossyDecoder.Decode(decodeBuffer, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
}
return pixelBufferDecoded;
return decodeBuffer;
}
catch
{
decodedFrame?.Dispose();
throw;
}
finally
{
webpInfo.Dispose();
}
}
/// <summary>
@ -335,7 +368,7 @@ internal class WebpAnimationDecoder : IDisposable
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="imageFrame">The image frame.</param>
/// <param name="backgroundColor">Color of the background.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, Color backgroundColor)
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, TPixel backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.restoreArea.HasValue)
@ -345,8 +378,9 @@ internal class WebpAnimationDecoder : IDisposable
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
pixelRegion.Fill(backgroundPixel);
pixelRegion.Fill(backgroundColor);
this.restoreArea = null;
}
/// <inheritdoc/>

2
src/ImageSharp/Formats/Webp/WebpCommonUtils.cs

@ -18,7 +18,7 @@ internal static class WebpCommonUtils
/// </summary>
/// <param name="row">The row to check.</param>
/// <returns>Returns true if alpha has non-0xff values.</returns>
public static unsafe bool CheckNonOpaque(Span<Bgra32> row)
public static unsafe bool CheckNonOpaque(ReadOnlySpan<Bgra32> row)
{
if (Avx2.IsSupported)
{

14
src/ImageSharp/ImageSharp.csproj

@ -44,6 +44,11 @@
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp\sixlabors.imagesharp.128.png" Pack="true" PackagePath="" />
<None Include="..\..\SixLabors.ImageSharp.props" Pack="true" PackagePath="build" />
<None Include="Common\InlineArray.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>InlineArray.tt</DependentUpon>
</None>
</ItemGroup>
<ItemGroup>
@ -51,6 +56,11 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Common\InlineArray.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>InlineArray.tt</DependentUpon>
</Compile>
<Compile Update="Formats\_Generated\ImageMetadataExtensions.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -154,6 +164,10 @@
</ItemGroup>
<ItemGroup>
<None Update="Common\InlineArray.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>InlineArray.cs</LastGenOutput>
</None>
<None Update="Formats\_Generated\ImageMetadataExtensions.tt">
<LastGenOutput>ImageMetadataExtensions.cs</LastGenOutput>
<Generator>TextTemplatingFileGenerator</Generator>

4
src/ImageSharp/IndexedImageFrame{TPixel}.cs

@ -30,7 +30,7 @@ public sealed class IndexedImageFrame<TPixel> : IPixelSource, IDisposable
/// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param>
/// <param name="palette">The color palette.</param>
internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory<TPixel> palette)
public IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory<TPixel> palette)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette));
@ -42,7 +42,7 @@ public sealed class IndexedImageFrame<TPixel> : IPixelSource, IDisposable
this.Height = height;
this.pixelBuffer = configuration.MemoryAllocator.Allocate2D<byte>(width, height);
// Copy the palette over. We want the lifetime of this frame to be independant of any palette source.
// Copy the palette over. We want the lifetime of this frame to be independent of any palette source.
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(palette.Length);
palette.Span.CopyTo(this.paletteOwner.GetSpan());
this.Palette = this.paletteOwner.Memory[..palette.Length];

6
src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs

@ -132,16 +132,14 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
/// <param name="source">The source image. Cannot be null.</param>
/// <param name="destination">The cloned/destination image. Cannot be null.</param>
protected virtual void AfterFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
}
=> destination.Metadata.AfterFrameApply(source, destination);
/// <summary>
/// This method is called after the process is applied to prepare the processor.
/// </summary>
/// <param name="destination">The cloned/destination image. Cannot be null.</param>
protected virtual void AfterImageApply(Image<TPixel> destination)
{
}
=> destination.Metadata.AfterImageApply(destination);
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.

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

@ -21,7 +21,7 @@ public interface IDither
/// <param name="source">The source image.</param>
/// <param name="destination">The destination quantized frame.</param>
/// <param name="bounds">The region of interest bounds.</param>
void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
IndexedImageFrame<TPixel> destination,
@ -38,7 +38,7 @@ public interface IDither
/// <param name="processor">The palette dithering processor.</param>
/// <param name="source">The source image.</param>
/// <param name="bounds">The region of interest bounds.</param>
void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds)

8
src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs

@ -15,22 +15,22 @@ public interface IPaletteDitherImageProcessor<TPixel>
/// <summary>
/// Gets the configuration instance to use when performing operations.
/// </summary>
Configuration Configuration { get; }
public Configuration Configuration { get; }
/// <summary>
/// Gets the dithering palette.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
public ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
/// </summary>
float DitherScale { get; }
public float DitherScale { get; }
/// <summary>
/// Returns the color from the dithering palette corresponding to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <returns>The <typeparamref name="TPixel"/> match.</returns>
TPixel GetPaletteColor(TPixel color);
public TPixel GetPaletteColor(TPixel color);
}

7
src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs

@ -95,7 +95,7 @@ public abstract class ImageProcessor<TPixel> : IImageProcessor<TPixel>
protected abstract void OnFrameApply(ImageFrame<TPixel> source);
/// <summary>
/// This method is called after the process is applied to prepare the processor.
/// This method is called after the process is applied to each frame.
/// </summary>
/// <param name="source">The source image. Cannot be null.</param>
protected virtual void AfterFrameApply(ImageFrame<TPixel> source)
@ -103,11 +103,10 @@ public abstract class ImageProcessor<TPixel> : IImageProcessor<TPixel>
}
/// <summary>
/// This method is called after the process is applied to prepare the processor.
/// This method is called after the process is applied to the complete image.
/// </summary>
protected virtual void AfterImageApply()
{
}
=> this.Source.Metadata.AfterImageApply(this.Source);
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.

521
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
@ -21,13 +22,7 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private Rgba32[] rgbaPalette;
private int transparentIndex;
private readonly TPixel transparentMatch;
/// <summary>
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
/// </summary>
private ColorDistanceCache cache;
private readonly HybridColorDistanceCache cache;
private readonly Configuration configuration;
/// <summary>
@ -36,26 +31,12 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
: this(configuration, palette, -1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int transparentIndex = -1)
{
this.configuration = configuration;
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
this.cache = new HybridColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
this.transparentIndex = transparentIndex;
this.transparentMatch = TPixel.FromRgba32(default);
}
/// <summary>
@ -70,21 +51,27 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <param name="transparencyThreshold">The transparency threshold.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetClosestColor(TPixel color, out TPixel match, short transparencyThreshold = -1)
{
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
Rgba32 rgba = color.ToRgba32();
if (transparencyThreshold > -1 && rgba.A < transparencyThreshold)
{
rgba = default;
}
// Check if the color is in the lookup table
if (!this.cache.TryGetValue(rgba, out short index))
if (this.cache.TryGetValue(rgba, out short index))
{
return this.GetClosestColorSlow(rgba, ref paletteRef, out match);
match = Unsafe.Add(ref paletteRef, (ushort)index);
return index;
}
match = Unsafe.Add(ref paletteRef, (ushort)index);
return index;
return this.GetClosestColorSlow(rgba, ref paletteRef, out match);
}
/// <summary>
@ -96,46 +83,25 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette);
this.transparentIndex = -1;
this.cache.Clear();
}
/// <summary>
/// Allows setting the transparent index after construction.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index)
{
if (index != this.transparentIndex)
{
this.cache.Clear();
}
this.transparentIndex = index;
}
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.NoInlining)]
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
if (this.transparentIndex >= 0 && rgba == default)
{
// We have explicit instructions. No need to search.
index = this.transparentIndex;
this.cache.Add(rgba, (byte)index);
match = this.transparentMatch;
return index;
}
float leastDistance = float.MaxValue;
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Rgba32 candidate = this.rgbaPalette[i];
float distance = DistanceSquared(rgba, candidate);
if (candidate.PackedValue == rgba.PackedValue)
{
index = i;
break;
}
// If it's an exact match, exit the loop
float distance = DistanceSquared(rgba, candidate);
if (distance == 0)
{
index = i;
@ -144,7 +110,6 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
if (distance < leastDistance)
{
// Less than... assign.
index = i;
leastDistance = distance;
}
@ -153,6 +118,7 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
// Now I have the index, pop it into the cache for next time
this.cache.Add(rgba, (byte)index);
match = Unsafe.Add(ref paletteRef, (uint)index);
return index;
}
@ -162,96 +128,415 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// <param name="a">The first point.</param>
/// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float 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;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
Vector4 va = new(a.R, a.G, a.B, a.A);
Vector4 vb = new(b.R, b.G, b.B, b.A);
return Vector4.DistanceSquared(va, vb);
}
public void Dispose() => this.cache.Dispose();
/// <summary>
/// A cache for storing color distance matching results.
/// 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>
/// <para>
/// The granularity of the cache has been determined based upon the current
/// suite of test images and provides the lowest possible memory usage while
/// providing enough match accuracy.
/// Entry count is currently limited to 2335905 entries (4MB).
/// </para>
/// 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>
private unsafe struct ColorDistanceCache : IDisposable
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
// https://github.com/dotnet/roslyn-analyzers/issues/6151
private readonly unsafe struct HybridColorDistanceCache : IDisposable
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
{
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) + 1;
private const int IndexGCount = (1 << IndexGBits) + 1;
private const int IndexBCount = (1 << IndexBBits) + 1;
private const int IndexACount = (1 << IndexABits) + 1;
private const int RShift = 8 - IndexRBits;
private const int GShift = 8 - IndexGBits;
private const int BShift = 8 - IndexBBits;
private const int AShift = 8 - IndexABits;
private const int Entries = IndexRCount * IndexGCount * IndexBCount * IndexACount;
private MemoryHandle tableHandle;
private readonly IMemoryOwner<short> table;
private readonly short* tablePointer;
public ColorDistanceCache(MemoryAllocator allocator)
private readonly CoarseCache coarseCache;
private readonly ExactCache exactCache;
public HybridColorDistanceCache(MemoryAllocator allocator)
{
this.exactCache = new ExactCache(allocator);
this.coarseCache = new CoarseCache(allocator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Add(Rgba32 color, short index)
{
this.table = allocator.Allocate<short>(Entries);
this.table.GetSpan().Fill(-1);
this.tableHandle = this.table.Memory.Pin();
this.tablePointer = (short*)this.tableHandle.Pointer;
if (this.exactCache.TryAdd(color.PackedValue, index))
{
return;
}
this.coarseCache.Add(color, index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool TryGetValue(Rgba32 color, out short match)
{
if (this.exactCache.TryGetValue(color.PackedValue, out match))
{
return true; // Exact match found
}
if (this.coarseCache.TryGetValue(color, out match))
{
return true; // Coarse match found
}
match = -1;
return false;
}
public readonly void Clear()
{
this.exactCache.Clear();
this.coarseCache.Clear();
}
public void Dispose()
{
this.exactCache.Dispose();
this.coarseCache.Dispose();
}
}
/// <summary>
/// A fixed-capacity dictionary with exactly 512 entries mapping a <see cref="uint"/> key
/// to a <see cref="short"/> value.
/// </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 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.
/// This guarantees highly efficient and predictable performance for small, fixed-size color palettes.
/// </remarks>
internal sealed unsafe class ExactCache : IDisposable
{
// 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;
// Entries array: stores up to 256 entries.
private readonly IMemoryOwner<Entry> entriesOwner;
private MemoryHandle entriesHandle;
private Entry* entries;
public const int Capacity = 512;
public ExactCache(MemoryAllocator allocator)
{
this.Count = 0;
// Allocate exactly 512 ints 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;
}
[MethodImpl(InliningOptions.ShortMethod)]
public readonly void Add(Rgba32 rgba, byte index)
public int Count { get; private set; }
/// <summary>
/// Adds a key/value pair to the dictionary.
/// If the key already exists, the dictionary is left unchanged.
/// </summary>
/// <param name="key">The key to add.</param>
/// <param name="value">The value to add.</param>
/// <returns><see langword="true"/> if the key was added; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(uint key, short value)
{
int idx = GetPaletteIndex(rgba);
this.tablePointer[idx] = index;
if (this.Count == Capacity)
{
return false; // Dictionary is full.
}
// 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)
{
Entry e = entries[i];
if (e.Key == key)
{
// Key already exists; do not overwrite.
return false;
}
i = e.Next;
}
short index = (short)this.Count;
this.Count++;
// Insert the new entry:
entries[index].Key = key;
entries[index].Value = value;
// Link this new entry into the bucket chain.
entries[index].Next = this.buckets[bucket];
this.buckets[bucket] = index;
return true;
}
[MethodImpl(InliningOptions.ShortMethod)]
public readonly bool TryGetValue(Rgba32 rgba, out short match)
/// <summary>
/// Tries to retrieve the value associated with the specified key.
/// Returns true if the key is found; otherwise, returns false.
/// </summary>
/// <param name="key">The key to search for.</param>
/// <param name="value">The value associated with the key, if found.</param>
/// <returns><see langword="true"/> if the key is found; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(uint key, out short value)
{
int idx = GetPaletteIndex(rgba);
match = this.tablePointer[idx];
return match > -1;
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF);
int i = this.buckets[bucket];
// If the bucket is empty, return immediately.
if (i == -1)
{
value = -1;
return false;
}
// Traverse the chain.
Entry* entries = this.entries;
do
{
Entry e = entries[i];
if (e.Key == key)
{
value = e.Value;
return true;
}
i = e.Next;
}
while (i != -1);
value = -1;
return false;
}
/// <summary>
/// Clears the cache resetting each entry to empty.
/// Clears the dictionary.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly void Clear() => this.table.GetSpan().Fill(-1);
public void Clear()
{
Span<short> bucketSpan = this.bucketsOwner.GetSpan();
bucketSpan.Fill(-1);
this.Count = 0;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetPaletteIndex(Rgba32 rgba)
public void Dispose()
{
int rIndex = rgba.R >> RShift;
int gIndex = rgba.G >> GShift;
int bIndex = rgba.B >> BShift;
int aIndex = rgba.A >> AShift;
return (aIndex * (IndexRCount * IndexGCount * IndexBCount)) +
(rIndex * (IndexGCount * IndexBCount)) +
(gIndex * IndexBCount) + bIndex;
this.bucketsHandle.Dispose();
this.bucketsOwner.Dispose();
this.entriesHandle.Dispose();
this.entriesOwner.Dispose();
this.buckets = null;
this.entries = 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>
/// <para>
/// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values,
/// using a quantized representation of 5,5,5,6 (RGB: 5 bits each, Alpha: 6 bits).
/// </para>
/// <para>
/// The cache quantizes the RGB channels to 5 bits each, resulting in 32 levels per channel and a total of 32³ = 32,768 buckets.
/// Each bucket is represented by an <see cref="AlphaBucket"/>, which holds a small, inline array of alpha entries.
/// Each alpha entry stores the alpha value quantized to 6 bits (0–63) along with a palette index (a 16-bit value).
/// </para>
/// <para>
/// Performance Characteristics:
/// - Lookup: O(1) for computing the bucket index from the RGB channels, plus a small constant time (up to 4 iterations)
/// to search through the alpha entries in the bucket.
/// - Insertion: O(1) for bucket index computation and a quick linear search over a very small (fixed) number of entries.
/// </para>
/// <para>
/// Memory Characteristics:
/// - The cache consists of 32,768 buckets.
/// - Each <see cref="AlphaBucket"/> is implemented using an inline array with a capacity of 4 entries.
/// - Each bucket occupies approximately 18 bytes.
/// - Overall, the buckets occupy roughly 32,768 × 18 = 589,824 bytes (576 KB).
/// </para>
/// <para>
/// This design provides nearly constant-time lookup and insertion with minimal memory usage,
/// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries).
/// </para>
/// </summary>
internal sealed unsafe class CoarseCache : IDisposable
{
// Use 5 bits per channel for R, G, and B: 32 levels each.
// Total buckets = 32^3 = 32768.
private const int RgbBits = 5;
private const int BucketCount = 1 << (RgbBits * 3); // 32768
private readonly IMemoryOwner<AlphaBucket> bucketsOwner;
private readonly AlphaBucket* buckets;
private MemoryHandle bucketHandle;
public CoarseCache(MemoryAllocator allocator)
{
this.bucketsOwner = allocator.Allocate<AlphaBucket>(BucketCount, AllocationOptions.Clean);
this.bucketHandle = this.bucketsOwner.Memory.Pin();
this.buckets = (AlphaBucket*)this.bucketHandle.Pointer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetBucketIndex(byte r, byte g, byte b)
{
int qr = r >> (8 - RgbBits);
int qg = g >> (8 - RgbBits);
int qb = b >> (8 - RgbBits);
// Combine the quantized channels into a single index.
return (qr << (RgbBits * 2)) | (qg << RgbBits) | qb;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte QuantizeAlpha(byte a)
// Quantize to 6 bits: shift right by (8 - 6) = 2 bits.
=> (byte)(a >> 2);
public void Add(Rgba32 color, short paletteIndex)
{
int bucketIndex = GetBucketIndex(color.R, color.G, color.B);
byte quantAlpha = QuantizeAlpha(color.A);
this.buckets[bucketIndex].Add(quantAlpha, paletteIndex);
}
public void Dispose()
{
if (this.table != null)
this.bucketHandle.Dispose();
this.bucketsOwner.Dispose();
}
public bool TryGetValue(Rgba32 color, out short paletteIndex)
{
int bucketIndex = GetBucketIndex(color.R, color.G, color.B);
byte quantAlpha = QuantizeAlpha(color.A);
return this.buckets[bucketIndex].TryGetValue(quantAlpha, out paletteIndex);
}
public void Clear()
{
Span<AlphaBucket> bucketsSpan = this.bucketsOwner.GetSpan();
bucketsSpan.Clear();
}
public struct AlphaEntry
{
// Store the alpha value quantized to 6 bits (0..63)
public byte QuantizedAlpha;
public short PaletteIndex;
}
public struct AlphaBucket
{
// Fixed capacity for alpha entries in this bucket.
// We choose a capacity of 4 for several reasons:
//
// 1. The alpha channel is quantized to 6 bits, so there are 64 possible distinct values.
// In the worst-case, a given RGB bucket might encounter up to 64 different alpha values.
//
// 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 4 values out of 64,
// the probability that these 4 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
// of unique alpha values. Allocating 4 slots per bucket provides a good trade-off:
// it captures the common-case scenario while keeping overall memory usage low.
//
// 4. Even if more than 4 unique alpha values occur in a bucket,
// our design overwrites the first entry. This behavior gives us some "wriggle room"
// while preserving the most frequently encountered or most recent values.
public const int Capacity = 4;
public byte Count;
private InlineArray4<AlphaEntry> entries;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(byte quantizedAlpha, out short paletteIndex)
{
this.tableHandle.Dispose();
this.table.Dispose();
for (int i = 0; i < this.Count; i++)
{
ref AlphaEntry entry = ref this.entries[i];
if (entry.QuantizedAlpha == quantizedAlpha)
{
paletteIndex = entry.PaletteIndex;
return true;
}
}
paletteIndex = -1;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(byte quantizedAlpha, short paletteIndex)
{
// Check for an existing entry with the same quantized alpha.
for (int i = 0; i < this.Count; i++)
{
ref AlphaEntry entry = ref this.entries[i];
if (entry.QuantizedAlpha == quantizedAlpha)
{
// Update palette index if found.
entry.PaletteIndex = paletteIndex;
return;
}
}
// If there's room, add a new entry.
if (this.Count < Capacity)
{
ref AlphaEntry newEntry = ref this.entries[this.Count];
newEntry.QuantizedAlpha = quantizedAlpha;
newEntry.PaletteIndex = paletteIndex;
this.Count++;
}
else
{
// Bucket is full. Overwrite the first entry to give us some wriggle room.
this.entries[0].QuantizedAlpha = quantizedAlpha;
this.entries[0].PaletteIndex = paletteIndex;
}
}
}
}

12
src/ImageSharp/Processing/Processors/Quantization/IQuantizer{TPixel}.cs

@ -16,12 +16,12 @@ public interface IQuantizer<TPixel> : IDisposable
/// <summary>
/// Gets the configuration.
/// </summary>
Configuration Configuration { get; }
public Configuration Configuration { get; }
/// <summary>
/// Gets the quantizer options defining quantization rules.
/// </summary>
QuantizerOptions Options { get; }
public QuantizerOptions Options { get; }
/// <summary>
/// Gets the quantized color palette.
@ -29,13 +29,13 @@ public interface IQuantizer<TPixel> : IDisposable
/// <exception cref="InvalidOperationException">
/// The palette has not been built via <see cref="AddPaletteColors"/>.
/// </exception>
ReadOnlyMemory<TPixel> Palette { get; }
public ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Adds colors to the quantized palette from the given pixel source.
/// </summary>
/// <param name="pixelRegion">The <see cref="Buffer2DRegion{T}"/> of source pixels to register.</param>
void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion);
public void AddPaletteColors(in Buffer2DRegion<TPixel> pixelRegion);
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
@ -49,7 +49,7 @@ public interface IQuantizer<TPixel> : IDisposable
/// Only executes the second (quantization) step. The palette has to be built by calling <see cref="AddPaletteColors"/>.
/// To run both steps, use <see cref="QuantizerUtilities.BuildPaletteAndQuantizeFrame{TPixel}"/>.
/// </remarks>
IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds);
public IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Returns the index and color from the quantized palette corresponding to the given color.
@ -57,7 +57,7 @@ public interface IQuantizer<TPixel> : IDisposable
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns>
byte GetQuantizedColor(TPixel color, out TPixel match);
public byte GetQuantizedColor(TPixel color, out TPixel match);
// TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);

766
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs

File diff suppressed because it is too large

23
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
public class PaletteQuantizer : IQuantizer
{
private readonly ReadOnlyMemory<Color> colorPalette;
private readonly int transparentIndex;
private readonly int transparencyIndex;
private readonly Color transparentColor;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
@ -25,27 +26,33 @@ public class PaletteQuantizer : IQuantizer
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
/// <param name="palette">The color palette to use.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options)
: this(palette, options, -1)
: this(palette, options, -1, default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
/// <param name="palette">The color palette to use.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
internal PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options, int transparentIndex)
/// <param name="transparencyIndex">The index of the color in the palette that should be considered as transparent.</param>
/// <param name="transparentColor">The color that should be considered as transparent.</param>
internal PaletteQuantizer(
ReadOnlyMemory<Color> palette,
QuantizerOptions options,
int transparencyIndex,
Color transparentColor)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.colorPalette = palette;
this.Options = options;
this.transparentIndex = transparentIndex;
this.transparencyIndex = transparencyIndex;
this.transparentColor = transparentColor;
}
/// <inheritdoc />
@ -66,6 +73,6 @@ public class PaletteQuantizer : IQuantizer
// treat the buffer as FILO.
TPixel[] palette = new TPixel[Math.Min(options.MaxColors, this.colorPalette.Length)];
Color.ToPixel(this.colorPalette.Span[..palette.Length], palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette, this.transparentIndex);
return new PaletteQuantizer<TPixel>(configuration, options, palette, this.transparencyIndex, this.transparentColor.ToPixel<TPixel>());
}
}

55
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -17,10 +17,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
"Design",
"CA1001:Types that own disposable fields should be disposable",
Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")]
internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
internal struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
private int transparencyIndex;
private TPixel transparentColor;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
@ -28,20 +30,37 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
/// <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="palette">The palette to use.</param>
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> palette)
: this(configuration, options, palette, -1, default)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{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="palette">The palette to use.</param>
/// <param name="transparencyIndex">The index of the color in the palette that should be considered as transparent.</param>
/// <param name="transparentColor">The color that should be considered as transparent.</param>
public PaletteQuantizer(
Configuration configuration,
QuantizerOptions options,
ReadOnlyMemory<TPixel> palette,
int transparentIndex)
int transparencyIndex,
TPixel transparentColor)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, transparentIndex);
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
this.transparencyIndex = transparencyIndex;
this.transparentColor = transparentColor;
}
/// <inheritdoc/>
@ -51,7 +70,7 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
public readonly ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
@ -60,21 +79,29 @@ internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion)
public readonly void AddPaletteColors(in Buffer2DRegion<TPixel> pixelRegion)
{
}
/// <summary>
/// Allows setting the transparent index after construction.
/// </summary>
/// <param name="index">An explicit index at which to match transparent pixels.</param>
public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match);
{
if (this.transparencyIndex >= 0 && color.Equals(this.transparentColor))
{
match = this.transparentColor;
return (byte)this.transparencyIndex;
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
public void SetTransparencyIndex(int transparencyIndex, TPixel transparentColor)
{
this.transparencyIndex = transparencyIndex;
this.transparentColor = transparentColor;
}
/// <inheritdoc/>
public void Dispose() => this.pixelMap.Dispose();
public readonly void Dispose() => this.pixelMap.Dispose();
}

19
src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs

@ -21,15 +21,30 @@ public static class QuantizerConstants
public const int MaxColors = 256;
/// <summary>
/// The minumim dithering scale used to adjust the amount of dither.
/// The minimum dithering scale used to adjust the amount of dither.
/// </summary>
public const float MinDitherScale = 0;
/// <summary>
/// The max dithering scale used to adjust the amount of dither.
/// The maximum dithering scale used to adjust the amount of dither.
/// </summary>
public const float MaxDitherScale = 1F;
/// <summary>
/// The default threshold at which to consider a pixel transparent.
/// </summary>
public const float DefaultTransparencyThreshold = 64 / 255F;
/// <summary>
/// The minimum threshold at which to consider a pixel transparent.
/// </summary>
public const float MinTransparencyThreshold = 0F;
/// <summary>
/// The maximum threshold at which to consider a pixel transparent.
/// </summary>
public const float MaxTransparencyThreshold = 1F;
/// <summary>
/// Gets the default dithering algorithm to use.
/// </summary>

11
src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs

@ -12,6 +12,7 @@ public class QuantizerOptions
{
private float ditherScale = QuantizerConstants.MaxDitherScale;
private int maxColors = QuantizerConstants.MaxColors;
private float threshold = QuantizerConstants.DefaultTransparencyThreshold;
/// <summary>
/// Gets or sets the algorithm to apply to the output image.
@ -38,4 +39,14 @@ public class QuantizerOptions
get => this.maxColors;
set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
/// <summary>
/// Gets or sets the threshold at which to consider a pixel transparent. Range 0..1.
/// Defaults to <see cref="QuantizerConstants.DefaultTransparencyThreshold"/>.
/// </summary>
public float TransparencyThreshold
{
get => this.threshold;
set => this.threshold = Numerics.Clamp(value, QuantizerConstants.MinTransparencyThreshold, QuantizerConstants.MaxTransparencyThreshold);
}
}

38
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -112,7 +112,12 @@ public static class QuantizerUtilities
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source);
=> quantizer.BuildPalette(
source.Configuration,
TransparentColorMode.Preserve,
pixelSamplingStrategy,
source,
Color.Transparent);
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
@ -123,27 +128,33 @@ public static class QuantizerUtilities
/// <param name="mode">The transparent color mode.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image to sample from.</param>
/// <param name="backgroundColor">The background color to use when clearing transparent pixels.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
Configuration configuration,
TransparentColorMode mode,
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source)
Image<TPixel> source,
Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
// We need to clone the region to ensure we don't alter the original image.
using Buffer2D<TPixel> clone = region.Buffer.CloneRegion(configuration, region.Rectangle);
quantizer.AddPaletteColors(clone.GetRegion());
Buffer2DRegion<TPixel> clonedRegion = clone.GetRegion();
EncodingUtilities.ClearTransparentPixels(configuration, in clonedRegion, backgroundColor);
quantizer.AddPaletteColors(in clonedRegion);
}
}
else
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
quantizer.AddPaletteColors(in region);
}
}
}
@ -160,7 +171,12 @@ public static class QuantizerUtilities
IPixelSamplingStrategy pixelSamplingStrategy,
ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source);
=> quantizer.BuildPalette(
source.Configuration,
TransparentColorMode.Preserve,
pixelSamplingStrategy,
source,
Color.Transparent);
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
@ -171,27 +187,33 @@ public static class QuantizerUtilities
/// <param name="mode">The transparent color mode.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image frame to sample from.</param>
/// <param name="backgroundColor">The background color to use when clearing transparent pixels.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
Configuration configuration,
TransparentColorMode mode,
IPixelSamplingStrategy pixelSamplingStrategy,
ImageFrame<TPixel> source)
ImageFrame<TPixel> source,
Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
// We need to clone the region to ensure we don't alter the original image.
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
using Buffer2D<TPixel> clone = region.Buffer.CloneRegion(configuration, region.Rectangle);
quantizer.AddPaletteColors(clone.GetRegion());
Buffer2DRegion<TPixel> clonedRegion = clone.GetRegion();
EncodingUtilities.ClearTransparentPixels(configuration, in clonedRegion, backgroundColor);
quantizer.AddPaletteColors(in clonedRegion);
}
}
else
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
quantizer.AddPaletteColors(in region);
}
}
}

71
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs

@ -74,6 +74,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
private readonly IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private int maxColors;
private short transparencyThreshold;
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel>? pixelMap;
private readonly bool isDithering;
@ -102,6 +103,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
this.pixelMap = default;
this.palette = default;
this.isDithering = this.isDithering = this.Options.Dither is not null;
this.transparencyThreshold = (short)(this.Options.TransparencyThreshold * 255);
}
/// <inheritdoc/>
@ -111,57 +113,57 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
public QuantizerOptions Options { get; }
/// <inheritdoc/>
public readonly ReadOnlyMemory<TPixel> Palette
public ReadOnlyMemory<TPixel> Palette
{
get
{
QuantizerUtilities.CheckPaletteState(in this.palette);
if (this.palette.IsEmpty)
{
this.ResolvePalette();
QuantizerUtilities.CheckPaletteState(in this.palette);
}
return this.palette;
}
}
/// <inheritdoc/>
public void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion)
public readonly void AddPaletteColors(in Buffer2DRegion<TPixel> pixelRegion)
=> this.Build3DHistogram(pixelRegion);
/// <summary>
/// Once all histogram data has been accumulated, this method computes the moments,
/// splits the color cube, and resolves the final palette from the accumulated histogram.
/// </summary>
private void ResolvePalette()
{
// TODO: Something is destroying the existing palette when adding new colors.
// When the QuantizingImageEncoder.PixelSamplingStrategy is DefaultPixelSamplingStrategy
// this leads to performance issues + the palette is not preserved.
// https://github.com/SixLabors/ImageSharp/issues/2498
this.Build3DHistogram(pixelRegion);
// Calculate the cumulative moments from the accumulated histogram.
this.Get3DMoments(this.memoryAllocator);
// Partition the histogram into color cubes.
this.BuildCube();
// Slice again since maxColors has been updated since the buffer was created.
// Compute the palette colors from the resolved cubes.
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan()[..this.maxColors];
ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
for (int k = 0; k < paletteSpan.Length; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
Moment moment = Volume(ref this.colorCube[k], momentsSpan);
if (moment.Weight > 0)
{
paletteSpan[k] = TPixel.FromScaledVector4(moment.Normalize());
}
}
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory[..paletteSpan.Length];
if (this.isDithering)
// Update the palette to the new computed colors.
this.palette = this.paletteOwner.Memory[..paletteSpan.Length];
// Create the pixel map if dithering is enabled.
if (this.isDithering && this.pixelMap is null)
{
// When called multiple times by QuantizerUtilities.BuildPalette
// this prevents memory churn caused by reallocation.
if (this.pixelMap is null)
{
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
}
else
{
this.pixelMap.Clear(result);
}
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette);
}
this.palette = result;
}
/// <inheritdoc/>
@ -172,12 +174,19 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// <inheritdoc/>
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 color cube might not match the correct color.
// In this case, we must use the pixel map to get the closest color.
if (this.isDithering)
{
return (byte)this.pixelMap!.GetClosestColor(color, out match);
return (byte)this.pixelMap!.GetClosestColor(color, out match, this.transparencyThreshold);
}
Rgba32 rgba = color.ToRgba32();
if (rgba.A < this.transparencyThreshold)
{
rgba = default;
}
const int shift = 8 - IndexBits;
int r = rgba.R >> shift;
@ -188,7 +197,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
ReadOnlySpan<byte> tagSpan = this.tagsOwner.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span);
match = Unsafe.Add(ref paletteRef, index);
match = Unsafe.Add(ref paletteRef, (nuint)index);
return index;
}
@ -360,7 +369,7 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>.
/// </summary>
/// <param name="source">The source pixel data.</param>
private readonly void Build3DHistogram(Buffer2DRegion<TPixel> source)
private readonly void Build3DHistogram(in Buffer2DRegion<TPixel> source)
{
Span<Moment> momentSpan = this.momentsOwner.GetSpan();
@ -368,6 +377,8 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(source.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
float transparencyThreshold = this.Options.TransparencyThreshold * 255;
for (int y = 0; y < source.Height; y++)
{
Span<TPixel> row = source.DangerousGetRowSpan(y);
@ -376,6 +387,10 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
for (int x = 0; x < bufferSpan.Length; x++)
{
Rgba32 rgba = bufferSpan[x];
if (rgba.A < transparencyThreshold)
{
rgba = default;
}
int r = (rgba.R >> (8 - IndexBits)) + 1;
int g = (rgba.G >> (8 - IndexBits)) + 1;

15
src/ImageSharp/Processing/Processors/Transforms/TransformProcessor{TPixel}.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.PixelFormats;
// TODO: DO we need this class?
namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
/// <summary>
@ -22,18 +23,4 @@ internal abstract class TransformProcessor<TPixel> : CloningImageProcessor<TPixe
: base(configuration, source, sourceRectangle)
{
}
/// <inheritdoc/>
protected override void AfterFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
base.AfterFrameApply(source, destination);
destination.Metadata.AfterFrameApply(source, destination);
}
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> destination)
{
base.AfterImageApply(destination);
destination.Metadata.AfterImageApply(destination);
}
}

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

@ -419,4 +419,15 @@ public class GifEncoderTests
}
});
}
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32)]
public void GifEncoder_CanDecode_Issue2866<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
// image.DebugSaveMultiFrame(provider);
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated");
}
}

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

@ -442,11 +442,12 @@ public partial class PngEncoderTests
}
[Theory]
[WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32)]
public void Encode_AnimatedFormatTransform_FromGif<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.613F)]
[WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32, 1.06F)]
public void Encode_AnimatedFormatTransform_FromGif<TPixel>(TestImageProvider<TPixel> provider, float percentage)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.RunsOnCI && !TestEnvironment.IsWindows)
if (TestEnvironment.RunsOnCI)
{
return;
}
@ -457,12 +458,14 @@ public partial class PngEncoderTests
image.Save(memStream, PngEncoder);
memStream.Position = 0;
image.DebugSave(provider: provider, extension: "png", encoder: PngEncoder);
using Image<TPixel> output = Image.Load<TPixel>(memStream);
// TODO: Find a better way to compare.
// The image has been visually checked but the quantization pattern used in the png encoder
// means we cannot use an exact comparison nor replicate using the quantizing processor.
ImageComparer.TolerantPercentage(0.613f).VerifySimilarity(output, image);
// The image has been visually checked but the coarse cache used by the palette quantizer
// can lead to minor differences between frames.
ImageComparer.TolerantPercentage(percentage).VerifySimilarity(output, image);
GifMetadata gif = image.Metadata.GetGifMetadata();
PngMetadata png = output.Metadata.GetPngMetadata();
@ -641,7 +644,7 @@ public partial class PngEncoderTests
encoded.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
// https://github.com/SixLabors/ImageSharp/issues/2469
// https://github.com/SixLabors/ImageSharp/issues/2668
[Theory]
[WithFile(TestImages.Png.Issue2668, PixelTypes.Rgba32)]
public void Issue2668_Quantized_Encode_Alpha<TPixel>(TestImageProvider<TPixel> provider)
@ -657,6 +660,35 @@ public partial class PngEncoderTests
encoded.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
[Fact]
public void Issue_2862()
{
// Create a grayscale palette (or any other palette with colors that are very close to each other):
Rgba32[] palette = [.. Enumerable.Range(0, 256).Select(i => new Rgba32((byte)i, (byte)i, (byte)i))];
using Image<Rgba32> image = new(254, 4);
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
image[x, y] = palette[x];
}
}
using MemoryStream ms = new();
image.Save(ms, new PngEncoder
{
ColorType = PngColorType.Palette,
BitDepth = PngBitDepth.Bit8,
Quantizer = new PaletteQuantizer(palette.Select(Color.FromPixel).ToArray())
});
ms.Position = 0;
using Image<Rgba32> encoded = Image.Load<Rgba32>(ms);
ImageComparer.Exact.VerifySimilarity(image, encoded);
}
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,

4
tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs

@ -106,7 +106,7 @@ public class WebpCommonUtilsTests
174, 183, 189, 255,
148, 158, 158, 255,
};
Span<Bgra32> row = MemoryMarshal.Cast<byte, Bgra32>(rowBytes);
ReadOnlySpan<Bgra32> row = MemoryMarshal.Cast<byte, Bgra32>(rowBytes);
bool noneOpaque;
for (int length = 8; length < row.Length; length += 8)
@ -188,7 +188,7 @@ public class WebpCommonUtilsTests
174, 183, 189, 255,
148, 158, 158, 255,
};
Span<Bgra32> row = MemoryMarshal.Cast<byte, Bgra32>(rowBytes);
ReadOnlySpan<Bgra32> row = MemoryMarshal.Cast<byte, Bgra32>(rowBytes);
bool noneOpaque;
for (int length = 8; length < row.Length; length += 8)

16
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -450,6 +450,22 @@ public class WebpDecoderTests
image.CompareToOriginal(provider, ReferenceDecoder);
}
// https://github.com/SixLabors/ImageSharp/issues/2866
[Theory]
[WithFile(Lossy.Issue2866, PixelTypes.Rgba32)]
public void WebpDecoder_CanDecode_Issue2866<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Web
using Image<TPixel> image = provider.GetImage(
WebpDecoder.Instance,
new WebpDecoderOptions() { BackgroundColorHandling = BackgroundColorHandling.Ignore });
// We can't use the reference decoder here.
// It creates frames of different size without blending the frames.
image.DebugSave(provider, extension: "webp", encoder: new WebpEncoder());
}
[Theory]
[WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)]
public void WebpDecoder_ThrowImageFormatException_OnInvalidImages<TPixel>(TestImageProvider<TPixel> provider)

2
tests/ImageSharp.Tests/Image/ImageFrameTests.cs

@ -119,7 +119,7 @@ public class ImageFrameTests
}
else
{
Span<La16> destination = MemoryMarshal.Cast<byte, La16>(actual);
Span<La16> destination = MemoryMarshal.Cast<byte, La16>(actual.AsSpan());
image.Frames.RootFrame.CopyPixelDataTo(destination);
}

2
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -197,7 +197,7 @@ public partial class ImageTests
}
else
{
Span<La16> destination = MemoryMarshal.Cast<byte, La16>(actual);
Span<La16> destination = MemoryMarshal.Cast<byte, La16>(actual.AsSpan());
image.CopyPixelDataTo(destination);
}

4
tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

@ -79,7 +79,7 @@ public class WuQuantizerTests
}
Configuration config = Configuration.Default;
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null, TransparencyThreshold = 0 });
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
@ -152,7 +152,7 @@ public class WuQuantizerTests
}
Configuration config = Configuration.Default;
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null, TransparencyThreshold = 0 });
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config))

2
tests/ImageSharp.Tests/TestImages.cs

@ -536,6 +536,7 @@ public static class TestImages
public const string Issue2450_B = "Gif/issues/issue_2450_2.gif";
public const string Issue2198 = "Gif/issues/issue_2198.gif";
public const string Issue2758 = "Gif/issues/issue_2758.gif";
public const string Issue2866 = "Gif/issues/issue_2866.gif";
public const string Issue2859_A = "Gif/issues/issue_2859_A.gif";
public const string Issue2859_B = "Gif/issues/issue_2859_B.gif";
}
@ -830,6 +831,7 @@ public static class TestImages
public const string Issue2670 = "Webp/issues/Issue2670.webp";
public const string Issue2763 = "Webp/issues/Issue2763.png";
public const string Issue2801 = "Webp/issues/Issue2801.webp";
public const string Issue2866 = "Webp/issues/Issue2866.webp";
}
}

36
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -64,20 +64,27 @@ public class MagickReferenceDecoder : ImageDecoder
settings.SetDefines(pngReadDefines);
using MagickImageCollection magickImageCollection = new(stream, settings);
int imageWidth = magickImageCollection.Max(x => x.Width);
int imageHeight = magickImageCollection.Max(x => x.Height);
List<ImageFrame<TPixel>> framesList = [];
foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
{
ImageFrame<TPixel> frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height);
ImageFrame<TPixel> frame = new(configuration, imageWidth, imageHeight);
framesList.Add(frame);
MemoryGroup<TPixel> framePixels = frame.PixelBuffer.FastMemoryGroup;
Buffer2DRegion<TPixel> buffer = frame.PixelBuffer.GetRegion(
imageWidth - magicFrame.Width,
imageHeight - magicFrame.Height,
magicFrame.Width,
magicFrame.Height);
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1)
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
FromRgba32Bytes(configuration, data, framePixels);
FromRgba32Bytes(configuration, data, buffer);
}
else if (magicFrame.Depth is 16 or 14)
{
@ -88,7 +95,7 @@ public class MagickReferenceDecoder : ImageDecoder
ushort[] data = pixels.ToShortArray(PixelMapping.RGBA);
Span<byte> bytes = MemoryMarshal.Cast<ushort, byte>(data.AsSpan());
FromRgba64Bytes(configuration, bytes, framePixels);
FromRgba64Bytes(configuration, bytes, buffer);
}
else
{
@ -111,33 +118,40 @@ public class MagickReferenceDecoder : ImageDecoder
PixelType = metadata.GetDecodedPixelTypeInfo()
};
}
private static void FromRgba32Bytes<TPixel>(Configuration configuration, Span<byte> rgbaBytes, IMemoryGroup<TPixel> destinationGroup)
private static void FromRgba32Bytes<TPixel>(
Configuration configuration,
Span<byte> rgbaBytes,
Buffer2DRegion<TPixel> destinationGroup)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Span<Rgba32> sourcePixels = MemoryMarshal.Cast<byte, Rgba32>(rgbaBytes);
foreach (Memory<TPixel> m in destinationGroup)
for (int y = 0; y < destinationGroup.Height; y++)
{
Span<TPixel> destBuffer = m.Span;
Span<TPixel> destBuffer = destinationGroup.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgba32(
configuration,
sourcePixels[..destBuffer.Length],
destBuffer);
sourcePixels = sourcePixels[destBuffer.Length..];
}
}
private static void FromRgba64Bytes<TPixel>(Configuration configuration, Span<byte> rgbaBytes, IMemoryGroup<TPixel> destinationGroup)
private static void FromRgba64Bytes<TPixel>(
Configuration configuration,
Span<byte> rgbaBytes,
Buffer2DRegion<TPixel> destinationGroup)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
foreach (Memory<TPixel> m in destinationGroup)
for (int y = 0; y < destinationGroup.Height; y++)
{
Span<TPixel> destBuffer = m.Span;
Span<TPixel> destBuffer = destinationGroup.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgba64Bytes(
configuration,
rgbaBytes,
destBuffer,
destBuffer.Length);
rgbaBytes = rgbaBytes[(destBuffer.Length * 8)..];
}
}

2
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d
oid sha256:9d7441b03c24acb887b2d9a6e2346bb23e2d38293c3df3ff489d48593f87b29a
size 9270

2
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp

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

4
tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:681b0e36298cb702683fb9ffb2a82f7dfd9080b268db19a03f413809f69d0e07
size 273269
oid sha256:596472e74050d968479b672c1d2436b179e41a7b99fcefb53286ad47e5a4fe13
size 273115

4
tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a899a84c6af24bfad89f9fde75957c7a979d65bcf096ab667cb976efd71cb560
size 271171
oid sha256:a025a094c285cea8510430b0e3657bc59231b38132781e298d60a74238879912
size 270699

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38597c6144d61960d25c74d7a465b1cdf69b7c0804a6dec68128a6c953258313
size 52688
oid sha256:2465dde9a5d6202194f7af3924ca24ab3151948d551549a711977d3302dbc0a3
size 51158

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f9191c71eea1f73aa4c55397ca26f240615c9c4a7fff9a05e6f2e046b5e4d8b
size 62323
oid sha256:82dcdd4f28a9ffafd36a21d06aee8adb49017df2d4abeee4205d65b1ae3df35e
size 59875

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b63810145832db459bb7a6b37a028a7b778f6b6b4e6eae00e50e6e21c5a06086
size 62199
oid sha256:a836c8efd7aa9818cf807cf56412e78399a6568798be23d0f3f6b89552856ff1
size 62172

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a67c14ef99a943706f050ff1ea0ef101429292d52bc14ed4610f8338736ff87e
size 56800
oid sha256:78900d779181140a02a2b9fb9fa922ca854d9905c1dc7e006592a3fdc00f8dee
size 58107

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:623dd82d372ba517b0d3357d06cffaf105d407a9090cbcbc6a76ae944ab33d67
size 59468
oid sha256:76f10d4280258d2941d85e795cf788977ca1e85bdc1b75b5a482b5bbdaa49d32
size 57900

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8edceef8e12c4f3d194523437045c5cf4e80c7bb95ff75f38c1f38a21872e3d0
size 59376
oid sha256:aba9172bb4d117ba1b0c5f32b46251d473cc06b3f697e5729da0c5768a70b5d2
size 59104

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b1d7019e8cb170ae67496f8250446c4f6b6217378658408c3d51a95c49a4c3bc
size 63287
oid sha256:e7d6ea824ba19632afa940b3062632d305bf3521b1795d46f3fea90abc1f0ed8
size 64431

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d7c03ede7ab3bd4e57e6a63e53e2e8c771e938fdc7d5dfe5c9339a2c9907c9cf
size 55550
oid sha256:3efcf6f924d3d07cad9dbf9dddb6104c3748ac4354298acf5afde66c2321e819
size 55358

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79b690b91223d1fe7ddf1b8826b4474b89644822bc8aa9adee3cf819bc095b4c
size 60979
oid sha256:2b9f295f6b539fbeeae3c473907fa450f9b8c94017abad4bf915a8a4a2e7b612
size 56982

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e22401dddf6552cd91517c1cdd142d3b9a66a7ad5c80d2e52ae07a7f583708e
size 57657
oid sha256:c4c45632b6cd387c929a9e0982f3943a7c3f64f27862c0b539bbf71228561f39
size 57886

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:819a0ce38e27e2adfa454d8c5ad5b24e818bf8954c9f2406f608dcecf506c2c4
size 59838
oid sha256:bf9e8bd50b62ba62ab04a5ab2af207414183a015567080fa7cdd827016694369
size 60458

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:007ac609ec61b39c7bdd04bc87a698f5cdc76eadd834c1457f41eb9c135c3f7b
size 60688
oid sha256:89864a77216b51cc5b9415453ade7f7ec64c1c112546aa47ee6b4b89f9b258a3
size 60543

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46892c07e9a93f1df71f0e38b331a437fb9b7c52d8f40cf62780cb6bd35d3b13
size 58963
oid sha256:0c03c3dc0b3da69ef4f55b5ad6d162da94ad46f4e426e318695bedc7e5bb3dfd
size 58725

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1b83345ca3de8d1fc0fbb5d8e68329b94ad79fc29b9f10a1392a97ffe9a0733e
size 58985
oid sha256:af86b108639f833972958fd2cc7d00221982069c40cab67b5bc6b8ce1a7e826d
size 59137

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c775a5b19ba09e1b335389e0dc12cb0c3feaff6072e904da750a676fcd6b07dc
size 59202
oid sha256:959b49f5498e4018bfb8a5fac8a688c51b06161dc0c6559547293c613ddca760
size 59248

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c88740c0553829eaa42ca751b34cc456623a84ccdff4020949a06ef4b4802d1
size 61137
oid sha256:fdc28c281666e381c7ba2483d033f73c88111f13eec10cc406e07730eb5fa709
size 60804

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a4a404b0767faac952435f768867cf7bf053848e1e3ef121624f136658a107c
size 58386
oid sha256:c80f215d4a839fb1ca722d03923b587bac6326d54d2d7a3656667e46464b4307
size 58011

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8cc216ed952216d203836dc559234216614f1ed059651677cc0ea714010bd932
size 58855
oid sha256:38fbfc201e8ef31b879e863f7f49ac1e731c4d7dfca58a80e1e45890565af979
size 58742

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7
size 871
oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc
size 875

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb3e3b9b3001e76505fb0e2db7ad200cad2a016c06f1993c60c3cab42c134863
size 867
oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc
size 875

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7
size 871
oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc
size 875

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7
size 871
oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc
size 875

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca70bb0200776efd00c4ef7596d4e1f2f5fbc68e447b395b25ef2b3c732e5156
size 44189
oid sha256:1de82d05feed0b3bd9d6d7d16507ff5dc06744843abaaf77fd4207edd5205488
size 44246

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8474b847b7d4a8f3e5c9793ca257ce46efcf49c473c731a9ca9c759851410b94
size 43066
oid sha256:ea34b188ce71a8fbd76fddf052fc1322fff62ba0acc218582b996d9b00c81671
size 42667

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20e80e7d9e68fd85bfbc63c61953327354b0634000ec142e01a42618995fd14c
size 44391
oid sha256:c179834a368c8fa4bb3e1a1fb2e12b567d7034c5a8e52741bc33ffa30ea73c8a
size 44251

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8af98bfcc5edef3f3ff33ee8f76f33ce2906a6677167e2b29e1dbe63b00a78d8
size 44202
oid sha256:a049a50155bf56c53a1b74e919806cbb83716842b5c0a233c44c87b3630115e0
size 44394

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b149ebbd550808ae46ff05b5ddcdb1fc0eb6ae0eacbe048e9a1ff24368d8f64d
size 45003
oid sha256:286314ca90912de65427d51269a3263ea58b3c32f2839f797f2689b7dac0c6ff
size 44953

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9316cbbcb137ae6ff31646f6a5ba1d0aec100db4512509f7684187e74d16a111
size 51074
oid sha256:76a3abb7c908e365abd8fc5b1fdc7536a71645a5fd59be61e200707e208fb341
size 51241

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08c39a43993deadebab21f1d3504027b5910a52adc437c167d77d62e5f5db46e
size 52762
oid sha256:968bba323acfabd9b1b02001e5b37047f6ab7fb7dae8c781eed2f84771beb9c9
size 52812

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c9c47fa755d603f8c148011511ee91f32444e0d94367f9db57593e3bf30f2e0
size 51808
oid sha256:146039cba79c21408296e77e2aef33ccc3bc952283011ee4b441451512b2a634
size 51680

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d2289ed4fa0c679f0f120d260fec8ab40b1599043cc0a1fbebc6b67e238ff87
size 51428
oid sha256:6937822a02885fc236f5520e947081883d1ccbdb3da04821d0da133e1f98d98e
size 51009

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:366e84ab8587735455798651096d2af5f965fc325f4852dc68356e94600598b1
size 52176
oid sha256:8c3f249cb608697afabe92a91f571a1a990424a212b92a9c2241e7ef9a173734
size 52022

4
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f8c6d416f09671777934e57bc67fb52ccc97145dc6f1869e628d9ffd7d8f6e7
size 119
oid sha256:9ab8374e77865606a2426e3d22628f717914472431de1d9d8ee9690d319850a0
size 118

4
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70
size 142244
oid sha256:ff67035f78690321c29a4e15c8de7c55bcb3260d667dbd9bced15de6b626fca1
size 148499

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:c5953aaa4569e97d1bf690e4429ae6684a4131521347cb8bf1f607d773018ee6
size 939085

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f934af128b85b9e8f557d71ac8b1f1473a0922d0754fc0c4ece0d0e3d8d94c39
size 7702
oid sha256:215b86efdfb603ad851a7f4b5830e0ff82fb49e7729fcfd0853a6c066b21507e
size 8235

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a51d04953c1c82d99884af62912d2271108c6bc62f18d4b32d0b5290c01fa7f7
size 247462
oid sha256:33f86d176382805fe60cc7cf8057583a8451f802982ebcd337bda8dbf69efd6a
size 248753

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

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

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:34eaa0696da00838e591b2c48e7797641521f7f3feb01abbd774591c4dd6f200
size 265546
oid sha256:24f738baad4417c2eedddf36064974ccd5ff9e1d1ac23e4f6c859c4fa789a447
size 266819

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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e
size 216246

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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e
size 216246

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:7efb8263a067de2f4368a43416049d13619a69761584867ec89867ed8b366c5e
size 226887

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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853
size 220689

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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853
size 220689

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:96fceb13a0ec386959e5bdad17e3e2896f43dc86c02abf0b88f882c898523563
size 230800

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:14b8be6579cea0742be6ab1d8a44b7fc7f7acc26698692dbe445435f1fa2e48a
size 262707

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:457a0b4e27a09440ff4e13792b68fb5a9da82b7ce6129ea15a5ea8dcd99bd522
size 274300
oid sha256:0b4ffa39ea41480b02ac183dcb28617278a46bcaef0a30af62fe17167f009bbd
size 274683

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f414473561bfa792c2e6342ff5e5dddffbdec5286932781b11a093803593b52a
size 313787
oid sha256:d10d2efb6d711bbff03a803785f7269ddc9f5ba9417597e60804f2476ad72af2
size 315621

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

Loading…
Cancel
Save