From ac1905328d37213c92fcf57d32417f9322294fe1 Mon Sep 17 00:00:00 2001 From: Andreas Eriksson <4438107+andreas-eriksson@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:51:36 +0200 Subject: [PATCH 1/9] Fix Identify returning incorrect frame count for animated PNGs The Identify method had two bugs when processing fdAT (FrameData) chunks: 1. A spurious Skip(4) before SkipChunkDataAndCrc caused the stream to be misaligned by 4 bytes, since chunk.Length already includes the 4-byte sequence number. 2. Unlike Decode, which consumes all fdAT chunks for a frame in one shot via ReadScanlines + ReadNextFrameDataChunk, Identify processed them individually, calling InitializeFrameMetadata for each chunk and inflating the frame count. The fix removes the extra Skip(4) and adds SkipRemainingFrameDataChunks to consume all continuation fdAT chunks for a frame, mirroring how ReadNextFrameDataChunk works during decoding. --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 30 +++++++++++++++++-- .../Formats/Png/PngDecoderTests.cs | 12 ++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../animated/issue-animated-frame-count.png | 3 ++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Png/animated/issue-animated-frame-count.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8962182679..52858ec129 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -428,9 +428,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); - // Skip sequence number - this.currentStream.Skip(4); + // Skip data for this and all remaining FrameData chunks belonging to the same frame + // (comparable to how Decode consumes them via ReadScanlines + ReadNextFrameDataChunk). this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); break; case PngChunkType.Data: @@ -2093,6 +2094,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore return 0; } + /// + /// Skips any remaining chunks belonging to the current frame. + /// This mirrors how is used during decoding: + /// consecutive fdAT chunks are consumed until a non-fdAT chunk is encountered, + /// which is stored in for the next iteration. + /// + /// Temporary buffer. + private void SkipRemainingFrameDataChunks(Span buffer) + { + while (this.TryReadChunk(buffer, out PngChunk chunk)) + { + if (chunk.Type is PngChunkType.FrameData) + { + chunk.Data?.Dispose(); + this.SkipChunkDataAndCrc(chunk); + } + else + { + // Not a FrameData chunk; store it so the next TryReadChunk call returns it. + this.nextChunk = chunk; + return; + } + } + } + /// /// Reads a chunk from the stream. /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index a58101a6bd..69e656849b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -411,6 +411,18 @@ public partial class PngDecoderTests Assert.Equal(expectedPixelSize, imageInfo.PixelType.BitsPerPixel); } + [Fact] + public void Identify_AnimatedPng_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(50, imageInfo.FrameMetadataCollection.Count); + } + [Theory] [WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)] public void Decode_MissingDataChunk_ThrowsException(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fab1b2891c..730e62d824 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -76,6 +76,7 @@ public static class TestImages public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png"; public const string FrameOffset = "Png/animated/frame-offset.png"; public const string DefaultNotAnimated = "Png/animated/default-not-animated.png"; + public const string AnimatedFrameCount = "Png/animated/issue-animated-frame-count.png"; public const string Issue2666 = "Png/issues/Issue_2666.png"; public const string Issue2882 = "Png/issues/Issue_2882.png"; diff --git a/tests/Images/Input/Png/animated/issue-animated-frame-count.png b/tests/Images/Input/Png/animated/issue-animated-frame-count.png new file mode 100644 index 0000000000..db8ff47b9b --- /dev/null +++ b/tests/Images/Input/Png/animated/issue-animated-frame-count.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62d51679bcb096ae45ae0f5bf874916ad929014f68ae43b487253d5050c8b68b +size 13561079 From 7b13e1df1b909510ba1303e4b7b25d9d1d8e9df4 Mon Sep 17 00:00:00 2001 From: Andreas Eriksson <4438107+andreas-eriksson@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:58:13 +0200 Subject: [PATCH 2/9] Add generated animated PNG tests for Identify and Decode frame counts --- .../Formats/Png/PngDecoderTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 69e656849b..0ba8866127 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -423,6 +423,55 @@ public partial class PngDecoderTests Assert.Equal(50, imageInfo.FrameMetadataCollection.Count); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(100)] + public void Identify_AnimatedPng_FrameCount_MatchesDecode(int frameCount) + { + using Image image = new(10, 10, Color.Red.ToPixel()); + for (int i = 1; i < frameCount; i++) + { + using ImageFrame frame = new(Configuration.Default, 10, 10); + image.Frames.AddFrame(frame); + } + + using MemoryStream stream = new(); + image.Save(stream, new PngEncoder()); + stream.Position = 0; + + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(frameCount, imageInfo.FrameMetadataCollection.Count); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(100)] + public void Decode_AnimatedPng_FrameCount(int frameCount) + { + using Image image = new(10, 10, Color.Red.ToPixel()); + for (int i = 1; i < frameCount; i++) + { + using ImageFrame frame = new(Configuration.Default, 10, 10); + image.Frames.AddFrame(frame); + } + + using MemoryStream stream = new(); + image.Save(stream, new PngEncoder()); + stream.Position = 0; + + using Image decoded = Image.Load(stream); + + Assert.Equal(frameCount, decoded.Frames.Count); + } + [Theory] [WithFile(TestImages.Png.Bad.MissingDataChunk, PixelTypes.Rgba32)] public void Decode_MissingDataChunk_ThrowsException(TestImageProvider provider) From b33f288a90e88160411d62553a40605b0ce2f842 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 1 Apr 2026 22:42:17 +1000 Subject: [PATCH 3/9] Add working-buffer blending and adjust row APIs --- .../Advanced/IRowOperation{TBuffer}.cs | 4 +- .../Advanced/ParallelRowIterator.cs | 8 +- .../Formats/Webp/WebpAnimationDecoder.cs | 13 +- .../PixelFormats/PixelBlender{TPixel}.cs | 198 ++++++++++++++++-- .../DrawImageProcessor{TPixelBg,TPixelFg}.cs | 16 +- 5 files changed, 208 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs index 8b46fc5c31..dfaec640b8 100644 --- a/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs +++ b/src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs @@ -15,12 +15,12 @@ public interface IRowOperation /// /// The bounds of the operation. /// The required buffer length. - int GetRequiredBufferLength(Rectangle bounds); + public int GetRequiredBufferLength(Rectangle bounds); /// /// Invokes the method passing the row and a buffer. /// /// The row y coordinate. /// The contiguous region of memory. - void Invoke(int y, Span span); + public void Invoke(int y, Span span); } diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs index b878f9ec0a..d170631a29 100644 --- a/src/ImageSharp/Advanced/ParallelRowIterator.cs +++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs @@ -68,7 +68,7 @@ public static partial class ParallelRowIterator ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -138,7 +138,7 @@ public static partial class ParallelRowIterator ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -195,7 +195,7 @@ public static partial class ParallelRowIterator ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, @@ -262,7 +262,7 @@ public static partial class ParallelRowIterator ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps }; RowIntervalOperationWrapper wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation); - Parallel.For( + _ = Parallel.For( 0, numOfSteps, parallelOptions, diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index a237054133..71e609fbcb 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; @@ -456,15 +457,21 @@ internal class WebpAnimationDecoder : IDisposable // The destination frame has already been prepopulated with the pixel data from the previous frame // so blending will leave the desired result which takes into consideration restoration to the // background color within the restore area. - PixelBlender blender = - PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + PixelColorBlendingMode.Normal, + PixelAlphaCompositionMode.SrcOver); + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + using IMemoryOwner workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate(restoreArea.Width * 3); + Span workingBuffer = workingBufferOwner.GetSpan(); for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; - blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer); } return; diff --git a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs index bad9ae01c1..fcd28796fb 100644 --- a/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Numerics; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.PixelFormats; @@ -52,16 +51,53 @@ public abstract class PixelBlender Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + float amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// @@ -87,14 +123,48 @@ public abstract class PixelBlender Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends a row against a constant source color using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A value between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + float amount, + Span workingBuffer) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// @@ -116,6 +186,27 @@ public abstract class PixelBlender ReadOnlySpan amount) => this.Blend(configuration, destination, background, source, amount); + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + => this.Blend(configuration, destination, background, source, amount, workingBuffer); + /// /// Blends 2 rows together /// @@ -142,20 +233,89 @@ public abstract class PixelBlender Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 3); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); - Span sourceVectors = buffer.Slice(maxLength * 2, maxLength); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 3)]); + } + + /// + /// Blends a row against a constant source color. + /// + /// to use internally + /// the destination span + /// the background span + /// the source color + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + TPixel source, + ReadOnlySpan amount) + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + + using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); + this.Blend( + configuration, + destination, + background, + source, + amount, + buffer.Memory.Span[..(maxLength * 2)]); + } + + /// + /// Blends 2 rows together using caller-provided temporary vector scratch. + /// + /// the pixel format of the source span + /// to use internally + /// the destination span + /// the background span + /// the source span + /// + /// A span with values between 0 and 1 indicating the weight of the second source vector. + /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. + /// + /// Reusable temporary vector scratch with capacity for at least 3 rows. + public void Blend( + Configuration configuration, + Span destination, + ReadOnlySpan background, + ReadOnlySpan source, + ReadOnlySpan amount, + Span workingBuffer) + where TPixelSrc : unmanaged, IPixel + { + int maxLength = destination.Length; + Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); + Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length)); + Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length)); + + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); + Span sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); PixelOperations.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// - /// Blends a row against a constant source color. + /// Blends a row against a constant source color using caller-provided temporary vector scratch. /// /// to use internally /// the destination span @@ -165,26 +325,28 @@ public abstract class PixelBlender /// A span with values between 0 and 1 indicating the weight of the second source vector. /// At amount = 0, "background" is returned, at amount = 1, "source" is returned. /// + /// Reusable temporary vector scratch with capacity for at least 2 rows. public void Blend( Configuration configuration, Span destination, ReadOnlySpan background, TPixel source, - ReadOnlySpan amount) + ReadOnlySpan amount, + Span workingBuffer) { int maxLength = destination.Length; Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length)); Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length)); + Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length)); - using IMemoryOwner buffer = configuration.MemoryAllocator.Allocate(maxLength * 2); - Span destinationVectors = buffer.Slice(0, maxLength); - Span backgroundVectors = buffer.Slice(maxLength, maxLength); + Span destinationVectors = workingBuffer[..maxLength]; + Span backgroundVectors = workingBuffer.Slice(maxLength, maxLength); PixelOperations.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale); this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount); - PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); + PixelOperations.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale); } /// diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs index 7a1ffb85f9..f2faee4f9c 100644 --- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs +++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -145,7 +146,7 @@ internal class DrawImageProcessor : ImageProcessor this.Blender, this.Opacity); - ParallelRowIterator.IterateRows( + ParallelRowIterator.IterateRows( configuration, new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height), in operation); @@ -161,7 +162,7 @@ internal class DrawImageProcessor : ImageProcessor /// /// A implementing the draw logic for . /// - private readonly struct RowOperation : IRowOperation + private readonly struct RowOperation : IRowOperation { private readonly Buffer2D background; private readonly Buffer2D foreground; @@ -190,13 +191,20 @@ internal class DrawImageProcessor : ImageProcessor this.opacity = opacity; } + /// + public int GetRequiredBufferLength(Rectangle bounds) + + // By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend + // We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending. + => 3 * bounds.Width; + /// [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) + public void Invoke(int y, Span span) { Span background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width); Span foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width); - this.blender.Blend(this.configuration, background, background, foreground, this.opacity); + this.blender.Blend(this.configuration, background, background, foreground, this.opacity, span); } } } From e06a015cf59148e24d1fc940aed3b9e4d8356103 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 11:39:29 +1000 Subject: [PATCH 4/9] Fix SIMD slicing and padding length handling. Fix #3104 --- .../Common/Helpers/SimdUtils.HwIntrinsics.cs | 14 ++++++++------ .../Encoder/SpectralConverter{TPixel}.cs | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs index ff5ea5de33..076590605d 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs @@ -752,7 +752,7 @@ internal static partial class SimdUtils /// Implementation is based on MagicScaler code: /// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L80-L182 /// - internal static unsafe void ByteToNormalizedFloat( + internal static void ByteToNormalizedFloat( ReadOnlySpan source, Span destination) { @@ -1172,8 +1172,10 @@ internal static partial class SimdUtils Vector256 rgb, rg, bx; Vector256 r, g, b; + // Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load, + // so we need 3 extra pixels of addressable slack beyond the vectorized chunk. const int bytesPerRgbStride = 24; - nuint count = (uint)source.Length / 8; + nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0; for (nuint i = 0; i < count; i++) { rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte(); @@ -1193,10 +1195,10 @@ internal static partial class SimdUtils } int sliceCount = (int)(count * 8); - redChannel = redChannel.Slice(sliceCount); - greenChannel = greenChannel.Slice(sliceCount); - blueChannel = blueChannel.Slice(sliceCount); - source = source.Slice(sliceCount); + redChannel = redChannel[sliceCount..]; + greenChannel = greenChannel[sliceCount..]; + blueChannel = blueChannel[sliceCount..]; + source = source[sliceCount..]; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs index b60ef68f11..8662c5c49b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -114,9 +114,9 @@ internal class SpectralConverter : SpectralConverter, IDisposable Span sourceRow = this.pixelBuffer.DangerousGetRowSpan(srcIndex); PixelOperations.Instance.UnpackIntoRgbPlanes(rLane, gLane, bLane, sourceRow); - rLane.Slice(paddingStartIndex).Fill(rLane[paddingStartIndex - 1]); - gLane.Slice(paddingStartIndex).Fill(gLane[paddingStartIndex - 1]); - bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]); + rLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(rLane[paddingStartIndex - 1]); + gLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(gLane[paddingStartIndex - 1]); + bLane.Slice(paddingStartIndex, paddedPixelsCount).Fill(bLane[paddingStartIndex - 1]); // Convert from rgb24 to target pixel type JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y); From a76c02f7c095df9b578a8c0336712b148abd92d1 Mon Sep 17 00:00:00 2001 From: Andreas <4438107+andreas-eriksson@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:46:22 +0200 Subject: [PATCH 5/9] Replace test image with a smaller one. Adjusted Identify_AnimatedPng_ReadsFrameCountCorrectly to expect 48 frames instead of 50. --- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 2 +- .../Images/Input/Png/animated/issue-animated-frame-count.png | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 0ba8866127..802f2aba39 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -420,7 +420,7 @@ public partial class PngDecoderTests ImageInfo imageInfo = Image.Identify(stream); Assert.NotNull(imageInfo); - Assert.Equal(50, imageInfo.FrameMetadataCollection.Count); + Assert.Equal(48, imageInfo.FrameMetadataCollection.Count); } [Theory] diff --git a/tests/Images/Input/Png/animated/issue-animated-frame-count.png b/tests/Images/Input/Png/animated/issue-animated-frame-count.png index db8ff47b9b..88427f4873 100644 --- a/tests/Images/Input/Png/animated/issue-animated-frame-count.png +++ b/tests/Images/Input/Png/animated/issue-animated-frame-count.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62d51679bcb096ae45ae0f5bf874916ad929014f68ae43b487253d5050c8b68b -size 13561079 +oid sha256:af4e320f586ab26c55612a7ccfc98a8c99cd6a0efe8a70d379503751d06fe8bd +size 51542 From 9569449ac46e844c33c7b4073c8773d9f3d87134 Mon Sep 17 00:00:00 2001 From: Andreas <4438107+andreas-eriksson@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:19:40 +0200 Subject: [PATCH 6/9] Fix MaxFrames handling in PNG decoder - Change >= to > for correct MaxFrames boundary - Skip fdAT chunk data when hitting maxFrames in Identify to maintain stream alignment - Add tests for Identify and Load with MaxFrames --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 12 ++++++---- .../Formats/Png/PngDecoderTests.cs | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 52858ec129..d794c66e27 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -214,7 +214,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameData: { - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { goto EOF; } @@ -275,7 +275,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore previousFrameControl = currentFrameControl; } - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { goto EOF; } @@ -402,7 +402,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameControl: ++frameCount; - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { break; } @@ -411,8 +411,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore break; case PngChunkType.FrameData: - if (frameCount >= this.maxFrames) + if (frameCount > this.maxFrames) { + // Must skip the chunk data even when we've hit maxFrames, because TryReadChunk + // restores the stream position to the start of the fdAT data after CRC validation. + this.SkipChunkDataAndCrc(chunk); + this.SkipRemainingFrameDataChunks(buffer); break; } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 802f2aba39..4712fc0dd5 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -423,6 +423,30 @@ public partial class PngDecoderTests Assert.Equal(48, imageInfo.FrameMetadataCollection.Count); } + [Fact] + public void Identify_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(new DecoderOptions { MaxFrames = 40 }, stream); + + Assert.NotNull(imageInfo); + Assert.Equal(40, imageInfo.FrameMetadataCollection.Count); + } + + [Fact] + public void Load_AnimatedPngWithMaxFrames_ReadsFrameCountCorrectly() + { + TestFile testFile = TestFile.Create(TestImages.Png.AnimatedFrameCount); + + using MemoryStream stream = new(testFile.Bytes, false); + using Image image = Image.Load(new DecoderOptions { MaxFrames = 40 }, stream); + + Assert.NotNull(image); + Assert.Equal(40, image.Frames.Count); + } + [Theory] [InlineData(1)] [InlineData(2)] From 90f0c0b5d47bc3d68cc4ad69222658f497e29516 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 21:32:38 +1000 Subject: [PATCH 7/9] Update and simplify quantization color caches. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 2 - .../Quantization/ColorMatchingMode.cs | 10 +- .../EuclideanPixelMap{TPixel,TCache}.cs | 116 +++++- .../Quantization/IColorIndexCache.cs | 387 +++++------------- .../Quantization/OctreeQuantizer{TPixel}.cs | 2 +- .../Processing/ColorMatchingCaches.cs | 302 ++++++++++++++ .../Formats/Png/PngEncoderTests.cs | 2 +- .../Quantization/PaletteQuantizerTests.cs | 158 +++++++ 8 files changed, 667 insertions(+), 312 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index fef49bffd4..2944b58e5f 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -523,10 +523,8 @@ internal static class AotCompilerTools private static void AotCompilePixelMaps() where TPixel : unmanaged, IPixel { - default(EuclideanPixelMap).GetClosestColor(default, out _); default(EuclideanPixelMap).GetClosestColor(default, out _); default(EuclideanPixelMap).GetClosestColor(default, out _); - default(EuclideanPixelMap).GetClosestColor(default, out _); } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs index 26fd7d5d76..c520d7c54b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs @@ -15,14 +15,8 @@ public enum ColorMatchingMode Coarse, /// - /// Enables an exact color match cache for the first 512 unique colors encountered, - /// falling back to coarse matching thereafter. - /// - Hybrid, - - /// - /// Performs exact color matching without any caching optimizations. - /// This is the slowest but most accurate matching strategy. + /// Performs exact color matching using a bounded exact-match cache with eviction. + /// This preserves exact color matching while accelerating repeated colors. /// Exact } diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs index 5b0c7252cb..e2e7206e09 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -71,32 +73,107 @@ internal sealed class EuclideanPixelMap : PixelMap [MethodImpl(InliningOptions.ColdPath)] private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) { - // Loop through the palette and find the nearest match. + ReadOnlySpan rgbaPalette = this.rgbaPalette; + ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPalette); int index = 0; - float leastDistance = float.MaxValue; - for (int i = 0; i < this.rgbaPalette.Length; i++) + int leastDistance = int.MaxValue; + int i = 0; + + if (Vector128.IsHardwareAccelerated && rgbaPalette.Length >= 4) { - Rgba32 candidate = this.rgbaPalette[i]; - if (candidate.PackedValue == rgba.PackedValue) - { - index = i; - break; - } + // Duplicate the query color so one 128-bit register can be subtracted from + // two packed RGBA candidates at a time after widening. + Vector128 pixel = Vector128.Create( + rgba.R, + rgba.G, + rgba.B, + rgba.A, + rgba.R, + rgba.G, + rgba.B, + rgba.A); - float distance = DistanceSquared(rgba, candidate); - if (distance == 0) + int vectorizedLength = rgbaPalette.Length & ~0x03; + + for (; i < vectorizedLength; i += 4) { - index = i; - break; + // Load four packed Rgba32 values (16 bytes) and widen them into two vectors: + // [c0.r, c0.g, c0.b, c0.a, c1.r, ...] and [c2.r, c2.g, c2.b, c2.a, c3.r, ...]. + Vector128 packed = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.Add(ref rgbaPaletteRef, i))); + Vector128 lowerDiff = Vector128.WidenLower(packed).AsInt16() - pixel; + Vector128 upperDiff = Vector128.WidenUpper(packed).AsInt16() - pixel; + + // MultiplyAddAdjacent collapses channel squares into RG + BA partial sums, + // so each pair of int lanes still corresponds to one candidate color. + Vector128 lowerPairs = Vector128_.MultiplyAddAdjacent(lowerDiff, lowerDiff); + Vector128 upperPairs = Vector128_.MultiplyAddAdjacent(upperDiff, upperDiff); + + // Sum the two partials for candidates i and i + 1. + ref int lowerRef = ref Unsafe.As, int>(ref lowerPairs); + int distance = lowerRef + Unsafe.Add(ref lowerRef, 1); + if (distance < leastDistance) + { + index = i; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref lowerRef, 2) + Unsafe.Add(ref lowerRef, 3); + if (distance < leastDistance) + { + index = i + 1; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + // Sum the two partials for candidates i + 2 and i + 3. + ref int upperRef = ref Unsafe.As, int>(ref upperPairs); + distance = upperRef + Unsafe.Add(ref upperRef, 1); + if (distance < leastDistance) + { + index = i + 2; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } + + distance = Unsafe.Add(ref upperRef, 2) + Unsafe.Add(ref upperRef, 3); + if (distance < leastDistance) + { + index = i + 3; + leastDistance = distance; + if (distance == 0) + { + goto Found; + } + } } + } + for (; i < rgbaPalette.Length; i++) + { + int distance = DistanceSquared(rgba, Unsafe.Add(ref rgbaPaletteRef, i)); if (distance < leastDistance) { index = i; leastDistance = distance; + if (distance == 0) + { + goto Found; + } } } + Found: + // Now I have the index, pop it into the cache for next time _ = this.cache.TryAdd(rgba, (short)index); match = Unsafe.Add(ref paletteRef, (uint)index); @@ -111,12 +188,12 @@ internal sealed class EuclideanPixelMap : PixelMap /// The second point. /// The distance squared. [MethodImpl(InliningOptions.ShortMethod)] - private static float DistanceSquared(Rgba32 a, Rgba32 b) + private static int DistanceSquared(Rgba32 a, Rgba32 b) { - float deltaR = a.R - b.R; - float deltaG = a.G - b.G; - float deltaB = a.B - b.B; - float deltaA = a.A - b.A; + int deltaR = a.R - b.R; + int deltaG = a.G - b.G; + int deltaB = a.B - b.B; + int deltaA = a.A - b.A; return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); } @@ -177,8 +254,7 @@ internal static class PixelMapFactory ColorMatchingMode colorMatchingMode) where TPixel : unmanaged, IPixel => colorMatchingMode switch { - ColorMatchingMode.Hybrid => new EuclideanPixelMap(configuration, palette), - ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette), + ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette), _ => new EuclideanPixelMap(configuration, palette), }; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs index 32d95137bc..76598e0046 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs @@ -56,147 +56,6 @@ internal interface IColorIndexCache : IColorIndexCache public static abstract T Create(MemoryAllocator allocator); } -/// -/// 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. -/// -/// -/// 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. -/// -internal unsafe struct HybridCache : IColorIndexCache -{ - private CoarseCache coarseCache; - private AccurateCache accurateCache; - - public HybridCache(MemoryAllocator allocator) - { - this.accurateCache = AccurateCache.Create(allocator); - this.coarseCache = CoarseCache.Create(allocator); - } - - /// - public static HybridCache Create(MemoryAllocator allocator) => new(allocator); - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public bool TryAdd(Rgba32 color, short index) - { - if (this.accurateCache.TryAdd(color, index)) - { - return true; - } - - return this.coarseCache.TryAdd(color, index); - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 color, out short value) - { - if (this.accurateCache.TryGetValue(color, out value)) - { - return true; - } - - return this.coarseCache.TryGetValue(color, out value); - } - - /// - public readonly void Clear() - { - this.accurateCache.Clear(); - this.coarseCache.Clear(); - } - - /// - public void Dispose() - { - this.accurateCache.Dispose(); - this.coarseCache.Dispose(); - } -} - -/// -/// A coarse cache for color distance lookups that uses a fixed-size lookup table. -/// -/// -/// This cache uses a fixed lookup table with 2,097,152 bins, each storing a 2-byte value, -/// resulting in a memory usage of approximately 4 MB. Lookups and insertions are -/// performed in constant time (O(1)) via direct table indexing. This design is optimized for -/// speed while maintaining a predictable, fixed memory footprint. -/// -internal unsafe struct CoarseCache : IColorIndexCache -{ - private const int IndexRBits = 5; - private const int IndexGBits = 5; - private const int IndexBBits = 5; - private const int IndexABits = 6; - private const int IndexRCount = 1 << IndexRBits; // 32 bins for red - private const int IndexGCount = 1 << IndexGBits; // 32 bins for green - private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue - private const int IndexACount = 1 << IndexABits; // 64 bins for alpha - private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 2,097,152 bins - - private readonly IMemoryOwner binsOwner; - private readonly short* binsPointer; - private MemoryHandle binsHandle; - - private CoarseCache(MemoryAllocator allocator) - { - this.binsOwner = allocator.Allocate(TotalBins); - this.binsOwner.GetSpan().Fill(-1); - this.binsHandle = this.binsOwner.Memory.Pin(); - this.binsPointer = (short*)this.binsHandle.Pointer; - } - - /// - public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryAdd(Rgba32 color, short value) - { - this.binsPointer[GetCoarseIndex(color)] = value; - return true; - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 color, out short value) - { - value = this.binsPointer[GetCoarseIndex(color)]; - return value > -1; // Coarse match found - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static int GetCoarseIndex(Rgba32 color) - { - int rIndex = color.R >> (8 - IndexRBits); - int gIndex = color.G >> (8 - IndexGBits); - int bIndex = color.B >> (8 - IndexBBits); - int aIndex = color.A >> (8 - IndexABits); - - return (aIndex * IndexRCount * IndexGCount * IndexBCount) + - (rIndex * IndexGCount * IndexBCount) + - (gIndex * IndexBCount) + - bIndex; - } - - /// - public readonly void Clear() - => this.binsOwner.GetSpan().Fill(-1); - - /// - public void Dispose() - { - this.binsHandle.Dispose(); - this.binsOwner.Dispose(); - } -} - /// /// /// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values, @@ -225,7 +84,7 @@ internal unsafe struct CoarseCache : IColorIndexCache /// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries). /// /// -internal unsafe struct CoarseCacheLite : IColorIndexCache +internal unsafe struct CoarseCache : IColorIndexCache { // Use 5 bits per channel for R, G, and B: 32 levels each. // Total buckets = 32^3 = 32768. @@ -236,7 +95,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache private readonly AlphaBucket* buckets; private MemoryHandle bucketHandle; - private CoarseCacheLite(MemoryAllocator allocator) + private CoarseCache(MemoryAllocator allocator) { this.bucketsOwner = allocator.Allocate(BucketCount, AllocationOptions.Clean); this.bucketHandle = this.bucketsOwner.Memory.Pin(); @@ -244,7 +103,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } /// - public static CoarseCacheLite Create(MemoryAllocator allocator) => new(allocator); + public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); /// public readonly bool TryAdd(Rgba32 color, short paletteIndex) @@ -289,14 +148,11 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } [MethodImpl(InliningOptions.ShortMethod)] - private static byte QuantizeAlpha(byte a) - - // Quantize to 6 bits: shift right by (8 - 6) = 2 bits. - => (byte)(a >> 2); + private static byte QuantizeAlpha(byte a) => (byte)(a >> 2); public struct AlphaEntry { - // Store the alpha value quantized to 6 bits (0..63) + // Store the alpha value quantized to 6 bits (0..63). public byte QuantizedAlpha; public short PaletteIndex; } @@ -312,7 +168,7 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache // 2. However, in practice (based on probability theory and typical image data), // the number of unique alpha values that actually occur for a given quantized RGB // bucket is usually very small. If you randomly sample 8 values out of 64, - // the probability that these 4 samples are all unique is high if the distribution + // the probability that these samples are all unique is high if the distribution // of alpha values is skewed or if only a few alpha values are used. // // 3. Statistically, for many real-world images, most RGB buckets will have only a couple @@ -377,51 +233,49 @@ internal unsafe struct CoarseCacheLite : IColorIndexCache } /// -/// A fixed-capacity dictionary with exactly 512 entries mapping a key -/// to a value. +/// A fixed-size exact-match cache that stores packed RGBA keys with 4-way set associativity. /// /// -/// 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 & 0x1FF), and collisions are -/// resolved through a linked chain stored in the field. +/// The cache holds 512 total entries split across 128 sets. Entries are evicted within a set +/// using round-robin replacement, but cached values are returned only when the full packed RGBA +/// key matches, preserving exact quantization results with predictable memory usage. /// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, -/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are -/// typically very short; in the worst-case, the number of iterations is bounded by 256. +/// on average, O(1) since each lookup probes at most four candidate entries within the selected set. /// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. /// internal unsafe struct AccurateCache : IColorIndexCache { - // 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 bucketsOwner; - private MemoryHandle bucketsHandle; - private short* buckets; + public const int Capacity = 512; + private const int Ways = 4; + private const int SetCount = Capacity / Ways; + private const int SetMask = SetCount - 1; - // Entries array: stores up to 256 entries. - private readonly IMemoryOwner entriesOwner; - private MemoryHandle entriesHandle; - private Entry* entries; + private readonly IMemoryOwner keysOwner; + private MemoryHandle keysHandle; + private uint* keys; - public const int Capacity = 512; + private readonly IMemoryOwner valuesOwner; + private MemoryHandle valuesHandle; + private ushort* values; + + private readonly IMemoryOwner nextVictimOwner; + private MemoryHandle nextVictimHandle; + private byte* nextVictim; private AccurateCache(MemoryAllocator allocator) { - this.Count = 0; - - // Allocate exactly 512 indexes for buckets. - this.bucketsOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); - Span 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(Capacity, AllocationOptions.Clean); - this.entriesHandle = this.entriesOwner.Memory.Pin(); - this.entries = (Entry*)this.entriesHandle.Pointer; - } + this.keysOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.keysHandle = this.keysOwner.Memory.Pin(); + this.keys = (uint*)this.keysHandle.Pointer; - public int Count { get; private set; } + this.valuesOwner = allocator.Allocate(Capacity, AllocationOptions.Clean); + this.valuesHandle = this.valuesOwner.Memory.Pin(); + this.values = (ushort*)this.valuesHandle.Pointer; + + this.nextVictimOwner = allocator.Allocate(SetCount, AllocationOptions.Clean); + this.nextVictimHandle = this.nextVictimOwner.Memory.Pin(); + this.nextVictim = (byte*)this.nextVictimHandle.Pointer; + } /// public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); @@ -430,140 +284,113 @@ internal unsafe struct AccurateCache : IColorIndexCache [MethodImpl(InliningOptions.ShortMethod)] public bool TryAdd(Rgba32 color, short value) { - if (this.Count == Capacity) - { - return false; // Dictionary is full. - } - uint key = color.PackedValue; + int set = GetSetIndex(key); + int start = set * Ways; + int empty = -1; + + uint* keys = this.keys; + ushort* values = this.values; + ushort storedValue = (ushort)(value + 1); - // The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A - // (with R in the most significant byte and A in the least significant). - // To compute the bucket index: - // 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels. - // 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A). - // 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A), - // which helps to counteract situations where one or more channels have a limited range. - // 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511, - // which corresponds to our fixed bucket count of 512. - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; - - // Traverse the collision chain. - Entry* entries = this.entries; - while (i != -1) + for (int i = start; i < start + Ways; i++) { - Entry e = entries[i]; - if (e.Key == key) + ushort candidate = values[i]; + if (candidate == 0) { - // Key already exists; do not overwrite. - return false; + empty = i; + continue; } - i = e.Next; + if (keys[i] == key) + { + values[i] = storedValue; + return true; + } } - short index = (short)this.Count; - this.Count++; + int slot = empty >= 0 ? empty : start + this.nextVictim[set]; + keys[slot] = key; + values[slot] = storedValue; - // Insert the new entry: - entries[index].Key = key; - entries[index].Value = value; + if (empty < 0) + { + this.nextVictim[set] = (byte)((this.nextVictim[set] + 1) & (Ways - 1)); + } - // Link this new entry into the bucket chain. - entries[index].Next = this.buckets[bucket]; - this.buckets[bucket] = index; return true; } /// [MethodImpl(InliningOptions.ShortMethod)] - public bool TryGetValue(Rgba32 color, out short value) + public readonly bool TryGetValue(Rgba32 color, out short value) { uint key = color.PackedValue; - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; + int start = GetSetIndex(key) * Ways; - // If the bucket is empty, return immediately. - if (i == -1) - { - value = -1; - return false; - } + uint* keys = this.keys; + ushort* values = this.values; - // Traverse the chain. - Entry* entries = this.entries; - do + for (int i = start; i < start + Ways; i++) { - Entry e = entries[i]; - if (e.Key == key) + ushort candidate = values[i]; + if (candidate != 0 && keys[i] == key) { - value = e.Value; + value = (short)(candidate - 1); return true; } - - i = e.Next; } - while (i != -1); value = -1; return false; } /// - /// Clears the dictionary. + /// Clears the cache. /// - public void Clear() + public readonly void Clear() { - Span bucketSpan = this.bucketsOwner.GetSpan(); - bucketSpan.Fill(-1); - this.Count = 0; + this.valuesOwner.GetSpan().Clear(); + this.nextVictimOwner.GetSpan().Clear(); } public void Dispose() { - this.bucketsHandle.Dispose(); - this.bucketsOwner.Dispose(); - this.entriesHandle.Dispose(); - this.entriesOwner.Dispose(); - this.buckets = null; - this.entries = null; + this.keysHandle.Dispose(); + this.keysOwner.Dispose(); + this.valuesHandle.Dispose(); + this.valuesOwner.Dispose(); + this.nextVictimHandle.Dispose(); + this.nextVictimOwner.Dispose(); + this.keys = null; + this.values = null; + this.nextVictim = null; } - private struct Entry - { - public uint Key; // The key (packed RGBA) - public short Value; // The value; -1 means unused. - public short Next; // Index of the next entry in the chain, or -1 if none. - } -} - -/// -/// Represents a cache that does not store any values. -/// It allows adding colors, but always returns false when trying to retrieve them. -/// -internal readonly struct NullCache : IColorIndexCache -{ - /// - public static NullCache Create(MemoryAllocator allocator) => default; - - /// - public bool TryAdd(Rgba32 color, short value) => true; - - /// - public bool TryGetValue(Rgba32 color, out short value) - { - value = -1; - return false; - } - - /// - public void Clear() - { - } - - /// - public void Dispose() - { - } + /// + /// Maps a packed RGBA key to one of the cache sets used by . + /// + /// The packed key. + /// The zero-based set index for the key. + /// + /// + /// The cache is 4-way set-associative, so this hash only needs to choose one of + /// sets before probing up to four candidate entries. + /// + /// + /// is laid out as R | (G << 8) | (B << 16) | (A << 24). + /// The XOR-fold mixes neighboring bytes into the low bits, and the final mask selects the + /// set. With the current 128-set layout that makes the selected set effectively depend on + /// the low 7 bits of R ^ G ^ B. Alpha still participates in the later exact key + /// comparison, but not in set selection. + /// + /// + /// Collisions are expected and acceptable here. Correctness comes from the full packed-key + /// comparison during probing; this hash only aims to spread keys cheaply enough that each + /// access touches at most one 4-entry set. + /// + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetSetIndex(uint key) + => (int)(((key >> 16) ^ (key >> 8) ^ key) & SetMask); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index 07596b68a8..bdf2ba20a8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -368,7 +368,7 @@ public struct OctreeQuantizer : IQuantizer public void Dispose() => this.nodesOwner.Dispose(); [StructLayout(LayoutKind.Sequential)] - internal unsafe struct OctreeNode + internal struct OctreeNode { public bool Leaf; public int PixelCount; diff --git a/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs b/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs new file mode 100644 index 0000000000..dbaf21a8ef --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Processing/ColorMatchingCaches.cs @@ -0,0 +1,302 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Benchmarks.Processing; + +[Config(typeof(Config.Standard))] +public class ColorMatchingCaches +{ + // IterationSetup forces BenchmarkDotNet to use a single benchmark invocation per iteration. + // Repeated lookups can safely replay a smaller working set because that workload is explicitly + // meant to model steady-state cache hits after warmup. + private const int RepeatedLookupCount = 262_144; + + // DitherLike should avoid replaying the same stream across multiple passes because that warms + // the caches in a way real high-churn input usually does not. Make the single pass larger instead. + private const int DitherLikeLookupCount = 1_048_576; + private const int RepeatedPassCount = 16; + + private Rgba32[] palette; + private Rgba32[] repeatedSeedColors; + private Rgba32[] repeatedLookups; + private Rgba32[] ditherLookups; + + private PixelMap coarse; + private PixelMap legacyCoarse; + private PixelMap exact; + private PixelMap uncached; + + [Params(16, 256)] + public int PaletteSize { get; set; } + + [Params(CacheWorkload.Repeated, CacheWorkload.DitherLike)] + public CacheWorkload Workload { get; set; } + + [GlobalSetup] + public void Setup() + { + this.palette = CreatePalette(this.PaletteSize); + this.repeatedSeedColors = CreateRepeatedSeedColors(this.palette); + this.repeatedLookups = CreateRepeatedLookups(this.repeatedSeedColors); + this.ditherLookups = CreateDitherLikeLookups(); + + this.coarse = CreatePixelMap(this.palette); + this.legacyCoarse = CreatePixelMap(this.palette); + this.exact = CreatePixelMap(this.palette); + this.uncached = CreatePixelMap(this.palette); + } + + [IterationSetup] + public void ResetCaches() + { + // Each benchmark iteration should start from the same cache state so we measure + // the cache policy itself rather than warm state carried over from a previous iteration. + this.coarse.Clear(this.palette); + this.legacyCoarse.Clear(this.palette); + this.exact.Clear(this.palette); + this.uncached.Clear(this.palette); + + if (this.Workload == CacheWorkload.Repeated) + { + // Prime the repeated workload so the benchmark reflects steady-state hit behavior + // instead of mostly measuring the first-wave fill cost. + Prime(this.coarse, this.repeatedSeedColors); + Prime(this.legacyCoarse, this.repeatedSeedColors); + Prime(this.exact, this.repeatedSeedColors); + Prime(this.uncached, this.repeatedSeedColors); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.coarse.Dispose(); + this.legacyCoarse.Dispose(); + this.exact.Dispose(); + this.uncached.Dispose(); + } + + [Benchmark(Baseline = true, Description = "Coarse")] + public int Coarse() => this.Run(this.coarse); + + [Benchmark(Description = "Legacy Coarse")] + public int LegacyCoarse() => this.Run(this.legacyCoarse); + + [Benchmark(Description = "Exact Cached")] + public int Exact() => this.Run(this.exact); + + [Benchmark(Description = "Exact Uncached")] + public int Uncached() => this.Run(this.uncached); + + public enum CacheWorkload + { + // A small working set that is intentionally reused after priming to measure hit-heavy behavior. + Repeated, + + // A deterministic high-churn stream intended to resemble dithered lookups where exact repeats are rare. + DitherLike + } + + private int Run(PixelMap map) + { + Rgba32[] lookups = this.Workload == CacheWorkload.Repeated ? this.repeatedLookups : this.ditherLookups; + int passCount = this.Workload == CacheWorkload.Repeated ? RepeatedPassCount : 1; + int checksum = 0; + + // Repeated intentionally replays the same lookup stream to measure steady-state hit behavior. + // DitherLike runs as a single larger pass so we do not turn a churn-heavy workload into an + // artificially warmed cache benchmark by replaying the exact same sequence. + for (int pass = 0; pass < passCount; pass++) + { + for (int i = 0; i < lookups.Length; i++) + { + checksum = unchecked((checksum * 31) + map.GetClosestColor(lookups[i], out _)); + } + } + + return checksum; + } + + private static PixelMap CreatePixelMap(Rgba32[] palette) + where TCache : struct, IColorIndexCache + => new EuclideanPixelMap(Configuration.Default, palette); + + private static void Prime(PixelMap map, Rgba32[] colors) + { + for (int i = 0; i < colors.Length; i++) + { + map.GetClosestColor(colors[i], out _); + } + } + + private static Rgba32[] CreatePalette(int count) + { + Rgba32[] result = new Rgba32[count]; + + for (int i = 0; i < result.Length; i++) + { + // Use the Knuth/golden-ratio multiplicative hash constant to spread colors across + // RGBA space without clustering into a gradient. That keeps the benchmark from + // accidentally favoring any cache because the palette itself is too regular. + uint value = unchecked((uint)(i + 1) * 2654435761U); + result[i] = new( + (byte)value, + (byte)(value >> 8), + (byte)(value >> 16), + (byte)((value >> 24) | 0x80)); + } + + return result; + } + + private static Rgba32[] CreateRepeatedSeedColors(Rgba32[] palette) + { + // Reuse colors derived from the palette but perturb them slightly so the workload still + // exercises nearest-color matching rather than only exact palette-entry hits. + int count = Math.Min(64, palette.Length * 2); + Rgba32[] result = new Rgba32[count]; + + for (int i = 0; i < result.Length; i++) + { + Rgba32 source = palette[(i * 17) % palette.Length]; + result[i] = new( + (byte)(source.R + ((i * 3) & 0x07)), + (byte)(source.G + ((i * 5) & 0x07)), + (byte)(source.B + ((i * 7) & 0x07)), + source.A); + } + + return result; + } + + private static Rgba32[] CreateRepeatedLookups(Rgba32[] seedColors) + { + Rgba32[] result = new Rgba32[RepeatedLookupCount]; + + // Cycle a small seed set to produce a stable, hit-heavy stream after priming. + for (int i = 0; i < result.Length; i++) + { + result[i] = seedColors[i % seedColors.Length]; + } + + return result; + } + + private static Rgba32[] CreateDitherLikeLookups() + { + Rgba32[] result = new Rgba32[DitherLikeLookupCount]; + + // Generate a deterministic pseudo-image signal with independent channel slopes so nearby + // samples are correlated but exact repeats are uncommon, which is closer to dithered input. + for (int i = 0; i < result.Length; i++) + { + int x = i & 511; + int y = i >> 9; + + result[i] = new( + (byte)((x * 17) + (y * 13)), + (byte)((x * 29) + (y * 7)), + (byte)((x * 11) + (y * 23)), + (byte)(255 - ((x * 3) + (y * 5)))); + } + + return result; + } + + /// + /// Preserves the original direct-mapped coarse cache implementation for side-by-side benchmarks. + /// + private unsafe struct LegacyCoarseCache : IColorIndexCache + { + private const int IndexRBits = 5; + private const int IndexGBits = 5; + private const int IndexBBits = 5; + private const int IndexABits = 6; + private const int IndexRCount = 1 << IndexRBits; + private const int IndexGCount = 1 << IndexGBits; + private const int IndexBCount = 1 << IndexBBits; + private const int IndexACount = 1 << IndexABits; + private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; + + private readonly IMemoryOwner binsOwner; + private readonly short* binsPointer; + private MemoryHandle binsHandle; + + private LegacyCoarseCache(MemoryAllocator allocator) + { + this.binsOwner = allocator.Allocate(TotalBins); + this.binsOwner.GetSpan().Fill(-1); + this.binsHandle = this.binsOwner.Memory.Pin(); + this.binsPointer = (short*)this.binsHandle.Pointer; + } + + public static LegacyCoarseCache Create(MemoryAllocator allocator) => new(allocator); + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool TryAdd(Rgba32 color, short value) + { + this.binsPointer[GetCoarseIndex(color)] = value; + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public readonly bool TryGetValue(Rgba32 color, out short value) + { + value = this.binsPointer[GetCoarseIndex(color)]; + return value > -1; + } + + public readonly void Clear() => this.binsOwner.GetSpan().Fill(-1); + + public void Dispose() + { + this.binsHandle.Dispose(); + this.binsOwner.Dispose(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetCoarseIndex(Rgba32 color) + { + int rIndex = color.R >> (8 - IndexRBits); + int gIndex = color.G >> (8 - IndexGBits); + int bIndex = color.B >> (8 - IndexBBits); + int aIndex = color.A >> (8 - IndexABits); + + return (aIndex * IndexRCount * IndexGCount * IndexBCount) + + (rIndex * IndexGCount * IndexBCount) + + (gIndex * IndexBCount) + + bIndex; + } + } + + /// + /// Preserves the uncached path for exact-cache comparison benchmarks. + /// + private readonly struct UncachedCache : IColorIndexCache + { + public static UncachedCache Create(MemoryAllocator allocator) => default; + + public bool TryAdd(Rgba32 color, short value) => true; + + public bool TryGetValue(Rgba32 color, out short value) + { + value = -1; + return false; + } + + public void Clear() + { + } + + public void Dispose() + { + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 4ebcbc13b6..eef8d5ba84 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -680,7 +680,7 @@ public partial class PngEncoderTests PaletteQuantizer quantizer = new( palette.Select(Color.FromPixel).ToArray(), - new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Hybrid }); + new QuantizerOptions { ColorMatchingMode = ColorMatchingMode.Exact }); using MemoryStream ms = new(); image.Save(ms, new PngEncoder diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index f2a4b079b5..07e9a4b0d6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -75,4 +76,161 @@ public class PaletteQuantizerTests IQuantizer quantizer = KnownQuantizers.Werner; Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } + + [Fact] + public void ExactColorMatchingMatchesUncachedAfterCacheOverflow() + { + Rgba32[] palette = + [ + new Rgba32(0, 0, 0), + new Rgba32(7, 0, 0) + ]; + + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int i = 0; i < AccurateCache.Capacity; i++) + { + cachedExact.GetClosestColor(CreateOverflowFillerColor(i), out _); + } + + Rgba32 first = new(1, 0, 0); + Rgba32 second = new(6, 0, 0); + + AssertMatchesUncached(exact, cachedExact, first); + AssertMatchesUncached(exact, cachedExact, second); + } + + [Fact] + public void ExactColorMatchingMatchesUncachedAcrossManyProbeBinsAfterRepeatedEviction() + { + Rgba32[] palette = CreateGrayscalePalette(256); + + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int i = 0; i < AccurateCache.Capacity * 2; i++) + { + cachedExact.GetClosestColor(CreateEvictionFillerColor(i), out _); + } + + for (int i = 0; i < AccurateCache.Capacity; i++) + { + AssertMatchesUncached(exact, cachedExact, CreateEvictionProbeColor(i)); + } + } + + [Fact] + public void ExactColorMatchingMatchesUncachedForDitherStressColorSequence() + { + Rgba32[] palette = CreateGrayscalePalette(16); + + using Image source = CreateDitherStressImage(); + using PixelMap exact = CreatePixelMap(palette); + using PixelMap cachedExact = CreatePixelMap(palette); + + for (int y = 0; y < source.Height; y++) + { + for (int x = 0; x < source.Width; x++) + { + AssertMatchesUncached(exact, cachedExact, source[x, y]); + } + } + } + + // Split the first 512 integers across R and G so the warmup loop produces 512 distinct exact colors: + // the low 8 bits go into R, and the ninth bit spills into G once R wraps after 255. + // Keeping B fixed and G offset away from zero also avoids accidentally probing the red-axis test colors below. + private static Rgba32 CreateOverflowFillerColor(int i) + => new((byte)i, (byte)(16 + (i >> 8)), 32); + + // Treat i as three packed 5-bit coordinates and expand each coordinate back to an 8-bit channel by + // shifting left by 3. That lands on the lower edge of each 5-bit coarse bucket, giving the test a + // deterministic way to fill many distinct coarse buckets before probing nearby exact colors. + private static Rgba32 CreateEvictionFillerColor(int i) + { + byte r = (byte)((i & 31) << 3); + byte g = (byte)(((i >> 5) & 31) << 3); + byte b = (byte)(((i >> 10) & 31) << 3); + return new(r, g, b); + } + + // Reconstruct the same 5-bit RGB bucket coordinates used by CreateEvictionFillerColor, then set the + // low 3 bits in each channel to 0b111. That keeps the probe inside the same coarse bucket while making + // it a different exact color, which is the shape that used to expose coarse-fallback false hits. + private static Rgba32 CreateEvictionProbeColor(int i) + { + byte r = (byte)(((i & 31) << 3) | 0x07); + byte g = (byte)((((i >> 5) & 31) << 3) | 0x07); + byte b = (byte)((((i >> 10) & 31) << 3) | 0x07); + return new(r, g, b); + } + + private static PixelMap CreatePixelMap(Rgba32[] palette) + where TCache : struct, IColorIndexCache + => new EuclideanPixelMap(Configuration.Default, palette); + + private static void AssertMatchesUncached(PixelMap exact, PixelMap cachedExact, Rgba32 color) + { + int exactIndex = exact.GetClosestColor(color, out Rgba32 exactMatch); + int cachedIndex = cachedExact.GetClosestColor(color, out Rgba32 cachedMatch); + + Assert.Equal(exactIndex, cachedIndex); + Assert.Equal(exactMatch, cachedMatch); + } + + private static Rgba32[] CreateGrayscalePalette(int count) + { + Rgba32[] palette = new Rgba32[count]; + for (int i = 0; i < count; i++) + { + byte value = count == 1 ? (byte)0 : (byte)((i * 255) / (count - 1)); + palette[i] = new Rgba32(value, value, value); + } + + return palette; + } + + // Generate a deterministic pseudo-image where each channel uses a different x/y slope. + // Neighboring pixels stay correlated, like real image content, but the combined RGB values + // churn heavily enough that exact repeats are rare. That makes this a useful stress input + // for verifying cached exact matching against an uncached baseline under dither-like access. + private static Image CreateDitherStressImage() + { + Image image = new(192, 96); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = new Rgba32( + (byte)((x * 17) + (y * 13)), + (byte)((x * 29) + (y * 7)), + (byte)((x * 11) + (y * 23))); + } + } + + return image; + } + + private readonly struct UncachedCache : IColorIndexCache + { + public static UncachedCache Create(MemoryAllocator allocator) => default; + + public bool TryAdd(Rgba32 color, short value) => true; + + public bool TryGetValue(Rgba32 color, out short value) + { + value = -1; + return false; + } + + public void Clear() + { + } + + public void Dispose() + { + } + } } From f0ce591a64381458cb276231a6c30c400fafe658 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 22:14:58 +1000 Subject: [PATCH 8/9] Rename quantizer and update tests --- src/ImageSharp/Advanced/AotCompilerTools.cs | 8 +- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 6 +- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 2 +- .../Formats/Tiff/TiffEncoderCore.cs | 2 +- .../Quantization/QuantizeExtensions.cs | 8 +- src/ImageSharp/Processing/KnownQuantizers.cs | 8 +- ...eQuantizer.cs => HexadecatreeQuantizer.cs} | 22 +- ...l}.cs => HexadecatreeQuantizer{TPixel}.cs} | 354 ++++++++++-------- .../Codecs/Png/EncodeIndexedPng.cs | 12 +- .../Formats/Bmp/BmpEncoderTests.cs | 8 +- .../Formats/GeneralFormatTests.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 6 +- .../Formats/WebP/WebpEncoderTests.cs | 10 +- ...Tests.cs => HexadecatreeQuantizerTests.cs} | 20 +- .../Processors/Quantization/QuantizerTests.cs | 26 +- .../Quantization/QuantizedImageTests.cs | 14 +- ...tColor_WithHexadecatreeQuantizer_rgb32.bmp | 3 + ...zed_Encode_Artifacts_Rgba32_issue_2469.png | 4 +- ...Bike_HexadecatreeQuantizer_ErrorDither.png | 3 + ...ox_Bike_HexadecatreeQuantizer_NoDither.png | 3 + ...e_HexadecatreeQuantizer_OrderedDither.png} | 0 ...InBox_Bike_OctreeQuantizer_ErrorDither.png | 3 - ...ionInBox_Bike_OctreeQuantizer_NoDither.png | 3 - ...ke_WebSafePaletteQuantizer_ErrorDither.png | 4 +- ..._Bike_WebSafePaletteQuantizer_NoDither.png | 4 +- ..._WebSafePaletteQuantizer_OrderedDither.png | 4 +- ...ike_WernerPaletteQuantizer_ErrorDither.png | 4 +- ...x_Bike_WernerPaletteQuantizer_NoDither.png | 4 +- ...e_WernerPaletteQuantizer_OrderedDither.png | 4 +- ...tionInBox_Bike_WuQuantizer_ErrorDither.png | 4 +- ...izationInBox_Bike_WuQuantizer_NoDither.png | 4 +- ...tial_HexadecatreeQuantizer_ErrorDither.png | 3 + ...Partial_HexadecatreeQuantizer_NoDither.png | 3 + ...al_HexadecatreeQuantizer_OrderedDither.png | 3 + ...oraPartial_OctreeQuantizer_ErrorDither.png | 3 - ...iphoraPartial_OctreeQuantizer_NoDither.png | 3 - ...aPartial_OctreeQuantizer_OrderedDither.png | 3 - ...al_WebSafePaletteQuantizer_ErrorDither.png | 4 +- ...rtial_WebSafePaletteQuantizer_NoDither.png | 4 +- ..._WebSafePaletteQuantizer_OrderedDither.png | 4 +- ...ial_WernerPaletteQuantizer_ErrorDither.png | 4 +- ...artial_WernerPaletteQuantizer_NoDither.png | 4 +- ...l_WernerPaletteQuantizer_OrderedDither.png | 4 +- ...liphoraPartial_WuQuantizer_ErrorDither.png | 4 +- ...CalliphoraPartial_WuQuantizer_NoDither.png | 4 +- ...phoraPartial_WuQuantizer_OrderedDither.png | 4 +- ...HexadecatreeQuantizer_ErrorDither_0.25.png | 3 + ..._HexadecatreeQuantizer_ErrorDither_0.5.png | 3 + ...HexadecatreeQuantizer_ErrorDither_0.75.png | 3 + ...id_HexadecatreeQuantizer_ErrorDither_0.png | 3 + ...id_HexadecatreeQuantizer_ErrorDither_1.png | 3 + ...adecatreeQuantizer_OrderedDither_0.25.png} | 0 ...xadecatreeQuantizer_OrderedDither_0.5.png} | 0 ...adecatreeQuantizer_OrderedDither_0.75.png} | 0 ..._HexadecatreeQuantizer_OrderedDither_0.png | 3 + ...HexadecatreeQuantizer_OrderedDither_1.png} | 0 ...david_OctreeQuantizer_ErrorDither_0.25.png | 3 - ..._david_OctreeQuantizer_ErrorDither_0.5.png | 3 - ...david_OctreeQuantizer_ErrorDither_0.75.png | 3 - ...le_david_OctreeQuantizer_ErrorDither_0.png | 3 - ...le_david_OctreeQuantizer_ErrorDither_1.png | 3 - ..._david_OctreeQuantizer_OrderedDither_0.png | 3 - ...WernerPaletteQuantizer_OrderedDither_1.png | 4 +- ...ale_david_WuQuantizer_ErrorDither_0.25.png | 4 +- ...cale_david_WuQuantizer_ErrorDither_0.5.png | 4 +- ...ale_david_WuQuantizer_ErrorDither_0.75.png | 4 +- ...gScale_david_WuQuantizer_ErrorDither_0.png | 4 +- ...gScale_david_WuQuantizer_ErrorDither_1.png | 4 +- ...e_david_WuQuantizer_OrderedDither_0.25.png | 4 +- ...e_david_WuQuantizer_OrderedDither_0.75.png | 4 +- ...cale_david_WuQuantizer_OrderedDither_0.png | 4 +- ...cale_david_WuQuantizer_OrderedDither_1.png | 4 +- ...ike_HexadecatreeQuantizer_ErrorDither.png} | 0 ...n_Bike_HexadecatreeQuantizer_NoDither.png} | 0 ...e_HexadecatreeQuantizer_OrderedDither.png} | 0 ...e_WernerPaletteQuantizer_OrderedDither.png | 4 +- ...ntization_Bike_WuQuantizer_ErrorDither.png | 4 +- ...Quantization_Bike_WuQuantizer_NoDither.png | 4 +- ...ization_Bike_WuQuantizer_OrderedDither.png | 4 +- ...tial_HexadecatreeQuantizer_ErrorDither.png | 3 + ...Partial_HexadecatreeQuantizer_NoDither.png | 3 + ...l_HexadecatreeQuantizer_OrderedDither.png} | 0 ...oraPartial_OctreeQuantizer_ErrorDither.png | 3 - ...iphoraPartial_OctreeQuantizer_NoDither.png | 3 - ...l_WernerPaletteQuantizer_OrderedDither.png | 4 +- ...liphoraPartial_WuQuantizer_ErrorDither.png | 4 +- ...CalliphoraPartial_WuQuantizer_NoDither.png | 4 +- ...phoraPartial_WuQuantizer_OrderedDither.png | 4 +- 90 files changed, 383 insertions(+), 354 deletions(-) rename src/ImageSharp/Processing/Processors/Quantization/{OctreeQuantizer.cs => HexadecatreeQuantizer.cs} (51%) rename src/ImageSharp/Processing/Processors/Quantization/{OctreeQuantizer{TPixel}.cs => HexadecatreeQuantizer{TPixel}.cs} (54%) rename tests/ImageSharp.Tests/Processing/Processors/Quantization/{OctreeQuantizerTests.cs => HexadecatreeQuantizerTests.cs} (76%) create mode 100644 tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png => ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png} (100%) delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png => ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png} (100%) rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png => ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png} (100%) rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png => ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png} (100%) create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png => ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png} (100%) delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png => ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png} (100%) rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantization_Bike_OctreeQuantizer_NoDither.png => ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png} (100%) rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png => ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png} (100%) create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png create mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png rename tests/Images/External/ReferenceOutput/QuantizerTests/{ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png => ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png} (100%) delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png delete mode 100644 tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 2944b58e5f..0f28b28901 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -54,7 +54,7 @@ internal static class AotCompilerTools /// /// This method doesn't actually do anything but serves an important purpose... /// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception: - /// "Attempting to JIT compile method... OctreeFrameQuantizer.ConstructPalette... while running in aot-only mode." + /// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode." /// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT /// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on /// iOS so it bombs out. @@ -479,7 +479,7 @@ internal static class AotCompilerTools private static void AotCompileQuantizers() where TPixel : unmanaged, IPixel { - AotCompileQuantizer(); + AotCompileQuantizer(); AotCompileQuantizer(); AotCompileQuantizer(); AotCompileQuantizer(); @@ -549,8 +549,8 @@ internal static class AotCompilerTools where TPixel : unmanaged, IPixel where TDither : struct, IDither { - OctreeQuantizer octree = default; - default(TDither).ApplyQuantizationDither, TPixel>(ref octree, default, default, default); + HexadecatreeQuantizer hexadecatree = default; + default(TDither).ApplyQuantizationDither, TPixel>(ref hexadecatree, default, default, default); PaletteQuantizer palette = default; default(TDither).ApplyQuantizationDither, TPixel>(ref palette, default, default, default); diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index e255568047..210c08464a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree; + public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; /// /// Gets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index ccc620d6c4..0bf57c5612 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -116,7 +116,7 @@ internal sealed class BmpEncoderCore this.bitsPerPixel = encoder.BitsPerPixel; // TODO: Use a palette quantizer if supplied. - this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.transparentColorMode = encoder.TransparentColorMode; this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 07c73dcf22..d2883e2811 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -117,7 +117,7 @@ internal sealed class GifEncoderCore if (globalQuantizer is null) { - // Is this a gif with color information. If so use that, otherwise use octree. + // Is this a gif with color information. If so use that, otherwise use the adaptive hexadecatree quantizer. if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) { int ti = GetTransparentIndex(quantized, frameMetadata); @@ -132,12 +132,12 @@ internal sealed class GifEncoderCore } else { - globalQuantizer = new OctreeQuantizer(options); + globalQuantizer = new HexadecatreeQuantizer(options); } } else { - globalQuantizer = new OctreeQuantizer(options); + globalQuantizer = new HexadecatreeQuantizer(options); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index a068613bf4..7859b2c902 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -15,7 +15,7 @@ public class TiffEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree; + public TiffEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree; /// /// Gets the number of bits per pixel. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d7508b02e8..e5e47166e9 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -71,7 +71,7 @@ internal sealed class TiffEncoderCore this.configuration = configuration; this.memoryAllocator = configuration.MemoryAllocator; this.PhotometricInterpretation = encoder.PhotometricInterpretation; - this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.BitsPerPixel = encoder.BitsPerPixel; this.HorizontalPredictor = encoder.HorizontalPredictor; diff --git a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs index bf6d2221f4..b0f5cb7d60 100644 --- a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs @@ -12,12 +12,12 @@ namespace SixLabors.ImageSharp.Processing; public static class QuantizeExtensions { /// - /// Applies quantization to the image using the . + /// Applies quantization to the image using the . /// /// The current image processing context. /// The . public static IImageProcessingContext Quantize(this IImageProcessingContext source) => - Quantize(source, KnownQuantizers.Octree); + Quantize(source, KnownQuantizers.Hexadecatree); /// /// Applies quantization to the image. @@ -29,7 +29,7 @@ public static class QuantizeExtensions source.ApplyProcessor(new QuantizeProcessor(quantizer)); /// - /// Applies quantization to the image using the . + /// Applies quantization to the image using the . /// /// The current image processing context. /// @@ -37,7 +37,7 @@ public static class QuantizeExtensions /// /// The . public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) => - Quantize(source, KnownQuantizers.Octree, rectangle); + Quantize(source, KnownQuantizers.Hexadecatree, rectangle); /// /// Applies quantization to the image. diff --git a/src/ImageSharp/Processing/KnownQuantizers.cs b/src/ImageSharp/Processing/KnownQuantizers.cs index 6fb3c72e81..b63ba597d1 100644 --- a/src/ImageSharp/Processing/KnownQuantizers.cs +++ b/src/ImageSharp/Processing/KnownQuantizers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -6,14 +6,14 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Processing; /// -/// Contains reusable static instances of known quantizing algorithms +/// Contains reusable static instances of known quantizing algorithms. /// public static class KnownQuantizers { /// - /// Gets the adaptive Octree quantizer. Fast with good quality. + /// Gets the adaptive hexadecatree quantizer. Fast with good quality. /// - public static IQuantizer Octree { get; } = new OctreeQuantizer(); + public static IQuantizer Hexadecatree { get; } = new HexadecatreeQuantizer(); /// /// Gets the Xiaolin Wu's Color Quantizer which generates high quality output. diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs similarity index 51% rename from src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs rename to src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs index 0a1032bf0d..6b2f5a0131 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer.cs @@ -6,25 +6,29 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// -/// Allows the quantization of images pixels using Octrees. -/// +/// Quantizes images by grouping colors in an adaptive 16-way tree and reducing those groups into a palette. /// -public class OctreeQuantizer : IQuantizer +/// +/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha. Fully opaque mid-tone colors +/// use RGB-only routing so more branch resolution is spent on visible color detail, while transparent, dark, and +/// light colors use alpha-aware routing so opacity changes can form their own palette buckets. +/// +public class HexadecatreeQuantizer : IQuantizer { /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// using the default . /// - public OctreeQuantizer() + public HexadecatreeQuantizer() : this(new QuantizerOptions()) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The quantizer options defining quantization rules. - public OctreeQuantizer(QuantizerOptions options) + /// The quantizer options that control palette size, dithering, and transparency behavior. + public HexadecatreeQuantizer(QuantizerOptions options) { Guard.NotNull(options, nameof(options)); this.Options = options; @@ -41,5 +45,5 @@ public class OctreeQuantizer : IQuantizer /// public IQuantizer CreatePixelSpecificQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : unmanaged, IPixel - => new OctreeQuantizer(configuration, options); + => new HexadecatreeQuantizer(configuration, options); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs similarity index 54% rename from src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs rename to src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs index bdf2ba20a8..b5d39d73ec 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs @@ -12,19 +12,28 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// -/// Encapsulates methods to calculate the color palette if an image using an Octree pattern. -/// +/// Quantizes an image by building an adaptive 16-way color tree and reducing it to the requested palette size. /// +/// +/// +/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha, giving the tree up to 16 children +/// per node and letting transparency participate directly in palette construction. +/// +/// +/// Fully opaque mid-tone colors use RGB-only routing so more branch resolution is spent on visible color detail. +/// Transparent, dark, and light colors use alpha-aware routing so opacity changes can form distinct palette buckets. +/// +/// /// The pixel format. #pragma warning disable CA1001 // Types that own disposable fields should be disposable // See https://github.com/dotnet/roslyn-analyzers/issues/6151 -public struct OctreeQuantizer : IQuantizer +public struct HexadecatreeQuantizer : IQuantizer #pragma warning restore CA1001 // Types that own disposable fields should be disposable where TPixel : unmanaged, IPixel { private readonly int maxColors; private readonly int bitDepth; - private readonly Octree octree; + private readonly Hexadecatree tree; private readonly IMemoryOwner paletteOwner; private ReadOnlyMemory palette; private PixelMap? pixelMap; @@ -32,19 +41,19 @@ public struct OctreeQuantizer : IQuantizer private bool isDisposed; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - /// The configuration which allows altering default behavior or extending the library. - /// The quantizer options defining quantization rules. + /// The configuration that provides memory allocation and pixel conversion services. + /// The quantizer options that control palette size, dithering, and transparency behavior. [MethodImpl(InliningOptions.ShortMethod)] - public OctreeQuantizer(Configuration configuration, QuantizerOptions options) + public HexadecatreeQuantizer(Configuration configuration, QuantizerOptions options) { this.Configuration = configuration; this.Options = options; this.maxColors = this.Options.MaxColors; this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8); - this.octree = new Octree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); + this.tree = new Hexadecatree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); this.paletteOwner = configuration.MemoryAllocator.Allocate(this.maxColors, AllocationOptions.Clean); this.pixelMap = default; this.palette = default; @@ -76,23 +85,28 @@ public struct OctreeQuantizer : IQuantizer /// public readonly void AddPaletteColors(in Buffer2DRegion pixelRegion) { - PixelRowDelegate pixelRowDelegate = new(this.octree); - QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( + PixelRowDelegate pixelRowDelegate = new(this.tree); + QuantizerUtilities.AddPaletteColors, TPixel, Rgba32, PixelRowDelegate>( ref Unsafe.AsRef(in this), in pixelRegion, in pixelRowDelegate); } + /// + /// Materializes the final palette from the accumulated tree and prepares the dither lookup map when needed. + /// private void ResolvePalette() { short paletteIndex = 0; Span paletteSpan = this.paletteOwner.GetSpan(); - this.octree.Palettize(paletteSpan, ref paletteIndex); + this.tree.Palettize(paletteSpan, ref paletteIndex); ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; if (this.isDithering) { + // Dithered colors often no longer land on a color that was seen during palette construction, + // so the quantization pass switches to nearest-palette matching once the palette is finalized. this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode); } @@ -108,17 +122,15 @@ public struct OctreeQuantizer : IQuantizer [MethodImpl(InliningOptions.ShortMethod)] public readonly byte GetQuantizedColor(TPixel color, out TPixel match) { - // Due to the addition of new colors by dithering that are not part of the original histogram, - // the octree nodes might not match the correct color. - // In this case, we must use the pixel map to get the closest color. if (this.isDithering) { + // Dithering introduces adjusted colors that were never inserted into the tree, so tree lookup + // is only reliable for the non-dithered path. return (byte)this.pixelMap!.GetClosestColor(color, out match); } ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span); - - int index = this.octree.GetPaletteIndex(color); + int index = this.tree.GetPaletteIndex(color); match = Unsafe.Add(ref paletteRef, (nuint)index); return (byte)index; } @@ -132,34 +144,43 @@ public struct OctreeQuantizer : IQuantizer this.paletteOwner.Dispose(); this.pixelMap?.Dispose(); this.pixelMap = null; - this.octree.Dispose(); + this.tree.Dispose(); } } + /// + /// Forwards source rows into the tree without creating an intermediate buffer. + /// private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate { - private readonly Octree octree; + private readonly Hexadecatree tree; - public PixelRowDelegate(Octree octree) => this.octree = octree; + /// + /// Initializes a new instance of the struct. + /// + /// The destination tree that should accumulate each visited row. + public PixelRowDelegate(Hexadecatree tree) => this.tree = tree; - public void Invoke(ReadOnlySpan row, int rowIndex) => this.octree.AddColors(row); + /// + public void Invoke(ReadOnlySpan row, int rowIndex) => this.tree.AddColors(row); } /// - /// A hexadecatree-based color quantization structure used for fast color distance lookups and palette generation. - /// This tree maintains a fixed pool of nodes (capacity 4096) where each node can have up to 16 children, stores - /// color accumulation data, and supports dynamic node allocation and reduction. It offers near-constant-time insertions - /// and lookups while consuming roughly 240 KB for the node pool. + /// Stores the adaptive 16-way partition tree used to accumulate colors and emit palette entries. /// - internal sealed class Octree : IDisposable + /// + /// The tree uses a fixed node arena for predictable allocation behavior, keeps per-level reducible node lists so + /// deeper buckets can be merged until the palette fits, and caches the previously inserted leaf so repeated colors + /// can be accumulated cheaply. + /// + internal sealed class Hexadecatree : IDisposable { - // The memory allocator. - private readonly MemoryAllocator allocator; - // Pooled buffer for OctreeNodes. - private readonly IMemoryOwner nodesOwner; + private readonly IMemoryOwner nodesOwner; - // Reducible nodes: one per level; we use an integer index; -1 means “no node.” + // One reducible-node head per level. + // Each entry stores a node index, or -1 when that level currently + // has no reducible nodes. private readonly short[] reducibleNodes; // Maximum number of allowable colors. @@ -186,13 +207,13 @@ public struct OctreeQuantizer : IQuantizer private readonly Stack freeIndices = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The configuration which allows altering default behavior or extending the library. - /// The maximum number of significant bits in the image. - /// The maximum number of colors to allow in the palette. - /// The threshold for transparent colors. - public Octree( + /// The configuration that provides the backing memory allocator. + /// The number of levels to descend before forcing leaves. + /// The maximum number of palette entries the reduced tree may retain. + /// The alpha threshold below which generated palette entries become fully transparent. + public Hexadecatree( Configuration configuration, int maxColorBits, int maxColors, @@ -207,8 +228,7 @@ public struct OctreeQuantizer : IQuantizer // Allocate a conservative buffer for nodes. const int capacity = 4096; - this.allocator = configuration.MemoryAllocator; - this.nodesOwner = this.allocator.Allocate(capacity, AllocationOptions.Clean); + this.nodesOwner = configuration.MemoryAllocator.Allocate(capacity, AllocationOptions.Clean); // Create the reducible nodes array (one per level 0 .. maxColorBits-1). this.reducibleNodes = new short[this.maxColorBits]; @@ -216,24 +236,24 @@ public struct OctreeQuantizer : IQuantizer // Reserve index 0 for the root. this.rootIndex = 0; - ref OctreeNode root = ref this.Nodes[this.rootIndex]; + ref Node root = ref this.Nodes[this.rootIndex]; root.Initialize(0, this.maxColorBits, this, this.rootIndex); } /// - /// Gets or sets the number of leaves in the tree. + /// Gets or sets the number of leaf nodes currently representing palette buckets. /// public int Leaves { get; set; } /// - /// Gets the full collection of nodes as a span. + /// Gets the underlying node arena. /// - internal Span Nodes => this.nodesOwner.Memory.Span; + internal Span Nodes => this.nodesOwner.Memory.Span; /// - /// Adds a span of colors to the octree. + /// Adds a row of colors to the tree. /// - /// A span of color values to be added. + /// The colors to accumulate. public void AddColors(ReadOnlySpan row) { for (int x = 0; x < row.Length; x++) @@ -243,12 +263,13 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Add a color to the Octree. + /// Adds a single color sample to the tree. /// - /// The color to add. + /// The color to accumulate. private void AddColor(Rgba32 color) { - // Ensure that the tree is not already full. + // Once the node arena is full and there are no recycled slots available, keep collapsing + // reducible leaves until the tree is small enough to make forward progress again. if (this.nextNode >= this.Nodes.Length && this.freeIndices.Count == 0) { while (this.Leaves > this.maxColors) @@ -257,32 +278,32 @@ public struct OctreeQuantizer : IQuantizer } } - // If the color is the same as the previous color, increment the node. - // Otherwise, add a new node. + // Scanlines often contain long runs of the same color. Caching the previous leaf lets those + // repeats skip the tree walk and just bump the accumulated sums in place. if (this.previousColor.Equals(color)) { if (this.previousNode == -1) { this.previousColor = color; - OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); } else { - OctreeNode.Increment(this.previousNode, color, this); + Node.Increment(this.previousNode, color, this); } } else { this.previousColor = color; - OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); + Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); } } /// - /// Construct the palette from the octree. + /// Reduces the tree to the requested palette size and emits the final palette entries. /// - /// The palette to construct. - /// The current palette index. + /// The destination palette span. + /// The running palette index. public void Palettize(Span palette, ref short paletteIndex) { while (this.Leaves > this.maxColors) @@ -294,48 +315,45 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Get the palette index for the passed color. + /// Gets the palette index selected by the tree for the supplied color. /// - /// The color to get the palette index for. - /// The . + /// The color to resolve. + /// The palette index represented by the best matching leaf in the reduced tree. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetPaletteIndex(TPixel color) => this.Nodes[this.rootIndex].GetPaletteIndex(color.ToRgba32(), 0, this); /// - /// Track the previous node and color. + /// Records the most recently touched leaf so repeated colors can bypass another descent. /// - /// The node index. + /// The leaf node index. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void TrackPrevious(int nodeIndex) => this.previousNode = nodeIndex; /// - /// Reduce the depth of the tree. + /// Collapses the deepest currently reducible node into a single leaf. /// private void Reduce() { - // Find the deepest level containing at least one reducible node int index = this.maxColorBits - 1; while ((index > 0) && (this.reducibleNodes[index] == -1)) { index--; } - // Reduce the node most recently added to the list at level 'index' - ref OctreeNode node = ref this.Nodes[this.reducibleNodes[index]]; + ref Node node = ref this.Nodes[this.reducibleNodes[index]]; this.reducibleNodes[index] = node.NextReducibleIndex; - - // Decrement the leaf count after reducing the node node.Reduce(this); - // And just in case I've reduced the last color to be added, and the next color to - // be added is the same, invalidate the previousNode... + // If the last inserted leaf was merged away, the next repeated color must walk the tree again. this.previousNode = -1; } - // Allocate a new OctreeNode from the pooled buffer. - // First check the freeIndices stack. + /// + /// Allocates a node index from the free list or from the unused tail of the arena. + /// + /// The allocated node index, or -1 if no node can be allocated. internal short AllocateNode() { if (this.freeIndices.Count > 0) @@ -354,9 +372,9 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Free a node index, making it available for re-allocation. + /// Returns a node index to the free list. /// - /// The index to free. + /// The node index to recycle. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void FreeNode(short index) { @@ -367,8 +385,11 @@ public struct OctreeQuantizer : IQuantizer /// public void Dispose() => this.nodesOwner.Dispose(); + /// + /// Represents one node in the hexadecatree node arena. + /// [StructLayout(LayoutKind.Sequential)] - internal struct OctreeNode + internal struct Node { public bool Leaf; public int PixelCount; @@ -380,19 +401,21 @@ public struct OctreeQuantizer : IQuantizer public short NextReducibleIndex; private InlineArray16 children; + /// + /// Gets the 16 child slots for this node. + /// [UnscopedRef] public Span Children => this.children; /// - /// Initialize the . + /// Initializes a node either as a leaf or as a reducible interior node. /// - /// The level of the node. - /// The number of significant color bits in the image. - /// The parent octree. - /// The index of the node. - public void Initialize(int level, int colorBits, Octree octree, short index) + /// The depth of the node being initialized. + /// The maximum tree depth. + /// The owning tree. + /// The node index in the arena. + public void Initialize(int level, int colorBits, Hexadecatree tree, short index) { - // Construct the new node. this.Leaf = level == colorBits; this.Red = 0; this.Green = 0; @@ -401,76 +424,73 @@ public struct OctreeQuantizer : IQuantizer this.PixelCount = 0; this.PaletteIndex = 0; this.NextReducibleIndex = -1; - - // Always clear the Children array. this.Children.Fill(-1); if (this.Leaf) { - octree.Leaves++; + tree.Leaves++; } else { - // Add this node to the reducible nodes list for its level. - this.NextReducibleIndex = octree.reducibleNodes[level]; - octree.reducibleNodes[level] = index; + // Track reducible nodes per level so palette reduction can always collapse the deepest + // buckets first without scanning the entire arena. + this.NextReducibleIndex = tree.reducibleNodes[level]; + tree.reducibleNodes[level] = index; } } /// - /// Add a color to the Octree. + /// Descends the tree for the supplied color, allocating nodes as needed until a leaf is reached. /// - /// The node index. - /// The color to add. - /// The number of significant color bits in the image. - /// The level of the node. - /// The parent octree. - public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Octree octree) + /// The current node index. + /// The color being accumulated. + /// The maximum tree depth. + /// The current depth. + /// The owning tree. + public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Hexadecatree tree) { - ref OctreeNode node = ref octree.Nodes[nodeIndex]; + ref Node node = ref tree.Nodes[nodeIndex]; if (node.Leaf) { - Increment(nodeIndex, color, octree); - octree.TrackPrevious(nodeIndex); + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; } - else - { - int index = GetColorIndex(color, level); - short childIndex; - Span children = node.Children; - childIndex = children[index]; + int index = GetColorIndex(color, level); + Span children = node.Children; + short childIndex = children[index]; + if (childIndex == -1) + { + childIndex = tree.AllocateNode(); if (childIndex == -1) { - childIndex = octree.AllocateNode(); - - if (childIndex == -1) - { - // No room in the tree, so increment the count and return. - Increment(nodeIndex, color, octree); - octree.TrackPrevious(nodeIndex); - return; - } - - ref OctreeNode child = ref octree.Nodes[childIndex]; - child.Initialize(level + 1, colorBits, octree, childIndex); - children[index] = childIndex; + // If the arena is exhausted and no node can be reclaimed yet, fall back to + // accumulating into the current node instead of failing the insert outright. + Increment(nodeIndex, color, tree); + tree.TrackPrevious(nodeIndex); + return; } - AddColor(childIndex, color, colorBits, level + 1, octree); + ref Node child = ref tree.Nodes[childIndex]; + child.Initialize(level + 1, colorBits, tree, childIndex); + children[index] = childIndex; } + + // Keep descending until we reach the leaf bucket that should accumulate this sample. + AddColor(childIndex, color, colorBits, level + 1, tree); } /// - /// Increment the color components of this node. + /// Adds the supplied color sample to an existing node's running sums. /// - /// The node index. - /// The color to increment by. - /// The parent octree. - public static void Increment(int nodeIndex, Rgba32 color, Octree octree) + /// The node index to update. + /// The color sample being accumulated. + /// The owning tree. + public static void Increment(int nodeIndex, Rgba32 color, Hexadecatree tree) { - ref OctreeNode node = ref octree.Nodes[nodeIndex]; + ref Node node = ref tree.Nodes[nodeIndex]; node.PixelCount++; node.Red += color.R; node.Green += color.G; @@ -479,10 +499,10 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Reduce this node by ensuring its children are all reduced (i.e. leaves) and then merging their data. + /// Merges all child nodes into this node and turns it into a leaf. /// - /// The parent octree. - public void Reduce(Octree octree) + /// The owning tree. + public void Reduce(Hexadecatree tree) { // If already a leaf, do nothing. if (this.Leaf) @@ -492,25 +512,27 @@ public struct OctreeQuantizer : IQuantizer // Now merge the (presumably reduced) children. int pixelCount = 0; - int sumRed = 0, sumGreen = 0, sumBlue = 0, sumAlpha = 0; + int sumRed = 0; + int sumGreen = 0; + int sumBlue = 0; + int sumAlpha = 0; Span children = this.Children; + for (int i = 0; i < children.Length; i++) { short childIndex = children[i]; if (childIndex != -1) { - ref OctreeNode child = ref octree.Nodes[childIndex]; + ref Node child = ref tree.Nodes[childIndex]; int pixels = child.PixelCount; - sumRed += child.Red; sumGreen += child.Green; sumBlue += child.Blue; sumAlpha += child.Alpha; pixelCount += pixels; - // Free the child immediately. children[i] = -1; - octree.FreeNode(childIndex); + tree.FreeNode(childIndex); } } @@ -529,16 +551,16 @@ public struct OctreeQuantizer : IQuantizer } this.Leaf = true; - octree.Leaves++; + tree.Leaves++; } /// - /// Traverse the tree to construct the palette. + /// Traverses the reduced tree and emits one palette color per leaf. /// - /// The parent octree. - /// The palette to construct. - /// The current palette index. - public void ConstructPalette(Octree octree, Span palette, ref short paletteIndex) + /// The owning tree. + /// The destination palette span. + /// The running palette index. + public void ConstructPalette(Hexadecatree tree, Span palette, ref short paletteIndex) { if (this.Leaf) { @@ -549,13 +571,12 @@ public struct OctreeQuantizer : IQuantizer Vector4.Zero, new Vector4(255)); - if (vector.W < octree.transparencyThreshold255) + if (vector.W < tree.transparencyThreshold255) { vector = Vector4.Zero; } palette[paletteIndex] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W)); - this.PaletteIndex = paletteIndex++; } else @@ -566,19 +587,20 @@ public struct OctreeQuantizer : IQuantizer int childIndex = children[i]; if (childIndex != -1) { - octree.Nodes[childIndex].ConstructPalette(octree, palette, ref paletteIndex); + tree.Nodes[childIndex].ConstructPalette(tree, palette, ref paletteIndex); } } } } /// - /// Get the palette index for the passed color. + /// Resolves the palette index represented by this node for the supplied color. /// - /// The color to get the palette index for. - /// The level of the node. - /// The parent octree. - public int GetPaletteIndex(Rgba32 color, int level, Octree octree) + /// The color to resolve. + /// The current tree depth. + /// The owning tree. + /// The palette index for the best reachable leaf, or -1 if no leaf can be reached. + public int GetPaletteIndex(Rgba32 color, int level, Hexadecatree tree) { if (this.Leaf) { @@ -590,15 +612,16 @@ public struct OctreeQuantizer : IQuantizer int childIndex = children[colorIndex]; if (childIndex != -1) { - return octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); + return tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); } + // After reductions the exact branch can disappear, so fall back to the first reachable descendant leaf. for (int i = 0; i < children.Length; i++) { childIndex = children[i]; if (childIndex != -1) { - int childPaletteIndex = octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); + int childPaletteIndex = tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); if (childPaletteIndex != -1) { return childPaletteIndex; @@ -610,37 +633,35 @@ public struct OctreeQuantizer : IQuantizer } /// - /// Gets the color index at the given level. + /// Computes the child slot for a color at the supplied tree level. /// - /// The color to get the index for. - /// The level to get the index at. + /// The color being routed. + /// The tree depth whose bit plane should be sampled. + /// The child slot index for the color at the supplied level. + /// + /// For fully opaque mid-tone colors the tree ignores alpha and routes on RGB only, preserving more branch + /// resolution for visible color detail. For transparent, dark, and light colors it includes alpha as the + /// most significant routing bit so opacity changes can form their own branches. + /// public static int GetColorIndex(Rgba32 color, int level) { - // Determine how many bits to shift based on the current tree level. - // At level 0, shift = 7; as level increases, the shift decreases. + // Sample one bit plane per level, starting at the most significant bit and moving downward. int shift = 7 - level; byte mask = (byte)(1 << shift); - // Compute the luminance of the RGB components using the BT.709 standard. - // This gives a measure of brightness for the color. + // Use BT.709 luminance as a cheap brightness estimate for deciding whether alpha carries + // useful information at this level for fully opaque colors. int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B); - // Define thresholds for determining when to include the alpha bit in the index. - // The thresholds are scaled according to the current level. - // 128 is the midpoint of the 8-bit range (0–255), so shifting it right by 'level' - // produces a threshold that scales with the color cube subdivision. + // Scale the brightness thresholds with depth so deeper levels become stricter about when + // to spend a branch bit on alpha instead of RGB detail. int darkThreshold = 128 >> level; - - // The light threshold is set symmetrically: 255 minus the scaled midpoint. int lightThreshold = 255 - (128 >> level); - // If the pixel is fully opaque and its brightness falls between the dark and light thresholds, - // ignore the alpha channel to maximize RGB resolution. - // Otherwise (if the pixel is dark, light, or semi-transparent), include the alpha bit - // to preserve any gradient that may be present. if (color.A == 255 && luminance > darkThreshold && luminance < lightThreshold) { - // Extract one bit each from R, G, and B channels and combine them into a 3-bit index. + // Fully opaque mid-tone colors route on RGB only, which preserves more visible color + // resolution because alpha would contribute no extra separation here. int rBits = ((color.R & mask) >> shift) << 2; int gBits = ((color.G & mask) >> shift) << 1; int bBits = (color.B & mask) >> shift; @@ -648,7 +669,8 @@ public struct OctreeQuantizer : IQuantizer } else { - // Extract one bit from each channel including alpha (alpha becomes the most significant bit). + // Transparent, dark, and light colors include alpha as the high routing bit so opacity + // changes can form distinct buckets alongside RGB differences. int aBits = ((color.A & mask) >> shift) << 3; int rBits = ((color.R & mask) >> shift) << 2; int gBits = ((color.G & mask) >> shift) << 1; diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs index 125b42680d..69779731be 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/EncodeIndexedPng.cs @@ -40,19 +40,19 @@ public class EncodeIndexedPng this.bmpCore.Dispose(); } - [Benchmark(Baseline = true, Description = "ImageSharp Octree Png")] - public void PngCoreOctree() + [Benchmark(Baseline = true, Description = "ImageSharp Hexadecatree Png")] + public void PngCoreHexadecatree() { using MemoryStream memoryStream = new(); - PngEncoder options = new() { Quantizer = KnownQuantizers.Octree }; + PngEncoder options = new() { Quantizer = KnownQuantizers.Hexadecatree }; this.bmpCore.SaveAsPng(memoryStream, options); } - [Benchmark(Description = "ImageSharp Octree NoDither Png")] - public void PngCoreOctreeNoDither() + [Benchmark(Description = "ImageSharp Hexadecatree NoDither Png")] + public void PngCoreHexadecatreeNoDither() { using MemoryStream memoryStream = new(); - PngEncoder options = new() { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; + PngEncoder options = new() { Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 5ebcc8bb96..6bd7e0103f 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -292,7 +292,7 @@ public class BmpEncoderTests [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32)] - public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider provider) + public void Encode_8BitColor_WithHexadecatreeQuantizer(TestImageProvider provider) where TPixel : unmanaged, IPixel { if (!TestEnvironment.Is64BitProcess) @@ -304,7 +304,7 @@ public class BmpEncoderTests BmpEncoder encoder = new() { BitsPerPixel = BmpBitsPerPixel.Bit8, - Quantizer = new OctreeQuantizer() + Quantizer = new HexadecatreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); @@ -385,7 +385,7 @@ public class BmpEncoderTests { BitsPerPixel = bitsPerPixel, SupportTransparency = false, - Quantizer = KnownQuantizers.Octree + Quantizer = KnownQuantizers.Hexadecatree }; image.SaveAsBmp(reencodedStream, encoder); reencodedStream.Seek(0, SeekOrigin.Begin); @@ -478,7 +478,7 @@ public class BmpEncoderTests { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency, - Quantizer = quantizer ?? KnownQuantizers.Octree + Quantizer = quantizer ?? KnownQuantizers.Hexadecatree }; // Does DebugSave & load reference CompareToReferenceInput(): diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 072b04fa0d..2b91c4dbfa 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -125,7 +125,7 @@ public class GeneralFormatTests public static readonly TheoryData QuantizerNames = new() { - nameof(KnownQuantizers.Octree), + nameof(KnownQuantizers.Hexadecatree), nameof(KnownQuantizers.WebSafe), nameof(KnownQuantizers.Werner), nameof(KnownQuantizers.Wu) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 370106ca30..b7bbe4971a 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -115,7 +115,7 @@ public class GifEncoderTests GifEncoder encoder = new() { ColorTableMode = FrameColorTableMode.Global, - Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -124,7 +124,7 @@ public class GifEncoderTests encoder = new GifEncoder { ColorTableMode = FrameColorTableMode.Local, - Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }), + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }), }; provider.Utility.SaveTestOutputFile(image, "gif", encoder, "local"); @@ -191,7 +191,7 @@ public class GifEncoderTests GifEncoder encoder = new() { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors }) + Quantizer = new HexadecatreeQuantizer(new QuantizerOptions { MaxColors = maxColors }) }; image.Save(outStream, encoder); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index f9836ffb13..ee82687167 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -135,9 +135,9 @@ public class WebpEncoderTests // Alpha thresholding is 64/255F. GifEncoder gifEncoder = new() { - Quantizer = new OctreeQuantizer(options) + Quantizer = new HexadecatreeQuantizer(options) }; - provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "octree"); + provider.Utility.SaveTestOutputFile(image, "gif", gifEncoder, "hexadecatree"); gifEncoder = new GifEncoder { @@ -152,8 +152,8 @@ public class WebpEncoderTests }; using Image cloned1 = image.Clone(); - cloned1.Mutate(c => c.Quantize(new OctreeQuantizer(options))); - provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "octree"); + cloned1.Mutate(c => c.Quantize(new HexadecatreeQuantizer(options))); + provider.Utility.SaveTestOutputFile(cloned1, "webp", encoder, "hexadecatree"); using Image cloned2 = image.Clone(); cloned2.Mutate(c => c.Quantize(new WuQuantizer(options))); @@ -162,7 +162,7 @@ public class WebpEncoderTests // Now blend the images with a blue background and save as webp. using Image background1 = new(image.Width, image.Height, Color.White.ToPixel()); background1.Mutate(c => c.DrawImage(cloned1, 1)); - provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "octree-blended"); + provider.Utility.SaveTestOutputFile(background1, "webp", encoder, "hexadecatree-blended"); using Image background2 = new(image.Width, image.Height, Color.White.ToPixel()); background2.Mutate(c => c.DrawImage(cloned2, 1)); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs similarity index 76% rename from tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs rename to tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs index c9f3daf0f2..4ef2159305 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/HexadecatreeQuantizerTests.cs @@ -8,37 +8,37 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization; [Trait("Category", "Processors")] -public class OctreeQuantizerTests +public class HexadecatreeQuantizerTests { [Fact] - public void OctreeQuantizerConstructor() + public void HexadecatreeQuantizerConstructor() { QuantizerOptions expected = new() { MaxColors = 128 }; - OctreeQuantizer quantizer = new(expected); + HexadecatreeQuantizer quantizer = new(expected); Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); expected = new QuantizerOptions { Dither = null }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); Assert.Null(quantizer.Options.Dither); expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; - quantizer = new OctreeQuantizer(expected); + quantizer = new HexadecatreeQuantizer(expected); Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] - public void OctreeQuantizerCanCreateFrameQuantizer() + public void HexadecatreeQuantizerCanCreateFrameQuantizer() { - OctreeQuantizer quantizer = new(); + HexadecatreeQuantizer quantizer = new(); IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); @@ -46,14 +46,14 @@ public class OctreeQuantizerTests Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }); + quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Null(frameQuantizer.Options.Dither); frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); + quantizer = new HexadecatreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index 2ba757c117..00e09d83b0 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -74,15 +74,15 @@ public class QuantizerTests = new() { // Known uses error diffusion by default. - KnownQuantizers.Octree, + KnownQuantizers.Hexadecatree, KnownQuantizers.WebSafe, KnownQuantizers.Werner, KnownQuantizers.Wu, - new OctreeQuantizer(NoDitherOptions), + new HexadecatreeQuantizer(NoDitherOptions), new WebSafePaletteQuantizer(NoDitherOptions), new WernerPaletteQuantizer(NoDitherOptions), new WuQuantizer(NoDitherOptions), - new OctreeQuantizer(OrderedDitherOptions), + new HexadecatreeQuantizer(OrderedDitherOptions), new WebSafePaletteQuantizer(OrderedDitherOptions), new WernerPaletteQuantizer(OrderedDitherOptions), new WuQuantizer(OrderedDitherOptions) @@ -91,52 +91,52 @@ public class QuantizerTests public static readonly TheoryData DitherScaleQuantizers = new() { - new OctreeQuantizer(Diffuser0_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions), new WuQuantizer(Diffuser0_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_25_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions), new WuQuantizer(Diffuser0_25_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_5_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions), new WuQuantizer(Diffuser0_5_ScaleDitherOptions), - new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions), + new HexadecatreeQuantizer(Diffuser0_75_ScaleDitherOptions), new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions), new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions), new WuQuantizer(Diffuser0_75_ScaleDitherOptions), - new OctreeQuantizer(DiffuserDitherOptions), + new HexadecatreeQuantizer(DiffuserDitherOptions), new WebSafePaletteQuantizer(DiffuserDitherOptions), new WernerPaletteQuantizer(DiffuserDitherOptions), new WuQuantizer(DiffuserDitherOptions), - new OctreeQuantizer(Ordered0_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions), new WuQuantizer(Ordered0_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_25_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_25_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions), new WuQuantizer(Ordered0_25_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_5_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_5_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions), new WuQuantizer(Ordered0_5_ScaleDitherOptions), - new OctreeQuantizer(Ordered0_75_ScaleDitherOptions), + new HexadecatreeQuantizer(Ordered0_75_ScaleDitherOptions), new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions), new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions), new WuQuantizer(Ordered0_75_ScaleDitherOptions), - new OctreeQuantizer(OrderedDitherOptions), + new HexadecatreeQuantizer(OrderedDitherOptions), new WebSafePaletteQuantizer(OrderedDitherOptions), new WernerPaletteQuantizer(OrderedDitherOptions), new WuQuantizer(OrderedDitherOptions), diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index d832136a98..e940886520 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -15,12 +15,12 @@ public class QuantizedImageTests { WernerPaletteQuantizer werner = new(); WebSafePaletteQuantizer webSafe = new(); - OctreeQuantizer octree = new(); + HexadecatreeQuantizer hexadecatree = new(); WuQuantizer wu = new(); Assert.NotNull(werner.Options.Dither); Assert.NotNull(webSafe.Options.Dither); - Assert.NotNull(octree.Options.Dither); + Assert.NotNull(hexadecatree.Options.Dither); Assert.NotNull(wu.Options.Dither); using (IQuantizer quantizer = werner.CreatePixelSpecificQuantizer(this.Configuration)) @@ -33,7 +33,7 @@ public class QuantizedImageTests Assert.NotNull(quantizer.Options.Dither); } - using (IQuantizer quantizer = octree.CreatePixelSpecificQuantizer(this.Configuration)) + using (IQuantizer quantizer = hexadecatree.CreatePixelSpecificQuantizer(this.Configuration)) { Assert.NotNull(quantizer.Options.Dither); } @@ -47,7 +47,7 @@ public class QuantizedImageTests [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, true)] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32, false)] - public void OctreeQuantizerYieldsCorrectTransparentPixel( + public void HexadecatreeQuantizerYieldsCorrectTransparentPixel( TestImageProvider provider, bool dither) where TPixel : unmanaged, IPixel @@ -60,7 +60,7 @@ public class QuantizedImageTests options.Dither = null; } - OctreeQuantizer quantizer = new(options); + HexadecatreeQuantizer quantizer = new(options); foreach (ImageFrame frame in image.Frames) { @@ -103,8 +103,8 @@ public class QuantizedImageTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - OctreeQuantizer octreeQuantizer = new(); - IQuantizer quantizer = octreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions { MaxColors = 128 }); + HexadecatreeQuantizer hexadecatreeQuantizer = new(); + IQuantizer quantizer = hexadecatreeQuantizer.CreatePixelSpecificQuantizer(Configuration.Default, new QuantizerOptions { MaxColors = 128 }); ImageFrame frame = image.Frames[0]; quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds); } diff --git a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp new file mode 100644 index 0000000000..f4ae3b9b68 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4 +size 9270 diff --git a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png index 4c78303750..ecf0691cd5 100644 --- a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png +++ b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1af50619f835b4470afac4553445176c121c3c9fa838dff937dcc56ae37941c3 -size 945821 +oid sha256:770061fbb29cd20bc700ce3fc57e38a758c632c3e89de51f5fbee3d5d522539e +size 912635 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 0000000000..d2b62e63ac --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27f6e8e195c4431dc7354a379152d3a8664582bc2bb1c8960ebf4088aa6505e2 +size 248709 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 0000000000..ecbf328d36 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46b5751dc43e9ad5541913cf851ef1b061aa474a95283c712511531202d7015e +size 239326 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index 327366f5b6..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0086044f12a7c58e49733f203af29a8aff2826ea654730274720eada15669254 -size 249163 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png deleted file mode 100644 index 3e0be536e3..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:85ee8479984aa52f837badbc49085c5448597fbfd987438fe25b58bad475e85f -size 239498 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png index 922c2bf9b2..28db1b73ac 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f -size 216030 +oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f +size 215873 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png index 922c2bf9b2..28db1b73ac 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f -size 216030 +oid sha256:af40e835e2f3cf0f406e15248169d058dc1ae69219f2bc5c3413ecea4eb4985f +size 215873 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png index 29c93d14e2..078c75c45a 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e6d91a3ec4f974af675dc360fd5fd623ec8773cdbc88c0a3a6506880838718a -size 226727 +oid sha256:5eb87f02c7924b764bbd2c951047b7204c56a0a1a0d6853a0fb3d30a56ed0184 +size 226633 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png index dbfab2b508..e80b9b8b12 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 -size 220192 +oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3 +size 220099 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png index dbfab2b508..e80b9b8b12 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 -size 220192 +oid sha256:84b55eefd699cd74a1a7de958762b095f196275d2bbde2750936aed9a47f68f3 +size 220099 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png index 86655af42b..ad899553d7 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dbd3189b559941f91dd6e0aa15b34a3e5081477400678c2396c6a66d398876f -size 230883 +oid sha256:c4548abed72e4f833b33eed14392206d7232112fc651becb2351fdee27da5bc1 +size 230687 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png index 82d5e5d592..a30d69d177 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4df5b1bc2c291ec1cf599580d198b447278412576ab998e099cc21110e82b3d -size 263152 +oid sha256:832173c8ca6bd7a8bf417d83b459ccddb541daed1c31539bf596cacea455441d +size 263018 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png index d8a1178adc..e5591852bf 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df63a3d12e2998d5242b64169ac86e3df7ab4be585a80daddc3e3888dfcb7095 -size 262298 +oid sha256:15a6dc485f0c3fd4c9fbbdb6b50437d58d68210790e37f8aab32e66a864e2746 +size 261872 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 0000000000..2e815d4d1c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6eeed563b407940e2a05f068c42b52738e6e1217a1500c9230f7068ca4e9f1e +size 304162 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 0000000000..a8f30e5f5b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dc7dc55af4ef0741a66c569876ad8a2df27164a653baa5bae536e6d121b2c11 +size 300528 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png new file mode 100644 index 0000000000..3ece7ee289 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b65e7903fbfa1ed0682221fdd86c6f0448b3f6a886cae5379720cce881a1f1e +size 305962 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index f29db004f5..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce381c2d261b9b1ca61d8f6e2ff07b992283c327dc6b7cf53c7e5c9317abb7d3 -size 316443 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png deleted file mode 100644 index 284c3a2702..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2bfc23a95df8a88ac6e2777d67f381e800d23647c162a9a97131a101bbb97143 -size 306703 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png deleted file mode 100644 index 5911faa723..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d3f58a108d933ec9ac0a5271af5b65d0a8ab9d521d54e48312b280cc42d71ac -size 322049 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png index 0205626738..03b9a37f73 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be -size 269397 +oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291 +size 268504 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png index 0205626738..03b9a37f73 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a2aae04edebcaca9b95f30963201794887fa0eac954b64c68bfe529b14fa9be -size 269397 +oid sha256:97c277005703b029a9e791e4c9dc3adcbe06054885fdd31e361e8a0a0222a291 +size 268504 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png index 68d91fc437..a1d28a1697 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3e9a338a5ae37c88ce0c348e0b655429220da051db3352779c277bb2dcb441 -size 270622 +oid sha256:b5fa657236e12cbb2a8d2cd747029723a6b3829b475f28626d7647d7b2150918 +size 271579 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png index 324bd92539..eba58870f4 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c -size 284481 +oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8 +size 284056 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png index 324bd92539..eba58870f4 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752760327cc1416c171a920f1e0e95e34eae6d78bd0c7393a3be427bf3c8e55c -size 284481 +oid sha256:532fa8044bb424b451343f89bf7cb954311641056bdbd5685cd7c4fa4ad8f3c8 +size 284056 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png index 52bf2a163f..de30e7574e 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:293459538454e07bc9ea1e9df1fa5b0eb986fde7de42f6c25b43e4c8859bd28a -size 285370 +oid sha256:61ed5f4d77428be46357609d80a66e884dedbb8c255fdcc71d49eeba0eed2bf2 +size 285037 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png index 05be1395ab..c56a90ad27 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90a2b7b3872c6eb1f1f039558d9f6ace92891c86951c801da01ad55b055fd670 -size 316544 +oid sha256:1cc2ef3cb819b5a82e0af32c3ab44aff0206530e291b00bdade58da2ebe4494a +size 308246 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png index d94d57759f..c3ab7996db 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff094e6bafe81e818bcbac69018dcfe29366389dfca0d63d8e05ef42896ffe1d -size 317309 +oid sha256:575c8d81152642fa0eec0ea9901d1941fea58b7686cfaac1d01e0bf59f393c4b +size 308330 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png index e016e3de69..47616cd31a 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee0778aac671365dd0afae06cdcf8f36243bd9815f684b975f83e297bb694e63 -size 323979 +oid sha256:ba295a5ddb79bc61f0be9a28a636fdcc63055c26c46872d407fe20ff785f11ed +size 310415 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png new file mode 100644 index 0000000000..2f939d957d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.25.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png new file mode 100644 index 0000000000..9e8002ad19 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9a87ef109c08411ca61d91ddcf010c272303a17abd90b6ba2204eac021055e5 +size 13665 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png new file mode 100644 index 0000000000..45e770bd96 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.75.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81496d88b42edf4b39ab723d0b5414b56140892f45d30fc2435904b630fa9af5 +size 13886 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png new file mode 100644 index 0000000000..2f939d957d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png new file mode 100644 index 0000000000..c84edd138f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_ErrorDither_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:997e5281abd8cf3a587984ec1b7e31487ec5ddf16326d025124833d536e4ac27 +size 13910 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.25.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.5.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.75.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png new file mode 100644 index 0000000000..2f939d957d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8a5da54da08f7450ffb5b49c412e654215e2c2e72c32919abc78b77dc828f5 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_HexadecatreeQuantizer_OrderedDither_1.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png deleted file mode 100644 index a2fb2a6760..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.25.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png deleted file mode 100644 index 8d99eb49b2..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abfdd1e40c2c1d7fde419bda1da6e534ed989598e790b8ae4de35152a83f77a0 -size 13686 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png deleted file mode 100644 index bf93c39ff8..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.75.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60c28eb1dc3c0416b20cec230917c0e4a70dd2929467bbab796ecbb04fe5a178 -size 13886 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png deleted file mode 100644 index a2fb2a6760..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png deleted file mode 100644 index 457298b544..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a523f097bf3b155f3823c5e400190b5d5e0d4470db7136576472c3257db76600 -size 13909 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png deleted file mode 100644 index a2fb2a6760..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:18a47a6fa0f7949daef6969a847d8bc04deeb16bb482211ec3a958bc63f23f89 -size 13158 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png index 878a36a477..f288b3c8b7 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2bd11fa19fab712b5cd6c2b36d673c7dce904b5032b860d257b00e095e4aadf -size 13432 +oid sha256:dd31b6fc59e1f9f88230d57b39362b76cedd0bd94e15904f69071ba3f465e48d +size 13656 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png index eaf7e8241d..62b1fb0558 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png index 02879b7a38..d2d2e3e4b9 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4ac8b88b317281738d833fc71f52348d9f4f45ea5a1303dd91fdb8b42be4267 -size 13186 +oid sha256:dd738ee2a397bb1ee305f03c70e185dea6f67827dc15b9df1966cfe8c0f28040 +size 13177 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png index ba05094800..c444923a29 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1305d54f2139d4577490317051d6ce94a7fc8dd45b902d87a30fb04098dd4594 -size 13407 +oid sha256:2a2df64f89df17428415932c2ef0028d8ad408b5276264d99e6038b70473ebde +size 13417 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png index eaf7e8241d..62b1fb0558 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png index b16a5a5c7b..74cbbd7581 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3fc3a7ace123c330ea06072eb36dd5d65ed9154d4d0f55a828fc542c8a422c1 -size 13472 +oid sha256:234854be2a3f774a58baf79f20e68c7331b6caff486ab4b1e509a96e2a3d70b9 +size 13455 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png index 6adac16cf5..a9bb2c3163 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35757f2e0831cae2fbd3cc11ffaaae855e853ebaa9a1a5564b6568a5e1c442e9 -size 16031 +oid sha256:ef65ce360293ca5659730747087c15735c15df1143204acb60120a5b68cd7cd4 +size 15905 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png index 5d1030e6b8..ceb9188005 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6679d6d6f7c8b44461956b54654cea71180a2b0d43712d3775e60cbedd90cc82 -size 17520 +oid sha256:6618f169cf4b585979f8e9261af88fe4a61c3c40b453a159cb643cc062a6a9dc +size 17517 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png index eaf7e8241d..62b1fb0558 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:0e88f74acac9cfa1a47a4402aa032975ec4bf698d51e6eb1ae103480e2e10489 +size 13160 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png index 567e5d6a3b..f6cb173678 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5af5d16f875172d73f8426928fc8edaa4a6cab321a968b6c29fca32d0fba0df5 -size 18182 +oid sha256:4acf21f23978c83c9872bb2575ab45e4f0bbc86c8610c99479b1469fc12df5f2 +size 18112 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_NoDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png index 10daff76b2..fd565c0a12 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8ba00e2948337f77d935d98349958c6a520958671e9ec714ff1bfadfb130e72 -size 44622 +oid sha256:4ded8db323023a7c7620bba3b2259a549571442fe0a37883c7755ac69ae9d6d5 +size 44646 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png index 37e5035d86..c342e3a230 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3802cfe67638a24869d6cc9ace1d94460b4c0c26f2c91b12b95fa8f979de64bb -size 101579 +oid sha256:83c8403f5d0e5457721d992c1e6980134e8a65a1f646163a4f091cf34583ca02 +size 101417 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png index e72ea4b246..d07231c185 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf2021eba9edbb2295924f8394472ac0bb237f0c462c39aa32a2074ef15f9acc -size 81771 +oid sha256:e5412b892143bb433804c662750a64a1660b2072520db53d76ec6897c636ec50 +size 81742 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png index 0997945e52..7d2070820d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d11b18946d373b995ecbb449c8c4cfcc7078aad1c8705997bcbf83131acde03 -size 102439 +oid sha256:a88a48586502de786aca0b36341cf6033fb3ec3ce7924ce1e2819fd14791ffe4 +size 102235 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png new file mode 100644 index 0000000000..79711e2ebc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_ErrorDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22920fb2379dee7d12fee52f6a39b8e46e1e99f77b91f879c51bb33a981dfdcb +size 98851 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png new file mode 100644 index 0000000000..fa6d4cb432 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_NoDither.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7137e1b87d317d7e139cde8499deafa89f27bddba146cc5736f9c0566778c5 +size 81609 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png similarity index 100% rename from tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png rename to tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_HexadecatreeQuantizer_OrderedDither.png diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png deleted file mode 100644 index 314a056060..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2236e81d33fcfb50afb9d5fd1a38c5ddf5d33fbb52de1c3204a4a9892fd334ce -size 99084 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png deleted file mode 100644 index 5293046724..0000000000 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4b59097d1507236af2556ae5f2638360b223b7752cd4c8f760bc14673d811d0 -size 81709 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png index b51076bd17..0fe3d30bfb 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a8d9c0d81525d9f37d2f36946939040aea30edfc2b7ec0bf329fb49f6c7d73f -size 69896 +oid sha256:aee197677c3276d4abb8fc027358b38be26462374e364841781626f0aa67e1a4 +size 69769 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png index 7204abff47..ef86e7c48d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4474b94e2d563938e10ec0526e7d94ba06b440db51b910604e752f7f9e814d66 -size 110757 +oid sha256:0b3c8dc7e653ef1846c7359e9a0f719bee91549846f160abb547cd0aab6a8a59 +size 110711 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png index 691623fc88..c65381c052 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58a61c1d9a1d05acd484948c3e5c0496dbc74c0060f5de71741de39eae04ffa8 -size 103875 +oid sha256:4b95721a963def9e82dd32e277ed4594213920d7808ad26696d01e4f8fda842e +size 103855 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png index e80e6c6e81..eb2c2a0c98 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6649918c0394ead13c016a57b6a08561290651bccac88f7f15ba0e29dc5faa4 -size 110422 +oid sha256:cb174c104cdcf35433c98522a1d9d52ccf42e8927e0b59fec3556aeee8b15a47 +size 110505 From c9f7e6ea60c516b887256a32f47aee42a448fc21 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Apr 2026 23:19:38 +1000 Subject: [PATCH 9/9] Add ImageInfo.GetPixelMemorySize; docs & tests --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- src/ImageSharp/ImageInfo.cs | 32 ++++++++++++++++++-- tests/ImageSharp.Tests/ImageInfoTests.cs | 30 ++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 78ceb0b233..3d32c7cdac 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -468,7 +468,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); stream.Read(this.currentLocalColorTable.GetSpan()[..length]); - rawColorTable = this.currentLocalColorTable!.GetSpan()[..length]; + rawColorTable = this.currentLocalColorTable.GetSpan()[..length]; } else if (this.globalColorTable != null) { diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index 0bbd73b63a..d27c4b9330 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -63,8 +63,12 @@ public class ImageInfo public int Height => this.Size.Height; /// - /// Gets the number of frames in the image. + /// Gets the number of frame metadata entries available for the image. /// + /// + /// This value is the same as count and may be 0 when frame + /// metadata was not populated by the decoder. + /// public int FrameCount => this.FrameMetadataCollection.Count; /// @@ -73,8 +77,12 @@ public class ImageInfo public ImageMetadata Metadata { get; } /// - /// Gets the collection of metadata associated with individual image frames. + /// Gets the metadata associated with the decoded image frames, if available. /// + /// + /// For multi-frame formats, decoders populate one entry per decoded frame. For single-frame formats, this + /// collection is typically empty. + /// public IReadOnlyList FrameMetadataCollection { get; } /// @@ -86,4 +94,24 @@ public class ImageInfo /// Gets the bounds of the image. /// public Rectangle Bounds => new(Point.Empty, this.Size); + + /// + /// Gets the total number of bytes required to store the image pixels in memory. + /// + /// + /// This reports the in-memory size of the pixel data represented by this , not the + /// encoded size of the image file. The value is computed from the image dimensions and + /// . When contains decoded frame metadata, the + /// per-frame size is multiplied by that count. Otherwise, the value is the in-memory size of the single + /// image frame represented by this . + /// + /// The total number of bytes required to store the image pixels in memory. + public long GetPixelMemorySize() + { + int count = this.FrameMetadataCollection.Count > 0 + ? this.FrameMetadataCollection.Count + : 1; + + return (long)this.Size.Width * this.Size.Height * (this.PixelType.BitsPerPixel / 8) * count; + } } diff --git a/tests/ImageSharp.Tests/ImageInfoTests.cs b/tests/ImageSharp.Tests/ImageInfoTests.cs index 322b0af196..748c8a4f6d 100644 --- a/tests/ImageSharp.Tests/ImageInfoTests.cs +++ b/tests/ImageSharp.Tests/ImageInfoTests.cs @@ -54,4 +54,34 @@ public class ImageInfoTests Assert.Equal(meta, info.Metadata); Assert.Equal(frameMetadata.Count, info.FrameMetadataCollection.Count); } + + [Fact] + public void GetPixelMemorySize_UsesSingleFrameWhenFrameMetadataIsEmpty() + { + const int width = 10; + const int height = 20; + + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); + + ImageInfo info = new(new Size(width, height), meta); + + Assert.Equal(width * height * 4, info.GetPixelMemorySize()); + } + + [Fact] + public void GetPixelMemorySize_UsesFrameMetadataCountWhenAvailable() + { + const int width = 10; + const int height = 20; + IReadOnlyList frameMetadata = [new(), new(), new()]; + + ImageMetadata meta = new() { DecodedImageFormat = PngFormat.Instance }; + meta.GetPngMetadata(); + + ImageInfo info = new(new Size(width, height), meta, frameMetadata); + + Assert.Equal(width * height * 4 * frameMetadata.Count, info.GetPixelMemorySize()); + } + }