From 6f3875383050668148f8280e83ff523cdcc6cfc9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 17:36:30 +1000 Subject: [PATCH 01/22] Add image and test --- .../Formats/WebP/WebpEncoderTests.cs | 15 +++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/issues/Issue2866.webp | 3 +++ 3 files changed, 19 insertions(+) create mode 100644 tests/Images/Input/Webp/issues/Issue2866.webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index c6751e2a66..da8f2ae0ce 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -582,6 +582,21 @@ public async Task CanSave_NonSeekableStream_Async(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + WebpEncoder encoder = new() + { + Quality = 100 + }; + + using Image image = provider.GetImage(); + image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.TolerantPercentage(0.0994F)); + } + private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a0e951e70d..911c248095 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -825,6 +825,7 @@ public static class Lossy public const string Issue2670 = "Webp/issues/Issue2670.webp"; public const string Issue2763 = "Webp/issues/Issue2763.png"; public const string Issue2801 = "Webp/issues/Issue2801.webp"; + public const string Issue2866 = "Webp/issues/Issue2866.webp"; } } diff --git a/tests/Images/Input/Webp/issues/Issue2866.webp b/tests/Images/Input/Webp/issues/Issue2866.webp new file mode 100644 index 0000000000..845569624d --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2866.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e8a52a6d528fe071e73b037543b682bf62da7bab6d98ab690f25dd97f7298e +size 248688 From a888544c711cedd5b4a7a8541c034b2ffa1303a9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 09:39:41 +1000 Subject: [PATCH 02/22] Fix GIF handling of unused global tables. --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 44 +++++++++++++------ .../Formats/Gif/MetadataExtensions.cs | 14 ++---- .../Formats/Gif/GifEncoderTests.cs | 14 ++++++ .../Formats/Png/PngEncoderTests.cs | 2 +- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Gif/issues/issue_2866.gif | 3 ++ 6 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 tests/Images/Input/Gif/issues/issue_2866.gif diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 1daa713cbc..2528b01141 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -88,18 +88,24 @@ public void Encode(Image image, Stream stream, CancellationToken GifMetadata gifMetadata = GetGifMetadata(image); this.colorTableMode ??= gifMetadata.ColorTableMode; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; - - // Quantize the first image frame returning a palette. - IndexedImageFrame? quantized = null; + bool useGlobalTableForFirstFrame = useGlobalTable; // Work out if there is an explicit transparent index set for the frame. We use that to ensure the // correct value is set for the background index when quantizing. GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1); + if (frameMetadata.ColorTableMode == GifColorTableMode.Local) + { + useGlobalTableForFirstFrame = false; + } + + // TODO: The first frame should not need to be quantized using the global table if it has a local color table. + // Quantize the first image frame returning a palette. + IndexedImageFrame? quantized = null; if (this.quantizer is null) { // Is this a gif with color information. If so use that, otherwise use octree. - if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) + if (useGlobalTable && gifMetadata.GlobalColorTable?.Length > 0) { // We avoid dithering by default to preserve the original colors. int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); @@ -118,8 +124,9 @@ public void Encode(Image image, Stream stream, CancellationToken } } - using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration)) + if (useGlobalTableForFirstFrame) { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration); if (useGlobalTable) { frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); @@ -131,6 +138,17 @@ public void Encode(Image image, Stream stream, CancellationToken quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); } } + else + { + quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( + image.Frames.RootFrame, + image.Frames.RootFrame.Bounds(), + frameMetadata, + true, + default, + false, + frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1); + } // Write the header. WriteHeader(stream); @@ -243,8 +261,8 @@ private void EncodeAdditionalFrames( return; } - PaletteQuantizer paletteQuantizer = default; - bool hasPaletteQuantizer = false; + PaletteQuantizer globalPaletteQuantizer = default; + bool hasGlobalPaletteQuantizer = false; // Store the first frame as a reference for de-duplication comparison. ImageFrame previousFrame = image.Frames.RootFrame; @@ -260,14 +278,14 @@ private void EncodeAdditionalFrames( GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local); - if (!useLocal && !hasPaletteQuantizer && i > 0) + if (!useLocal && !hasGlobalPaletteQuantizer && i > 0) { // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. // This allows a reduction of memory usage across multi-frame gifs using a global palette // and also allows use to reuse the cache from previous runs. int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1; - paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); - hasPaletteQuantizer = true; + globalPaletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); + hasGlobalPaletteQuantizer = true; } this.EncodeAdditionalFrame( @@ -278,16 +296,16 @@ private void EncodeAdditionalFrames( encodingFrame, useLocal, gifMetadata, - paletteQuantizer, + globalPaletteQuantizer, previousDisposalMethod); previousFrame = currentFrame; previousDisposalMethod = gifMetadata.DisposalMethod; } - if (hasPaletteQuantizer) + if (hasGlobalPaletteQuantizer) { - paletteQuantizer.Dispose(); + globalPaletteQuantizer.Dispose(); } } diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index ad06462e77..7ced5096d8 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -60,21 +60,15 @@ public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhe => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source) - { - Color background = Color.Transparent; - if (source.GlobalColorTable != null) - { - background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex]; - } - return new() + // We cannot trust the global GIF palette so don't use it. + // https://github.com/SixLabors/ImageSharp/issues/2866 + => new() { - ColorTable = source.GlobalColorTable, ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, RepeatCount = source.RepeatCount, - BackgroundColor = background, + BackgroundColor = Color.Transparent, }; - } internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source) { diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index a7e16f7737..f396239f53 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using System.Linq; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.Gif; @@ -382,4 +383,17 @@ public void Encode_Animated_VisualTest(TestImageProvider provide provider.Utility.SaveTestOutputFile(image, "png", new PngEncoder(), "animated"); provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); } + + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32)] + public void GifEncoder_CanDecode_Issue2866(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + bool anyGlobal = ((IEnumerable)image.Frames).Any(x => x.Metadata.GetGifMetadata().ColorTableMode == GifColorTableMode.Global); + + // image.DebugSaveMultiFrame(provider); + provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); + } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index ca5aae961c..ef95c59389 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -485,6 +485,7 @@ public void Encode_APng(TestImageProvider provider) [Theory] [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32)] public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -494,7 +495,6 @@ public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider image = provider.GetImage(GifDecoder.Instance); - using MemoryStream memStream = new(); image.Save(memStream, PngEncoder); memStream.Position = 0; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 911c248095..6c04d5a840 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -535,6 +535,7 @@ public static class Issues public const string Issue2450_B = "Gif/issues/issue_2450_2.gif"; public const string Issue2198 = "Gif/issues/issue_2198.gif"; public const string Issue2758 = "Gif/issues/issue_2758.gif"; + public const string Issue2866 = "Gif/issues/issue_2866.gif"; } public static readonly string[] Animated = diff --git a/tests/Images/Input/Gif/issues/issue_2866.gif b/tests/Images/Input/Gif/issues/issue_2866.gif new file mode 100644 index 0000000000..0ead86bf89 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2866.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b2a9e3728c41e1b45d6f865e4692eadbed28dcaec65806e6bda22a9a16f930f +size 7526725 From ef8c79d1c267456296064462a592ae741129582e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 09:40:24 +1000 Subject: [PATCH 03/22] Fix WEBP animation disposal and blending --- .../Formats/Webp/BitReader/BitReaderBase.cs | 2 +- .../Formats/Webp/Lossy/Vp8Decoder.cs | 14 +-- .../Formats/Webp/WebpAnimationDecoder.cs | 117 ++++++++++++------ .../Formats/Webp/WebpBlendMethod.cs | 6 +- .../Formats/WebP/WebpDecoderTests.cs | 16 +++ .../Formats/WebP/WebpEncoderTests.cs | 15 --- .../ReferenceCodecs/MagickReferenceDecoder.cs | 35 ++++-- 7 files changed, 128 insertions(+), 77 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs index 83f9e797ab..2b843cc8f6 100644 --- a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs +++ b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs @@ -32,7 +32,7 @@ protected BitReaderBase(Stream inputStream, int imageDataSize, MemoryAllocator m /// Used for allocating memory during reading data from the stream. protected static IMemoryOwner ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator) { - IMemoryOwner data = memoryAllocator.Allocate(bytesToRead); + IMemoryOwner data = memoryAllocator.Allocate(bytesToRead, AllocationOptions.Clean); Span dataSpan = data.Memory.Span; input.Read(dataSpan[..bytesToRead], 0, bytesToRead); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs index b3c5bfaf41..eb4a517511 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs @@ -67,14 +67,14 @@ public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp int extraY = extraRows * this.CacheYStride; int extraUv = extraRows / 2 * this.CacheUvStride; this.YuvBuffer = memoryAllocator.Allocate((WebpConstants.Bps * 17) + (WebpConstants.Bps * 9) + extraY); - this.CacheY = memoryAllocator.Allocate((16 * this.CacheYStride) + extraY); + this.CacheY = memoryAllocator.Allocate((16 * this.CacheYStride) + extraY, AllocationOptions.Clean); int cacheUvSize = (16 * this.CacheUvStride) + extraUv; - this.CacheU = memoryAllocator.Allocate(cacheUvSize); - this.CacheV = memoryAllocator.Allocate(cacheUvSize); - this.TmpYBuffer = memoryAllocator.Allocate((int)width); - this.TmpUBuffer = memoryAllocator.Allocate((int)width); - this.TmpVBuffer = memoryAllocator.Allocate((int)width); - this.Pixels = memoryAllocator.Allocate((int)(width * height * 4)); + this.CacheU = memoryAllocator.Allocate(cacheUvSize, AllocationOptions.Clean); + this.CacheV = memoryAllocator.Allocate(cacheUvSize, AllocationOptions.Clean); + this.TmpYBuffer = memoryAllocator.Allocate((int)width, AllocationOptions.Clean); + this.TmpUBuffer = memoryAllocator.Allocate((int)width, AllocationOptions.Clean); + this.TmpVBuffer = memoryAllocator.Allocate((int)width, AllocationOptions.Clean); + this.Pixels = memoryAllocator.Allocate((int)(width * height * 4), AllocationOptions.Clean); #if DEBUG // Filling those buffers with 205, is only useful for debugging, diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 65f1a4da46..595d622622 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -81,16 +81,29 @@ public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration confi /// The width of the image. /// The height of the image. /// The size of the image data in bytes. - public Image Decode(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize) + public Image Decode( + BufferedReadStream stream, + WebpFeatures features, + uint width, + uint height, + uint completeDataSize) where TPixel : unmanaged, IPixel { Image? image = null; ImageFrame? previousFrame = null; + WebpFrameData? prevFrameData = null; this.metadata = new ImageMetadata(); this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata.RepeatCount = features.AnimationLoopCount; + Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? Color.Transparent + : features.AnimationBackgroundColor!.Value; + + this.webpMetadata.BackgroundColor = backgroundColor; + TPixel backgroundPixel = backgroundColor.ToPixel(); + Span buffer = stackalloc byte[4]; uint frameCount = 0; int remainingBytes = (int)completeDataSize; @@ -101,10 +114,15 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat switch (chunkType) { case WebpChunkType.FrameData: - Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore - ? new Color(new Bgra32(0, 0, 0, 0)) - : features.AnimationBackgroundColor!.Value; - uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, backgroundColor); + uint dataSize = this.ReadFrame( + stream, + ref image, + ref previousFrame, + ref prevFrameData, + width, + height, + backgroundPixel); + remainingBytes -= (int)dataSize; break; case WebpChunkType.Xmp: @@ -132,10 +150,18 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat /// The stream, where the image should be decoded from. Cannot be null. /// The image to decode the information to. /// The previous frame. + /// The previous frame data. /// The width of the image. /// The height of the image. /// The default background color of the canvas in. - private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) + private uint ReadFrame( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ref WebpFrameData? prevFrameData, + uint width, + uint height, + TPixel backgroundColor) where TPixel : unmanaged, IPixel { WebpFrameData frameData = WebpFrameData.Parse(stream); @@ -174,40 +200,54 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima break; } - ImageFrame? currentFrame = null; - ImageFrame imageFrame; + bool isKeyFrame = false; + ImageFrame currentFrame; if (previousFrame is null) { - image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new Image(this.configuration, (int)width, (int)height, backgroundColor, this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData); - imageFrame = image.Frames.RootFrame; + currentFrame = image.Frames.RootFrame; + isKeyFrame = true; } else { - currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection. + // If the frame is a key frame we do not need to clone the frame or clear it. + isKeyFrame = prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground + && this.restoreArea == image!.Bounds; - SetFrameMetadata(currentFrame.Metadata, frameData); + if (isKeyFrame) + { + currentFrame = image!.Frames.CreateFrame(backgroundColor); + } + else + { + // This clones the frame and adds it the collection. + currentFrame = image!.Frames.AddFrame(previousFrame); + if (prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground) + { + this.RestoreToBackground(currentFrame, backgroundColor); + } + } - imageFrame = currentFrame; + SetFrameMetadata(currentFrame.Metadata, frameData); } - Rectangle regionRectangle = frameData.Bounds; + Rectangle interest = frameData.Bounds; + bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over; + using Buffer2D pixelData = this.DecodeImageFrameData(frameData, webpInfo); + DrawDecodedImageFrameOnCanvas(pixelData, currentFrame, interest, blend); + + webpInfo?.Dispose(); + previousFrame = currentFrame; + prevFrameData = frameData; if (frameData.DisposalMethod is WebpDisposalMethod.RestoreToBackground) { - this.RestoreToBackground(imageFrame, backgroundColor); + this.restoreArea = interest; } - using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - - bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendMethod.Over; - DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); - - previousFrame = currentFrame ?? image.Frames.RootFrame; - this.restoreArea = regionRectangle; - return (uint)(stream.Position - streamStartPosition); } @@ -257,31 +297,27 @@ private Buffer2D DecodeImageFrameData(WebpFrameData frameData, W try { - Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer; + Buffer2D decodeBuffer = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); - losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); + losslessDecoder.Decode(decodeBuffer, (int)frameData.Width, (int)frameData.Height); } else { WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); - lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); + lossyDecoder.Decode(decodeBuffer, (int)frameData.Width, (int)frameData.Height, webpInfo, this.alphaData); } - return pixelBufferDecoded; + return decodeBuffer; } catch { decodedFrame?.Dispose(); throw; } - finally - { - webpInfo.Dispose(); - } } /// @@ -290,17 +326,17 @@ private Buffer2D DecodeImageFrameData(WebpFrameData frameData, W /// The type of the pixel. /// The decoded image. /// The image frame to draw into. - /// The area of the frame. + /// The area of the frame to draw to. /// Whether to blend the decoded frame data onto the target frame. private static void DrawDecodedImageFrameOnCanvas( Buffer2D decodedImageFrame, ImageFrame imageFrame, - Rectangle restoreArea, + Rectangle interest, bool blend) where TPixel : unmanaged, IPixel { // Trim the destination frame to match the restore area. The source frame is already trimmed. - Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(interest); if (blend) { // The destination frame has already been prepopulated with the pixel data from the previous frame @@ -309,10 +345,10 @@ private static void DrawDecodedImageFrameOnCanvas( PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); - for (int y = 0; y < restoreArea.Height; y++) + for (int y = 0; y < interest.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y); blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); } @@ -320,10 +356,10 @@ private static void DrawDecodedImageFrameOnCanvas( return; } - for (int y = 0; y < restoreArea.Height; y++) + for (int y = 0; y < interest.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y); decodedPixelRow.CopyTo(framePixelRow); } } @@ -335,7 +371,7 @@ private static void DrawDecodedImageFrameOnCanvas( /// The pixel format. /// The image frame. /// Color of the background. - private void RestoreToBackground(ImageFrame imageFrame, Color backgroundColor) + private void RestoreToBackground(ImageFrame imageFrame, TPixel backgroundColor) where TPixel : unmanaged, IPixel { if (!this.restoreArea.HasValue) @@ -345,8 +381,7 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value); Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); - TPixel backgroundPixel = backgroundColor.ToPixel(); - pixelRegion.Fill(backgroundPixel); + pixelRegion.Fill(backgroundColor); } /// diff --git a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs index f16f7650c7..2b6ef17f17 100644 --- a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats.Webp; @@ -12,11 +12,11 @@ public enum WebpBlendMethod /// Do not blend. After disposing of the previous frame, /// render the current frame on the canvas by overwriting the rectangle covered by the current frame. /// - Source = 0, + Source = 1, /// /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. /// - Over = 1, + Over = 0, } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 0dda304b64..29362105db 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -450,6 +450,22 @@ public void WebpDecoder_CanDecode_Issue2670(TestImageProvider pr image.CompareToOriginal(provider, ReferenceDecoder); } + // https://github.com/SixLabors/ImageSharp/issues/2866 + [Theory] + [WithFile(Lossy.Issue2866, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Issue2866(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Web + using Image image = provider.GetImage( + WebpDecoder.Instance, + new WebpDecoderOptions() { BackgroundColorHandling = BackgroundColorHandling.Ignore }); + + // We can't use the reference decoder here. + // It creates frames of different size without blending the frames. + image.DebugSave(provider, extension: "webp", encoder: new WebpEncoder()); + } + [Theory] [WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)] public void WebpDecoder_ThrowImageFormatException_OnInvalidImages(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index da8f2ae0ce..c6751e2a66 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -582,21 +582,6 @@ public async Task CanSave_NonSeekableStream_Async(TestImageProvider(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - WebpEncoder encoder = new() - { - Quality = 100 - }; - - using Image image = provider.GetImage(); - image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.TolerantPercentage(0.0994F)); - } - private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 80b5536ebd..a06e19594d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -44,26 +44,33 @@ protected override Image Decode(DecoderOptions options, Stream s settings.SetDefines(pngReadDefines); using MagickImageCollection magickImageCollection = new(stream, settings); + int imageWidth = magickImageCollection.Max(x => x.Width); + int imageHeight = magickImageCollection.Max(x => x.Height); + List> framesList = new(); foreach (IMagickImage magicFrame in magickImageCollection) { - ImageFrame frame = new(configuration, magicFrame.Width, magicFrame.Height); + ImageFrame frame = new(configuration, imageWidth, imageHeight); framesList.Add(frame); - MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; + Buffer2DRegion buffer = frame.PixelBuffer.GetRegion( + imageWidth - magicFrame.Width, + imageHeight - magicFrame.Height, + magicFrame.Width, + magicFrame.Height); using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) { byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - FromRgba32Bytes(configuration, data, framePixels); + FromRgba32Bytes(configuration, data, buffer); } else if (magicFrame.Depth is 16 or 14) { ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); - FromRgba64Bytes(configuration, bytes, framePixels); + FromRgba64Bytes(configuration, bytes, buffer); } else { @@ -83,32 +90,40 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata))); } - private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + private static void FromRgba32Bytes( + Configuration configuration, + Span rgbaBytes, + Buffer2DRegion destinationGroup) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { Span sourcePixels = MemoryMarshal.Cast(rgbaBytes); - foreach (Memory m in destinationGroup) + for (int y = 0; y < destinationGroup.Height; y++) { - Span destBuffer = m.Span; + Span destBuffer = destinationGroup.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgba32( configuration, sourcePixels[..destBuffer.Length], destBuffer); + sourcePixels = sourcePixels[destBuffer.Length..]; } } - private static void FromRgba64Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + private static void FromRgba64Bytes( + Configuration configuration, + Span rgbaBytes, + Buffer2DRegion destinationGroup) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - foreach (Memory m in destinationGroup) + for (int y = 0; y < destinationGroup.Height; y++) { - Span destBuffer = m.Span; + Span destBuffer = destinationGroup.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgba64Bytes( configuration, rgbaBytes, destBuffer, destBuffer.Length); + rgbaBytes = rgbaBytes[(destBuffer.Length * 8)..]; } } From bd1649dee5d13ec292dcc504bbd531837aace46c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 13:06:42 +1000 Subject: [PATCH 04/22] Ensure WEBP decoder unsets the restore area. --- src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 595d622622..add09657e7 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -382,6 +382,8 @@ private void RestoreToBackground(ImageFrame imageFrame, TPixel b Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value); Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); pixelRegion.Fill(backgroundColor); + + this.restoreArea = null; } /// From 2b239ec60519301d3a7e7941fd32e876a1c81d4f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 13:07:46 +1000 Subject: [PATCH 05/22] Fix GIF restore to background behavior and background color assignment. --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 110 +++++++++++------- ...e1962_Rgba32_issue1962_tiniest_gif_1st.png | 4 +- ...2012BadMinCode_Rgba32_issue2012_drona1.png | 4 +- 3 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 68f4e5fa2d..f838e33b36 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -89,6 +89,9 @@ internal sealed class GifDecoderCore : ImageDecoderCore /// private GifMetadata? gifMetadata; + // The background color used to fill the frame. + private Color backgroundColor; + /// /// Initializes a new instance of the class. /// @@ -108,9 +111,12 @@ protected override Image Decode(BufferedReadStream stream, Cance uint frameCount = 0; Image? image = null; ImageFrame? previousFrame = null; + GifDisposalMethod? previousDisposalMethod = null; + try { this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); + TPixel backgroundPixel = this.backgroundColor.ToPixel(); // Loop though the respective gif parts and read the data. int nextFlag = stream.ReadByte(); @@ -123,7 +129,7 @@ protected override Image Decode(BufferedReadStream stream, Cance break; } - this.ReadFrame(stream, ref image, ref previousFrame); + this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel); // Reset per-frame state. this.imageDescriptor = default; @@ -417,7 +423,14 @@ private void ReadComments(BufferedReadStream stream) /// The containing image data. /// The image to decode the information to. /// The previous frame. - private void ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame) + /// The previous disposal method. + /// The background color pixel. + private void ReadFrame( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ref GifDisposalMethod? previousDisposalMethod, + TPixel backgroundPixel) where TPixel : unmanaged, IPixel { this.ReadImageDescriptor(stream); @@ -444,7 +457,7 @@ private void ReadFrame(BufferedReadStream stream, ref Image? ima } ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); - this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor); + this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundPixel); // Skip any remaining blocks SkipBlock(stream); @@ -457,47 +470,38 @@ private void ReadFrame(BufferedReadStream stream, ref Image? ima /// The containing image data. /// The image to decode the information to. /// The previous frame. + /// The previous disposal method. /// The color table containing the available colors. /// The + /// The background color pixel. private void ReadFrameColors( BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, + ref GifDisposalMethod? previousDisposalMethod, ReadOnlySpan colorTable, - in GifImageDescriptor descriptor) + in GifImageDescriptor descriptor, + TPixel backgroundPixel) where TPixel : unmanaged, IPixel { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; bool transFlag = this.graphicsControlExtension.TransparencyFlag; - ImageFrame? prevFrame = null; - ImageFrame? currentFrame = null; - ImageFrame imageFrame; + GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod; + ImageFrame currentFrame; if (previousFrame is null) { - if (!transFlag) - { - image = new Image(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata); - } - else - { - // This initializes the image to become fully transparent because the alpha channel is zero. - image = new Image(this.configuration, imageWidth, imageHeight, this.metadata); - } + image = transFlag + ? new Image(this.configuration, imageWidth, imageHeight, this.metadata) + : new Image(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata); this.SetFrameMetadata(image.Frames.RootFrame.Metadata); - - imageFrame = image.Frames.RootFrame; + currentFrame = image.Frames.RootFrame; } else { - if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) - { - prevFrame = previousFrame; - } - // We create a clone of the frame and add it. // We will overpaint the difference of pixels on the current frame to create a complete image. // This ensures that we have enough pixel data to process without distortion. #2450 @@ -505,9 +509,19 @@ private void ReadFrameColors( this.SetFrameMetadata(currentFrame.Metadata); - imageFrame = currentFrame; + if (previousDisposalMethod == GifDisposalMethod.RestoreToBackground) + { + this.RestoreToBackground(currentFrame, backgroundPixel, transFlag); + } + } + + Rectangle interest = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height)); + previousFrame = currentFrame; + previousDisposalMethod = disposalMethod; - this.RestoreToBackground(imageFrame); + if (disposalMethod == GifDisposalMethod.RestoreToBackground) + { + this.restoreArea = interest; } if (colorTable.Length == 0) @@ -573,7 +587,7 @@ private void ReadFrameColors( } lzwDecoder.DecodePixelRow(indicesRow); - ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); + ref TPixel rowRef = ref MemoryMarshal.GetReference(currentFrame.PixelBuffer.DangerousGetRowSpan(writeY)); if (!transFlag) { @@ -605,19 +619,6 @@ private void ReadFrameColors( } } } - - if (prevFrame != null) - { - previousFrame = prevFrame; - return; - } - - previousFrame = currentFrame ?? image.Frames.RootFrame; - - if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground) - { - this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); - } } /// @@ -638,6 +639,11 @@ private void ReadFrameMetadata(BufferedReadStream stream, List(768, AllocationOptions.Clean); stream.Read(this.currentLocalColorTable.GetSpan()[..length]); } + else + { + this.currentLocalColorTable = null; + this.currentLocalColorTableSize = 0; + } // Skip the frame indices. Pixels length + mincode size. // The gif format does not tell us the length of the compressed data beforehand. @@ -662,7 +668,9 @@ private void ReadFrameMetadata(BufferedReadStream stream, List /// The pixel format. /// The frame. - private void RestoreToBackground(ImageFrame frame) + /// The background color. + /// Whether the background is transparent. + private void RestoreToBackground(ImageFrame frame, TPixel background, bool transparent) where TPixel : unmanaged, IPixel { if (this.restoreArea is null) @@ -672,7 +680,14 @@ private void RestoreToBackground(ImageFrame frame) Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value); Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest); - pixelRegion.Clear(); + if (transparent) + { + pixelRegion.Clear(); + } + else + { + pixelRegion.Fill(background); + } this.restoreArea = null; } @@ -787,7 +802,18 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s } } - this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex; + // If the global color table is present, we can set the background color + // otherwise we default to transparent to match browser behavior. + ReadOnlyMemory? table = this.gifMetadata.GlobalColorTable; + byte index = this.logicalScreenDescriptor.BackgroundColorIndex; + if (table is not null && index < table.Value.Length) + { + this.backgroundColor = table.Value.Span[index]; + } + else + { + this.backgroundColor = Color.Transparent; + } } private unsafe struct ScratchBuffer diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png index 24f5e9c0cd..a52b27708a 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f8c6d416f09671777934e57bc67fb52ccc97145dc6f1869e628d9ffd7d8f6e7 -size 119 +oid sha256:9ab8374e77865606a2426e3d22628f717914472431de1d9d8ee9690d319850a0 +size 118 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png index b07e806620..6f2fc842f8 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70 -size 142244 +oid sha256:ff67035f78690321c29a4e15c8de7c55bcb3260d667dbd9bced15de6b626fca1 +size 148499 From f63ad846c2cb8f28236c31b66f0412ec68f941f3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 15:21:00 +1000 Subject: [PATCH 06/22] Remove TODO: --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 2528b01141..f51c97db9a 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -98,8 +98,6 @@ public void Encode(Image image, Stream stream, CancellationToken useGlobalTableForFirstFrame = false; } - // TODO: The first frame should not need to be quantized using the global table if it has a local color table. - // Quantize the first image frame returning a palette. IndexedImageFrame? quantized = null; if (this.quantizer is null) From 7cecea9020efba12bb29260db7335fe8251de981 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 15:50:27 +1000 Subject: [PATCH 07/22] Revert breaking change --- src/ImageSharp/Formats/Webp/WebpBlendMethod.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs index 2b6ef17f17..6961eb8541 100644 --- a/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs @@ -8,15 +8,15 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// public enum WebpBlendMethod { - /// - /// Do not blend. After disposing of the previous frame, - /// render the current frame on the canvas by overwriting the rectangle covered by the current frame. - /// - Source = 1, - /// /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. /// Over = 0, + + /// + /// Do not blend. After disposing of the previous frame, + /// render the current frame on the canvas by overwriting the rectangle covered by the current frame. + /// + Source = 1, } From 9c5bcfa6fd11a1494a334701564a7f3f9614c34c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 15:53:18 +1000 Subject: [PATCH 08/22] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a35175ff49..d9e0f1e08f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -146,7 +146,7 @@ jobs: XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - name: Export Failed Output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip From 5d77de9b9ceef9d5091aba4791ec588e09fd9fe7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Feb 2025 22:52:29 +1000 Subject: [PATCH 09/22] Fix PNG animation encoding and quantizer output --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 15 +- .../Formats/Gif/MetadataExtensions.cs | 13 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 12 - src/ImageSharp/Formats/Png/PngEncoderCore.cs | 321 +++++++++++------- src/ImageSharp/Formats/Png/PngMetadata.cs | 2 +- .../Formats/Webp/WebpAnimationDecoder.cs | 12 +- .../Formats/Webp/WebpCommonUtils.cs | 2 +- src/ImageSharp/IndexedImageFrame{TPixel}.cs | 4 +- .../Quantization/EuclideanPixelMap{TPixel}.cs | 300 ++++++++++++---- .../Quantization/OctreeQuantizer{TPixel}.cs | 51 ++- .../Quantization/WuQuantizer{TPixel}.cs | 52 +-- .../Formats/Gif/GifEncoderTests.cs | 2 - .../Formats/Png/PngEncoderTests.cs | 45 ++- .../Formats/WebP/WebpCommonUtilsTests.cs | 4 +- .../ImageSharp.Tests/Image/ImageFrameTests.cs | 2 +- tests/ImageSharp.Tests/Image/ImageTests.cs | 2 +- 16 files changed, 567 insertions(+), 272 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index f838e33b36..fccb994358 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -112,6 +112,7 @@ protected override Image Decode(BufferedReadStream stream, Cance Image? image = null; ImageFrame? previousFrame = null; GifDisposalMethod? previousDisposalMethod = null; + bool globalColorTableUsed = false; try { @@ -129,7 +130,7 @@ protected override Image Decode(BufferedReadStream stream, Cance break; } - this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel); + globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel); // Reset per-frame state. this.imageDescriptor = default; @@ -164,6 +165,13 @@ protected override Image Decode(BufferedReadStream stream, Cance break; } } + + // We cannot always trust the global GIF palette has actually been used. + // https://github.com/SixLabors/ImageSharp/issues/2866 + if (!globalColorTableUsed) + { + this.gifMetadata.ColorTableMode = GifColorTableMode.Local; + } } finally { @@ -425,7 +433,7 @@ private void ReadComments(BufferedReadStream stream) /// The previous frame. /// The previous disposal method. /// The background color pixel. - private void ReadFrame( + private bool ReadFrame( BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, @@ -461,6 +469,8 @@ private void ReadFrame( // Skip any remaining blocks SkipBlock(stream); + + return !hasLocalColorTable; } /// @@ -809,6 +819,7 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s if (table is not null && index < table.Value.Length) { this.backgroundColor = table.Value.Span[index]; + this.gifMetadata.BackgroundColorIndex = index; } else { diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index 7ced5096d8..4e9977b3fd 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -60,15 +60,20 @@ public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhe => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source) + { + bool global = source.ColorTableMode == GifColorTableMode.Global; + Color color = global && source.GlobalColorTable.HasValue && source.GlobalColorTable.Value.Span.Length > source.BackgroundColorIndex + ? source.GlobalColorTable.Value.Span[source.BackgroundColorIndex] + : Color.Transparent; - // We cannot trust the global GIF palette so don't use it. - // https://github.com/SixLabors/ImageSharp/issues/2866 - => new() + return new() { ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local, + ColorTable = global ? source.GlobalColorTable : null, RepeatCount = source.RepeatCount, - BackgroundColor = Color.Transparent, + BackgroundColor = color, }; + } internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source) { diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index dcbaf3140d..50a6b44f83 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,8 +2,6 @@ // Licensed under the Six Labors Split License. #nullable disable -using SixLabors.ImageSharp.Processing.Processors.Quantization; - namespace SixLabors.ImageSharp.Formats.Png; /// @@ -11,16 +9,6 @@ namespace SixLabors.ImageSharp.Formats.Png; /// public class PngEncoder : QuantizingImageEncoder { - /// - /// Initializes a new instance of the class. - /// - public PngEncoder() - - // Hack. TODO: Investigate means to fix/optimize the Wu quantizer. - // The Wu quantizer does not handle the default sampling strategy well for some larger images. - // It's expensive and the results are not better than the extensive strategy. - => this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy(); - /// /// Gets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e01b5c2a59..ee4781db47 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -124,6 +125,14 @@ internal sealed class PngEncoderCore : IDisposable /// private int derivedTransparencyIndex = -1; + /// + /// The default background color of the canvas when animating. + /// This color may be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when a frame disposal mode is . + /// + private Color? backgroundColor; + /// /// Initializes a new instance of the class. /// @@ -161,118 +170,163 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame? clonedFrame = null; ImageFrame currentFrame = image.Frames.RootFrame; - int currentFrameIndex = 0; - - bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; - if (clearTransparency) - { - currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame); - } - - // Do not move this. We require an accurate bit depth for the header chunk. - IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth( - pngMetadata, - currentFrame, - currentFrame.Bounds(), - null); - - this.WriteHeaderChunk(stream); - this.WriteGammaChunk(stream); - this.WriteCicpChunk(stream, metadata); - this.WriteColorProfileChunk(stream, metadata); - this.WritePaletteChunk(stream, quantized); - this.WriteTransparencyChunk(stream, pngMetadata); - this.WritePhysicalChunk(stream, metadata); - this.WriteExifChunk(stream, metadata); - this.WriteXmpChunk(stream, metadata); - this.WriteTextChunks(stream, pngMetadata); + IndexedImageFrame? quantized = null; + PaletteQuantizer? paletteQuantizer = null; + Buffer2DRegion currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); - if (image.Frames.Count > 1) + try { - this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount); - } + int currentFrameIndex = 0; - // If the first frame isn't animated, write it as usual and skip it when writing animated frames - if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1) - { - FrameControl frameControl = new((uint)this.width, (uint)this.height); - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); - currentFrameIndex++; - } + bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; + if (clearTransparency) + { + currentFrame = clonedFrame = currentFrame.Clone(); + ClearTransparentPixels(currentFrame, this.backgroundColor.Value); + } - if (image.Frames.Count > 1) - { - // Write the first animated frame. - currentFrame = image.Frames[currentFrameIndex]; - PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); - PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; - FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); - uint sequenceNumber = 1; - if (pngMetadata.AnimateRootFrame) + // Do not move this. We require an accurate bit depth for the header chunk. + quantized = this.CreateQuantizedImageAndUpdateBitDepth( + pngMetadata, + image, + currentFrame, + currentFrame.Bounds(), + null); + + this.WriteHeaderChunk(stream); + this.WriteGammaChunk(stream); + this.WriteCicpChunk(stream, metadata); + this.WriteColorProfileChunk(stream, metadata); + this.WritePaletteChunk(stream, quantized); + this.WriteTransparencyChunk(stream, pngMetadata); + this.WritePhysicalChunk(stream, metadata); + this.WriteExifChunk(stream, metadata); + this.WriteXmpChunk(stream, metadata); + this.WriteTextChunks(stream, pngMetadata); + + if (image.Frames.Count > 1) { - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + this.WriteAnimationControlChunk( + stream, + (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), + pngMetadata.RepeatCount); } - else + + // If the first frame isn't animated, write it as usual and skip it when writing animated frames + if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1) { - sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true); + cancellationToken.ThrowIfCancellationRequested(); + FrameControl frameControl = new((uint)this.width, (uint)this.height); + this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + currentFrameIndex++; } - currentFrameIndex++; + if (image.Frames.Count > 1) + { + // Write the first animated frame. + currentFrame = image.Frames[currentFrameIndex]; + currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); - // Capture the global palette for reuse on subsequent frames. - ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray(); + PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame); + PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod; + FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0); + uint sequenceNumber = 1; + if (pngMetadata.AnimateRootFrame) + { + this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false); + } + else + { + sequenceNumber += this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, true); + } - // Write following frames. - ImageFrame previousFrame = image.Frames.RootFrame; + currentFrameIndex++; - // This frame is reused to store de-duplicated pixel buffers. - using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); + // Capture the global palette for reuse on subsequent frames. + ReadOnlyMemory previousPalette = quantized?.Palette.ToArray(); - for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) - { - ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; - currentFrame = image.Frames[currentFrameIndex]; - ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; + if (!previousPalette.IsEmpty) + { + // Use the previously derived global palette and a shared quantizer to + // quantize the subsequent frames. This allows us to cache the color matching resolution. + paletteQuantizer ??= new( + this.configuration, + this.quantizer!.Options, + previousPalette, + this.derivedTransparencyIndex); + } - frameMetadata = GetPngFrameMetadata(currentFrame); - bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; + // Write following frames. + ImageFrame previousFrame = image.Frames.RootFrame; - (bool difference, Rectangle bounds) = - AnimationUtilities.DeDuplicatePixels( - image.Configuration, - prev, - currentFrame, - nextFrame, - encodingFrame, - Color.Transparent, - blend); + // This frame is reused to store de-duplicated pixel buffers. + using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size()); - if (clearTransparency) + for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) { - ClearTransparentPixels(encodingFrame); - } + cancellationToken.ThrowIfCancellationRequested(); + + ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame; + currentFrame = image.Frames[currentFrameIndex]; + ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; + + frameMetadata = GetPngFrameMetadata(currentFrame); + bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; + Color background = frameMetadata.DisposalMethod == PngDisposalMethod.RestoreToBackground + ? this.backgroundColor.Value + : Color.Transparent; + + (bool difference, Rectangle bounds) = + AnimationUtilities.DeDuplicatePixels( + image.Configuration, + prev, + currentFrame, + nextFrame, + encodingFrame, + background, + blend); + + if (clearTransparency) + { + ClearTransparentPixels(encodingFrame, background); + } - // Each frame control sequence number must be incremented by the number of frame data chunks that follow. - frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); + // Each frame control sequence number must be incremented by the number of frame data chunks that follow. + frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); - // Dispose of previous quantized frame and reassign. - quantized?.Dispose(); - quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); - sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; + // Dispose of previous quantized frame and reassign. + quantized?.Dispose(); - previousFrame = currentFrame; - previousDisposal = frameMetadata.DisposalMethod; - } - } + quantized = this.CreateQuantizedFrame( + this.encoder, + this.colorType, + this.bitDepth, + pngMetadata, + image, + encodingFrame, + bounds, + paletteQuantizer, + default); - this.WriteEndChunk(stream); + Buffer2DRegion encodingFrameRegion = encodingFrame.PixelBuffer.GetRegion(bounds); + sequenceNumber += this.WriteDataChunks(frameControl, in encodingFrameRegion, quantized, stream, true) + 1; + + previousFrame = currentFrame; + previousDisposal = frameMetadata.DisposalMethod; + } + } - stream.Flush(); + this.WriteEndChunk(stream); - // Dispose of allocations from final frame. - clonedFrame?.Dispose(); - quantized?.Dispose(); + stream.Flush(); + } + finally + { + // Dispose of allocations from final frame. + clonedFrame?.Dispose(); + quantized?.Dispose(); + paletteQuantizer?.Dispose(); + } } /// @@ -293,7 +347,9 @@ private static PngMetadata GetPngMetadata(Image image) if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif)) { AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata(); - return PngMetadata.FromAnimatedMetadata(ani); + PngMetadata metadata = PngMetadata.FromAnimatedMetadata(ani); + metadata.ColorType = PngColorType.Palette; + return metadata; } if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp)) @@ -335,13 +391,14 @@ private static PngFrameMetadata GetPngFrameMetadata(ImageFrame f /// /// The type of the pixel. /// The cloned image frame where the transparent pixels will be changed. - private static void ClearTransparentPixels(ImageFrame clone) + /// The color to change transparent pixels to. + private static void ClearTransparentPixels(ImageFrame clone, Color color) where TPixel : unmanaged, IPixel => clone.ProcessPixelRows(accessor => { // TODO: We should be able to speed this up with SIMD and masking. Rgba32 rgba32 = default; - Rgba32 transparent = Color.Transparent; + Rgba32 transparent = color; for (int y = 0; y < accessor.Height; y++) { Span span = accessor.GetRowSpan(y); @@ -362,18 +419,35 @@ private static void ClearTransparentPixels(ImageFrame clone) /// /// The type of the pixel. /// The image metadata. - /// The frame to quantize. + /// The image. + /// The current image frame. /// The area of interest within the frame. - /// Any previously derived palette. + /// The quantizer containing any previously derived palette. /// The quantized image. private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth( PngMetadata metadata, + Image image, ImageFrame frame, Rectangle bounds, - ReadOnlyMemory? previousPalette) + PaletteQuantizer? paletteQuantizer) where TPixel : unmanaged, IPixel { - IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, bounds, previousPalette); + PngFrameMetadata frameMetadata = GetPngFrameMetadata(frame); + Color background = frameMetadata.DisposalMethod == PngDisposalMethod.RestoreToBackground + ? this.backgroundColor ?? Color.Transparent + : Color.Transparent; + + IndexedImageFrame? quantized = this.CreateQuantizedFrame( + this.encoder, + this.colorType, + this.bitDepth, + metadata, + image, + frame, + bounds, + paletteQuantizer, + background); + this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized); return quantized; } @@ -876,6 +950,7 @@ private void WriteXmpChunk(Stream stream, ImageMetadata meta) /// /// The containing image data. /// The image meta data. + /// CICP matrix coefficients other than Identity are not supported in PNG. private void WriteCicpChunk(Stream stream, ImageMetadata metaData) { if (metaData.CicpProfile is null) @@ -1139,7 +1214,7 @@ private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata fram /// The quantized pixel data. Can be null. /// The stream. /// Is writing fdAT or IDAT. - private uint WriteDataChunks(FrameControl frameControl, Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame) + private uint WriteDataChunks(in FrameControl frameControl, in Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame) where TPixel : unmanaged, IPixel { byte[] buffer; @@ -1157,12 +1232,12 @@ private uint WriteDataChunks(FrameControl frameControl, Buffer2DRegionThe image frame pixel buffer. /// The quantized pixels. /// The deflate stream. - private void EncodePixels(Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(in Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { int bytesPerScanline = this.CalculateScanlineLength(pixels.Width); @@ -1256,7 +1331,7 @@ private void EncodePixels(Buffer2DRegion pixels, IndexedImageFra /// The type of the pixel. /// The image frame pixel buffer. /// The deflate stream. - private void EncodeAdam7Pixels(Buffer2DRegion pixels, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(in Buffer2DRegion pixels, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { for (int pass = 0; pass < 7; pass++) @@ -1292,7 +1367,7 @@ private void EncodeAdam7Pixels(Buffer2DRegion pixels, ZlibDeflat // Encode data // Note: quantized parameter not used // Note: row parameter not used - this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1); + this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1); deflateStream.Write(filter); this.SwapScanlineBuffers(); @@ -1464,6 +1539,7 @@ private void SwapScanlineBuffers() /// The PNG metadata. /// if set to true [use16 bit]. /// The bytes per pixel. + [MemberNotNull(nameof(backgroundColor))] private void SanitizeAndSetEncoderOptions( PngEncoder encoder, PngMetadata pngMetadata, @@ -1502,6 +1578,7 @@ private void SanitizeAndSetEncoderOptions( this.interlaceMode = (encoder.InterlaceMethod ?? pngMetadata.InterlaceMethod)!.Value; this.chunkFilter = encoder.SkipMetadata ? PngChunkFilter.ExcludeAll : encoder.ChunkFilter ?? PngChunkFilter.None; + this.backgroundColor = pngMetadata.TransparentColor ?? Color.Transparent; } /// @@ -1512,17 +1589,21 @@ private void SanitizeAndSetEncoderOptions( /// The color type. /// The bits per component. /// The image metadata. - /// The frame to quantize. + /// The image. + /// The current image frame. /// The frame area of interest. - /// Any previously derived palette. + /// The quantizer containing any previously derived palette. + /// The background color. private IndexedImageFrame? CreateQuantizedFrame( QuantizingImageEncoder encoder, PngColorType colorType, byte bitDepth, PngMetadata metadata, + Image image, ImageFrame frame, Rectangle bounds, - ReadOnlyMemory? previousPalette) + PaletteQuantizer? paletteQuantizer, + Color backgroundColor) where TPixel : unmanaged, IPixel { if (colorType is not PngColorType.Palette) @@ -1530,22 +1611,15 @@ private void SanitizeAndSetEncoderOptions( return null; } - if (previousPalette is not null) + if (paletteQuantizer.HasValue) { - // Use the previously derived palette created by quantizing the root frame to quantize the current frame. - using PaletteQuantizer paletteQuantizer = new( - this.configuration, - this.quantizer!.Options, - previousPalette.Value, - this.derivedTransparencyIndex); - paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); - return paletteQuantizer.QuantizeFrame(frame, bounds); + return paletteQuantizer.Value.QuantizeFrame(frame, bounds); } // Use the metadata to determine what quantization depth to use if no quantizer has been set. if (this.quantizer is null) { - if (metadata.ColorTable is not null) + if (metadata.ColorTable.HasValue && !metadata.ColorTable.Value.IsEmpty) { // We can use the color data from the decoded metadata here. // We avoid dithering by default to preserve the original colors. @@ -1578,7 +1652,22 @@ private void SanitizeAndSetEncoderOptions( // Create quantized frame returning the palette and set the bit depth. using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration); - frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); + if (image.Frames.Count > 1) + { + // Encoding animated frames with a global palette requires a transparent pixel in the palette + // since we only encode the delta between frames. To ensure that we have a transparent pixel + // we create a fake frame with a containing only transparent pixels and add it to the palette. + using Buffer2D px = image.Configuration.MemoryAllocator.Allocate2D(Math.Min(256, image.Width), Math.Min(256, image.Height)); + TPixel backGroundPixel = backgroundColor.ToPixel(); + for (int i = 0; i < px.Height; i++) + { + px.DangerousGetRowSpan(i).Fill(backGroundPixel); + } + + frameQuantizer.AddPaletteColors(px.GetRegion()); + } + + frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); return frameQuantizer.QuantizeFrame(frame, bounds); } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index d9028dd807..0d2515c2a2 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -96,7 +96,7 @@ internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) { // Should the conversion be from a format that uses a 24bit palette entries (gif) // we need to clone and adjust the color table to allow for transparency. - Color[]? colorTable = metadata.ColorTable.HasValue ? metadata.ColorTable.Value.ToArray() : null; + Color[]? colorTable = metadata.ColorTable?.ToArray(); if (colorTable != null) { for (int i = 0; i < colorTable.Length; i++) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index add09657e7..b9f58c3d84 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -200,22 +200,19 @@ private uint ReadFrame( break; } - bool isKeyFrame = false; ImageFrame currentFrame; if (previousFrame is null) { image = new Image(this.configuration, (int)width, (int)height, backgroundColor, this.metadata); - SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData); - currentFrame = image.Frames.RootFrame; - isKeyFrame = true; + SetFrameMetadata(currentFrame.Metadata, frameData); } else { // If the frame is a key frame we do not need to clone the frame or clear it. - isKeyFrame = prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground - && this.restoreArea == image!.Bounds; + bool isKeyFrame = prevFrameData?.DisposalMethod is WebpDisposalMethod.RestoreToBackground + && this.restoreArea == image!.Bounds; if (isKeyFrame) { @@ -300,8 +297,7 @@ private Buffer2D DecodeImageFrameData(WebpFrameData frameData, W Buffer2D decodeBuffer = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = - new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(decodeBuffer, (int)frameData.Width, (int)frameData.Height); } else diff --git a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs index 49482260bb..69eeab1204 100644 --- a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs @@ -68,7 +68,7 @@ public static WebpFrameMetadata GetWebpFrameMetadata(ImageFrame /// /// The row to check. /// Returns true if alpha has non-0xff values. - public static unsafe bool CheckNonOpaque(Span row) + public static unsafe bool CheckNonOpaque(ReadOnlySpan row) { if (Avx2.IsSupported) { diff --git a/src/ImageSharp/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/IndexedImageFrame{TPixel}.cs index 6807e77ad2..49c9e33eb1 100644 --- a/src/ImageSharp/IndexedImageFrame{TPixel}.cs +++ b/src/ImageSharp/IndexedImageFrame{TPixel}.cs @@ -30,7 +30,7 @@ public sealed class IndexedImageFrame : IPixelSource, IDisposable /// The frame width. /// The frame height. /// The color palette. - internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) + public IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette) { Guard.NotNull(configuration, nameof(configuration)); Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette)); @@ -42,7 +42,7 @@ internal IndexedImageFrame(Configuration configuration, int width, int height, R this.Height = height; this.pixelBuffer = configuration.MemoryAllocator.Allocate2D(width, height); - // Copy the palette over. We want the lifetime of this frame to be independant of any palette source. + // Copy the palette over. We want the lifetime of this frame to be independent of any palette source. this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length); palette.Span.CopyTo(this.paletteOwner.GetSpan()); this.Palette = this.paletteOwner.Memory[..palette.Length]; diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 72148374aa..1e13e664b6 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -27,7 +27,7 @@ internal sealed class EuclideanPixelMap : IDisposable /// /// Do not make this readonly! Struct value would be always copied on non-readonly method calls. /// - private ColorDistanceCache cache; + private HybridColorDistanceCache cache; private readonly Configuration configuration; /// @@ -51,7 +51,7 @@ public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory pal this.configuration = configuration; this.Palette = palette; this.rgbaPalette = new Rgba32[palette.Length]; - this.cache = new ColorDistanceCache(configuration.MemoryAllocator); + this.cache = new HybridColorDistanceCache(configuration.MemoryAllocator); PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); this.transparentIndex = transparentIndex; @@ -80,13 +80,13 @@ public int GetClosestColor(TPixel color, out TPixel match) color.ToRgba32(ref rgba); // Check if the color is in the lookup table - if (!this.cache.TryGetValue(rgba, out short index)) + if (this.cache.TryGetValue(rgba, out short index)) { - return this.GetClosestColorSlow(rgba, ref paletteRef, out match); + match = Unsafe.Add(ref paletteRef, (ushort)index); + return index; } - match = Unsafe.Add(ref paletteRef, (ushort)index); - return index; + return this.GetClosestColorSlow(rgba, ref paletteRef, out match); } /// @@ -155,6 +155,7 @@ private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel m // Now I have the index, pop it into the cache for next time this.cache.Add(rgba, (byte)index); match = Unsafe.Add(ref paletteRef, (uint)index); + return index; } @@ -177,84 +178,265 @@ private static float DistanceSquared(Rgba32 a, Rgba32 b) public void Dispose() => this.cache.Dispose(); /// - /// A cache for storing color distance matching results. + /// A hybrid cache for color distance lookups that combines an exact-match dictionary with + /// a fallback coarse lookup table. /// /// - /// - /// The granularity of the cache has been determined based upon the current - /// suite of test images and provides the lowest possible memory usage while - /// providing enough match accuracy. - /// Entry count is currently limited to 2335905 entries (4MB). - /// + /// This cache uses a fallback table with 2,097,152 bins, each storing a 2-byte value + /// (approximately 4 MB total), while the exact-match dictionary is limited to 256 entries + /// and occupies roughly 4 KB. Overall, the worst-case memory usage is about 2 MB. + /// Lookups and insertions are performed in constant time (O(1)) because the fallback table + /// is accessed via direct indexing and the dictionary employs a simple hash-based bucket mechanism. + /// The design achieves extremely fast color distance lookups with a predictable, fixed memory footprint. /// - private unsafe struct ColorDistanceCache : IDisposable +#pragma warning disable CA1001 // Types that own disposable fields should be disposable + + // https://github.com/dotnet/roslyn-analyzers/issues/6151 + private unsafe struct HybridColorDistanceCache : IDisposable +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { private const int IndexRBits = 5; private const int IndexGBits = 5; private const int IndexBBits = 5; private const int IndexABits = 6; - private const int IndexRCount = (1 << IndexRBits) + 1; - private const int IndexGCount = (1 << IndexGBits) + 1; - private const int IndexBCount = (1 << IndexBBits) + 1; - private const int IndexACount = (1 << IndexABits) + 1; - private const int RShift = 8 - IndexRBits; - private const int GShift = 8 - IndexGBits; - private const int BShift = 8 - IndexBBits; - private const int AShift = 8 - IndexABits; - private const int Entries = IndexRCount * IndexGCount * IndexBCount * IndexACount; - private MemoryHandle tableHandle; - private readonly IMemoryOwner table; - private readonly short* tablePointer; - - public ColorDistanceCache(MemoryAllocator allocator) + 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 fallbackTable; + private readonly short* fallbackPointer; + private MemoryHandle fallbackHandle; + + private readonly ExactCache exactCache; + + public HybridColorDistanceCache(MemoryAllocator allocator) { - this.table = allocator.Allocate(Entries); - this.table.GetSpan().Fill(-1); - this.tableHandle = this.table.Memory.Pin(); - this.tablePointer = (short*)this.tableHandle.Pointer; + this.fallbackTable = allocator.Allocate(TotalBins); + this.fallbackTable.GetSpan().Fill(-1); + this.fallbackHandle = this.fallbackTable.Memory.Pin(); + this.fallbackPointer = (short*)this.fallbackHandle.Pointer; + + this.exactCache = new ExactCache(allocator); } - [MethodImpl(InliningOptions.ShortMethod)] - public readonly void Add(Rgba32 rgba, byte index) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void Add(Rgba32 color, short index) { - int idx = GetPaletteIndex(rgba); - this.tablePointer[idx] = index; + if (this.exactCache.TryAdd(color.PackedValue, index)) + { + return; + } + + this.fallbackPointer[GetCoarseIndex(color)] = index; } - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 rgba, out short match) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool TryGetValue(Rgba32 color, out short match) { - int idx = GetPaletteIndex(rgba); - match = this.tablePointer[idx]; - return match > -1; + if (this.exactCache.TryGetValue(color.PackedValue, out match)) + { + return true; // Exact match found + } + + match = this.fallbackPointer[GetCoarseIndex(color)]; + return match > -1; // Coarse match found } - /// - /// Clears the cache resetting each entry to empty. - /// - [MethodImpl(InliningOptions.ShortMethod)] - public readonly void Clear() => this.table.GetSpan().Fill(-1); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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; + } - [MethodImpl(InliningOptions.ShortMethod)] - private static int GetPaletteIndex(Rgba32 rgba) + public readonly void Clear() { - int rIndex = rgba.R >> RShift; - int gIndex = rgba.G >> GShift; - int bIndex = rgba.B >> BShift; - int aIndex = rgba.A >> AShift; - - return (aIndex * (IndexRCount * IndexGCount * IndexBCount)) + - (rIndex * (IndexGCount * IndexBCount)) + - (gIndex * IndexBCount) + bIndex; + this.exactCache.Clear(); + this.fallbackTable.GetSpan().Fill(-1); } public void Dispose() { - if (this.table != null) + this.fallbackHandle.Dispose(); + this.fallbackTable.Dispose(); + this.exactCache.Dispose(); + } + } + + /// + /// A fixed-capacity dictionary with exactly 512 entries mapping a key + /// to a value. + /// + /// + /// 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 overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, + /// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are + /// typically very short; in the worst-case, the number of iterations is bounded by 256. + /// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. + /// + internal sealed unsafe class ExactCache : IDisposable + { + // Buckets array: each bucket holds the index (0-based) into the entries array + // of the first entry in the chain, or -1 if empty. + private readonly IMemoryOwner bucketsOwner; + private MemoryHandle bucketsHandle; + private short* buckets; + + // Entries array: stores up to 256 entries. + private readonly IMemoryOwner entriesOwner; + private MemoryHandle entriesHandle; + private Entry* entries; + + public const int Capacity = 512; + + public ExactCache(MemoryAllocator allocator) + { + this.Count = 0; + + // Allocate exactly 512 ints for buckets. + this.bucketsOwner = allocator.Allocate(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; + } + + public int Count { get; private set; } + + /// + /// Adds a key/value pair to the dictionary. + /// If the key already exists, the dictionary is left unchanged. + /// + /// The key to add. + /// The value to add. + /// if the key was added; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(uint key, short value) + { + if (this.Count == Capacity) + { + return false; // Dictionary is full. + } + + // The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A + // (with R in the most significant byte and A in the least significant). + // To compute the bucket index: + // 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels. + // 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A). + // 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A), + // which helps to counteract situations where one or more channels have a limited range. + // 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511, + // which corresponds to our fixed bucket count of 512. + int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); + int i = this.buckets[bucket]; + + // Traverse the collision chain. + Entry* entries = this.entries; + while (i != -1) + { + Entry e = entries[i]; + if (e.Key == key) + { + // Key already exists; do not overwrite. + return false; + } + + i = e.Next; + } + + short index = (short)this.Count; + this.Count++; + + // Insert the new entry: + entries[index].Key = key; + entries[index].Value = value; + + // Link this new entry into the bucket chain. + entries[index].Next = this.buckets[bucket]; + this.buckets[bucket] = index; + return true; + } + + /// + /// Tries to retrieve the value associated with the specified key. + /// Returns true if the key is found; otherwise, returns false. + /// + /// The key to search for. + /// The value associated with the key, if found. + /// if the key is found; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(uint key, out short value) + { + int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); + int i = this.buckets[bucket]; + + // If the bucket is empty, return immediately. + if (i == -1) { - this.tableHandle.Dispose(); - this.table.Dispose(); + value = -1; + return false; + } + + // Traverse the chain. + Entry* entries = this.entries; + do + { + Entry e = entries[i]; + if (e.Key == key) + { + value = e.Value; + return true; + } + + i = e.Next; } + while (i != -1); + + value = -1; + return false; + } + + /// + /// Clears the dictionary. + /// + public void Clear() + { + Span bucketSpan = this.bucketsOwner.GetSpan(); + bucketSpan.Fill(-1); + this.Count = 0; + } + + public void Dispose() + { + this.bucketsHandle.Dispose(); + this.bucketsOwner.Dispose(); + this.entriesHandle.Dispose(); + this.entriesOwner.Dispose(); + this.buckets = null; + this.entries = null; + } + + private struct Entry + { + public uint Key; // The key (packed RGBA) + public short Value; // The value; -1 means unused. + public short Next; // Index of the next entry in the chain, or -1 if none. } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index fe422882bc..84124056cd 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -60,38 +60,43 @@ public OctreeQuantizer(Configuration configuration, QuantizerOptions options) public QuantizerOptions Options { get; } /// - public readonly ReadOnlyMemory Palette + public ReadOnlyMemory Palette { get { - QuantizerUtilities.CheckPaletteState(in this.palette); + if (this.palette.IsEmpty) + { + this.ResolvePalette(); + QuantizerUtilities.CheckPaletteState(in this.palette); + } + return this.palette; } } /// - public void AddPaletteColors(Buffer2DRegion pixelRegion) + public readonly void AddPaletteColors(Buffer2DRegion pixelRegion) { - using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(pixelRegion.Width)) + using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(pixelRegion.Width); + Span bufferSpan = buffer.GetSpan(); + + // Loop through each row + for (int y = 0; y < pixelRegion.Height; y++) { - Span bufferSpan = buffer.GetSpan(); + Span row = pixelRegion.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); - // Loop through each row - for (int y = 0; y < pixelRegion.Height; y++) + for (int x = 0; x < bufferSpan.Length; x++) { - Span row = pixelRegion.DangerousGetRowSpan(y); - PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); - - for (int x = 0; x < bufferSpan.Length; x++) - { - Rgba32 rgba = bufferSpan[x]; - - // Add the color to the Octree - this.octree.AddColor(rgba); - } + // Add the color to the Octree + this.octree.AddColor(bufferSpan[x]); } } + } + [MemberNotNull(nameof(pixelMap))] + private void ResolvePalette() + { int paletteIndex = 0; Span paletteSpan = this.paletteOwner.GetSpan(); @@ -109,17 +114,7 @@ public void AddPaletteColors(Buffer2DRegion pixelRegion) this.octree.Palletize(paletteSpan, max, ref paletteIndex); ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; - // When called multiple times by QuantizerUtilities.BuildPalette - // this prevents memory churn caused by reallocation. - if (this.pixelMap is null) - { - this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - } - else - { - this.pixelMap.Clear(result); - } - + this.pixelMap = new EuclideanPixelMap(this.Configuration, result); this.palette = result; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index f6928c3dd4..183a2bad9a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -111,35 +111,43 @@ public WuQuantizer(Configuration configuration, QuantizerOptions options) public QuantizerOptions Options { get; } /// - public readonly ReadOnlyMemory Palette + public ReadOnlyMemory Palette { get { - QuantizerUtilities.CheckPaletteState(in this.palette); + if (this.palette.IsEmpty) + { + this.ResolvePalette(); + QuantizerUtilities.CheckPaletteState(in this.palette); + } + return this.palette; } } /// - public void AddPaletteColors(Buffer2DRegion pixelRegion) + public readonly void AddPaletteColors(Buffer2DRegion pixelRegion) + => this.Build3DHistogram(pixelRegion); + + /// + /// Once all histogram data has been accumulated, this method computes the moments, + /// splits the color cube, and resolves the final palette from the accumulated histogram. + /// + private void ResolvePalette() { - // TODO: Something is destroying the existing palette when adding new colors. - // When the QuantizingImageEncoder.PixelSamplingStrategy is DefaultPixelSamplingStrategy - // this leads to performance issues + the palette is not preserved. - // https://github.com/SixLabors/ImageSharp/issues/2498 - this.Build3DHistogram(pixelRegion); + // Calculate the cumulative moments from the accumulated histogram. this.Get3DMoments(this.memoryAllocator); + + // Partition the histogram into color cubes. this.BuildCube(); - // Slice again since maxColors has been updated since the buffer was created. + // Compute the palette colors from the resolved cubes. Span paletteSpan = this.paletteOwner.GetSpan()[..this.maxColors]; ReadOnlySpan momentsSpan = this.momentsOwner.GetSpan(); for (int k = 0; k < paletteSpan.Length; k++) { this.Mark(ref this.colorCube[k], (byte)k); - Moment moment = Volume(ref this.colorCube[k], momentsSpan); - if (moment.Weight > 0) { ref TPixel color = ref paletteSpan[k]; @@ -147,22 +155,14 @@ public void AddPaletteColors(Buffer2DRegion pixelRegion) } } - ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; - if (this.isDithering) + // Update the palette to the new computed colors. + this.palette = this.paletteOwner.Memory[..paletteSpan.Length]; + + // Create the pixel map if dithering is enabled. + if (this.isDithering && this.pixelMap is null) { - // When called multiple times by QuantizerUtilities.BuildPalette - // this prevents memory churn caused by reallocation. - if (this.pixelMap is null) - { - this.pixelMap = new EuclideanPixelMap(this.Configuration, result); - } - else - { - this.pixelMap.Clear(result); - } + this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette); } - - this.palette = result; } /// @@ -549,7 +549,7 @@ private readonly float Maximize(ref Box cube, int direction, int first, int last /// The first set. /// The second set. /// Returns a value indicating whether the box has been split. - private bool Cut(ref Box set1, ref Box set2) + private readonly bool Cut(ref Box set1, ref Box set2) { ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment whole = Volume(ref set1, momentSpan); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index f396239f53..1e152703ab 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -391,8 +391,6 @@ public void GifEncoder_CanDecode_Issue2866(TestImageProvider pro { using Image image = provider.GetImage(); - bool anyGlobal = ((IEnumerable)image.Frames).Any(x => x.Metadata.GetGifMetadata().ColorTableMode == GifColorTableMode.Global); - // image.DebugSaveMultiFrame(provider); provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index ef95c59389..db4290c175 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -484,12 +484,12 @@ public void Encode_APng(TestImageProvider provider) } [Theory] - [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32)] - [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32)] - public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider) + [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.613F)] + [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32, 1.06F)] + public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider, float percentage) where TPixel : unmanaged, IPixel { - if (TestEnvironment.RunsOnCI && !TestEnvironment.IsWindows) + if (TestEnvironment.RunsOnCI) { return; } @@ -499,12 +499,14 @@ public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider output = Image.Load(memStream); // TODO: Find a better way to compare. - // The image has been visually checked but the quantization pattern used in the png encoder - // means we cannot use an exact comparison nor replicate using the quantizing processor. - ImageComparer.TolerantPercentage(0.613f).VerifySimilarity(output, image); + // The image has been visually checked but the coarse cache used by the palette quantizer + // can lead to minor differences between frames. + ImageComparer.TolerantPercentage(percentage).VerifySimilarity(output, image); GifMetadata gif = image.Metadata.GetGifMetadata(); PngMetadata png = output.Metadata.GetPngMetadata(); @@ -699,6 +701,35 @@ public void Issue2668_Quantized_Encode_Alpha(TestImageProvider p encoded.CompareToReferenceOutput(ImageComparer.Exact, provider); } + [Fact] + public void Issue_2862() + { + // Create a grayscale palette (or any other palette with colors that are very close to each other): + Rgba32[] palette = Enumerable.Range(0, 256).Select(i => new Rgba32((byte)i, (byte)i, (byte)i)).ToArray(); + + using Image image = new(254, 4); + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = palette[x]; + } + } + + using MemoryStream ms = new(); + image.Save(ms, new PngEncoder + { + ColorType = PngColorType.Palette, + BitDepth = PngBitDepth.Bit8, + Quantizer = new PaletteQuantizer(palette.Select(Color.FromPixel).ToArray()) + }); + + ms.Position = 0; + + using Image encoded = Image.Load(ms); + ImageComparer.Exact.VerifySimilarity(image, encoded); + } + private static void TestPngEncoderCore( TestImageProvider provider, PngColorType pngColorType, diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs index a3fe028db5..1491cd13cf 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs @@ -106,7 +106,7 @@ private static void RunCheckNoneOpaqueWithNoneOpaquePixelsTest() 174, 183, 189, 255, 148, 158, 158, 255, }; - Span row = MemoryMarshal.Cast(rowBytes); + ReadOnlySpan row = MemoryMarshal.Cast(rowBytes); bool noneOpaque; for (int length = 8; length < row.Length; length += 8) @@ -188,7 +188,7 @@ private static void RunCheckNoneOpaqueWithOpaquePixelsTest() 174, 183, 189, 255, 148, 158, 158, 255, }; - Span row = MemoryMarshal.Cast(rowBytes); + ReadOnlySpan row = MemoryMarshal.Cast(rowBytes); bool noneOpaque; for (int length = 8; length < row.Length; length += 8) diff --git a/tests/ImageSharp.Tests/Image/ImageFrameTests.cs b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs index 3b9779ea42..baa8ca699a 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs @@ -119,7 +119,7 @@ public void CopyPixelDataTo_Success(bool disco, bool byteSpan) } else { - Span destination = MemoryMarshal.Cast(actual); + Span destination = MemoryMarshal.Cast(actual.AsSpan()); image.Frames.RootFrame.CopyPixelDataTo(destination); } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index ca51f7f5cb..6d68e1ab94 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -197,7 +197,7 @@ public void CopyPixelDataTo_Success(bool disco, bool byteSpan) } else { - Span destination = MemoryMarshal.Cast(actual); + Span destination = MemoryMarshal.Cast(actual.AsSpan()); image.CopyPixelDataTo(destination); } From 6da9bc353805645c9da8f72a5f1ccf5c9a4d92a1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Feb 2025 10:10:08 +1000 Subject: [PATCH 10/22] Fix transparency mode, update quantized refs --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 57 ++++++++++++------- src/ImageSharp/Memory/Buffer2DExtensions.cs | 33 +++++++++++ ...de_8BitColor_WithOctreeQuantizer_rgb32.bmp | 2 +- ...Encode_8BitColor_WithWuQuantizer_rgb32.bmp | 2 +- ...onFilterInBox_Rgba32_CalliphoraPartial.png | 4 +- ...rksWithAllErrorDiffusers_Bike_Atkinson.png | 4 +- ..._WorksWithAllErrorDiffusers_Bike_Burks.png | 4 +- ...hAllErrorDiffusers_Bike_FloydSteinberg.png | 4 +- ...lErrorDiffusers_Bike_JarvisJudiceNinke.png | 4 +- ...orksWithAllErrorDiffusers_Bike_Sierra2.png | 4 +- ...orksWithAllErrorDiffusers_Bike_Sierra3.png | 4 +- ...sWithAllErrorDiffusers_Bike_SierraLite.png | 4 +- ...thAllErrorDiffusers_Bike_StevensonArce.png | 4 +- ...WorksWithAllErrorDiffusers_Bike_Stucki.png | 4 +- ...orDiffusers_CalliphoraPartial_Atkinson.png | 4 +- ...ErrorDiffusers_CalliphoraPartial_Burks.png | 4 +- ...users_CalliphoraPartial_FloydSteinberg.png | 4 +- ...rs_CalliphoraPartial_JarvisJudiceNinke.png | 4 +- ...rorDiffusers_CalliphoraPartial_Sierra2.png | 4 +- ...rorDiffusers_CalliphoraPartial_Sierra3.png | 4 +- ...Diffusers_CalliphoraPartial_SierraLite.png | 4 +- ...fusers_CalliphoraPartial_StevensonArce.png | 4 +- ...rrorDiffusers_CalliphoraPartial_Stucki.png | 4 +- ...DependOnSinglePixelType_Bgra32_filter0.png | 4 +- ...tDependOnSinglePixelType_Rgb24_filter0.png | 4 +- ...DependOnSinglePixelType_Rgba32_filter0.png | 4 +- ...ndOnSinglePixelType_RgbaVector_filter0.png | 4 +- ..._WorksWithAllDitherers_Bike_Bayer16x16.png | 4 +- ...er_WorksWithAllDitherers_Bike_Bayer2x2.png | 4 +- ...er_WorksWithAllDitherers_Bike_Bayer4x4.png | 4 +- ...er_WorksWithAllDitherers_Bike_Bayer8x8.png | 4 +- ..._WorksWithAllDitherers_Bike_Ordered3x3.png | 4 +- ...Ditherers_CalliphoraPartial_Bayer16x16.png | 4 +- ...llDitherers_CalliphoraPartial_Bayer2x2.png | 4 +- ...llDitherers_CalliphoraPartial_Bayer4x4.png | 4 +- ...llDitherers_CalliphoraPartial_Bayer8x8.png | 4 +- ...Ditherers_CalliphoraPartial_Ordered3x3.png | 4 +- ...zed_Encode_Artifacts_Rgba32_issue_2469.png | 4 +- ...antized_Encode_Alpha_Rgba32_Issue_2668.png | 4 +- ...InBox_Bike_OctreeQuantizer_ErrorDither.png | 4 +- ...Box_Bike_OctreeQuantizer_OrderedDither.png | 4 +- ...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 +- ...onInBox_Bike_WuQuantizer_OrderedDither.png | 4 +- ...oraPartial_OctreeQuantizer_ErrorDither.png | 4 +- ...aPartial_OctreeQuantizer_OrderedDither.png | 4 +- ...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 +- ...phoraPartial_WuQuantizer_OrderedDither.png | 4 +- ...david_OctreeQuantizer_ErrorDither_0.25.png | 4 +- ..._david_OctreeQuantizer_ErrorDither_0.5.png | 4 +- ...david_OctreeQuantizer_ErrorDither_0.75.png | 4 +- ...le_david_OctreeQuantizer_ErrorDither_0.png | 4 +- ...le_david_OctreeQuantizer_ErrorDither_1.png | 4 +- ...vid_OctreeQuantizer_OrderedDither_0.25.png | 4 +- ...avid_OctreeQuantizer_OrderedDither_0.5.png | 4 +- ...vid_OctreeQuantizer_OrderedDither_0.75.png | 4 +- ..._david_OctreeQuantizer_OrderedDither_0.png | 4 +- ..._david_OctreeQuantizer_OrderedDither_1.png | 4 +- ...bSafePaletteQuantizer_ErrorDither_0.25.png | 4 +- ...ebSafePaletteQuantizer_ErrorDither_0.5.png | 4 +- ...bSafePaletteQuantizer_ErrorDither_0.75.png | 4 +- ..._WebSafePaletteQuantizer_ErrorDither_0.png | 4 +- ..._WebSafePaletteQuantizer_ErrorDither_1.png | 4 +- ...afePaletteQuantizer_OrderedDither_0.25.png | 4 +- ...SafePaletteQuantizer_OrderedDither_0.5.png | 4 +- ...afePaletteQuantizer_OrderedDither_0.75.png | 4 +- ...ebSafePaletteQuantizer_OrderedDither_0.png | 4 +- ...ebSafePaletteQuantizer_OrderedDither_1.png | 4 +- ...ernerPaletteQuantizer_ErrorDither_0.25.png | 4 +- ...WernerPaletteQuantizer_ErrorDither_0.5.png | 4 +- ...ernerPaletteQuantizer_ErrorDither_0.75.png | 4 +- ...d_WernerPaletteQuantizer_ErrorDither_0.png | 4 +- ...d_WernerPaletteQuantizer_ErrorDither_1.png | 4 +- ...nerPaletteQuantizer_OrderedDither_0.25.png | 4 +- ...rnerPaletteQuantizer_OrderedDither_0.5.png | 4 +- ...nerPaletteQuantizer_OrderedDither_0.75.png | 4 +- ...WernerPaletteQuantizer_OrderedDither_0.png | 4 +- ...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 +- ...le_david_WuQuantizer_OrderedDither_0.5.png | 4 +- ...e_david_WuQuantizer_OrderedDither_0.75.png | 4 +- ...cale_david_WuQuantizer_OrderedDither_0.png | 4 +- ...cale_david_WuQuantizer_OrderedDither_1.png | 4 +- ...ation_Bike_OctreeQuantizer_ErrorDither.png | 4 +- ...ion_Bike_OctreeQuantizer_OrderedDither.png | 4 +- ...ke_WebSafePaletteQuantizer_ErrorDither.png | 4 +- ..._Bike_WebSafePaletteQuantizer_NoDither.png | 4 +- ..._WebSafePaletteQuantizer_OrderedDither.png | 4 +- ...ike_WernerPaletteQuantizer_ErrorDither.png | 4 +- ...n_Bike_WernerPaletteQuantizer_NoDither.png | 4 +- ...e_WernerPaletteQuantizer_OrderedDither.png | 4 +- ...ntization_Bike_WuQuantizer_ErrorDither.png | 4 +- ...Quantization_Bike_WuQuantizer_NoDither.png | 4 +- ...ization_Bike_WuQuantizer_OrderedDither.png | 4 +- ...oraPartial_OctreeQuantizer_ErrorDither.png | 4 +- ...iphoraPartial_OctreeQuantizer_NoDither.png | 4 +- ...aPartial_OctreeQuantizer_OrderedDither.png | 4 +- ...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 +- 122 files changed, 307 insertions(+), 259 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ee4781db47..24ac45a3fa 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -182,7 +182,8 @@ public void Encode(Image image, Stream stream, CancellationToken if (clearTransparency) { currentFrame = clonedFrame = currentFrame.Clone(); - ClearTransparentPixels(currentFrame, this.backgroundColor.Value); + currentFrameRegion = currentFrame.PixelBuffer.GetRegion(); + ClearTransparentPixels(in currentFrameRegion, this.backgroundColor.Value); } // Do not move this. We require an accurate bit depth for the header chunk. @@ -217,7 +218,7 @@ public void Encode(Image image, Stream stream, CancellationToken { cancellationToken.ThrowIfCancellationRequested(); FrameControl frameControl = new((uint)this.width, (uint)this.height); - this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); + this.WriteDataChunks(in frameControl, in currentFrameRegion, quantized, stream, false); currentFrameIndex++; } @@ -286,9 +287,10 @@ public void Encode(Image image, Stream stream, CancellationToken background, blend); + Buffer2DRegion encodingFrameRegion = encodingFrame.PixelBuffer.GetRegion(bounds); if (clearTransparency) { - ClearTransparentPixels(encodingFrame, background); + ClearTransparentPixels(in encodingFrameRegion, background); } // Each frame control sequence number must be incremented by the number of frame data chunks that follow. @@ -308,7 +310,6 @@ public void Encode(Image image, Stream stream, CancellationToken paletteQuantizer, default); - Buffer2DRegion encodingFrameRegion = encodingFrame.PixelBuffer.GetRegion(bounds); sequenceNumber += this.WriteDataChunks(frameControl, in encodingFrameRegion, quantized, stream, true) + 1; previousFrame = currentFrame; @@ -392,27 +393,26 @@ private static PngFrameMetadata GetPngFrameMetadata(ImageFrame f /// The type of the pixel. /// The cloned image frame where the transparent pixels will be changed. /// The color to change transparent pixels to. - private static void ClearTransparentPixels(ImageFrame clone, Color color) + private static void ClearTransparentPixels(in Buffer2DRegion clone, Color color) where TPixel : unmanaged, IPixel - => clone.ProcessPixelRows(accessor => + { + Rgba32 rgba32 = default; + Rgba32 transparent = color; + for (int y = 0; y < clone.Height; y++) { - // TODO: We should be able to speed this up with SIMD and masking. - Rgba32 rgba32 = default; - Rgba32 transparent = color; - for (int y = 0; y < accessor.Height; y++) + Span row = clone.DangerousGetRowSpan(y); + for (int x = 0; x < row.Length; x++) { - Span span = accessor.GetRowSpan(y); - for (int x = 0; x < accessor.Width; x++) - { - span[x].ToRgba32(ref rgba32); + ref TPixel pixel = ref row[x]; + pixel.ToRgba32(ref rgba32); - if (rgba32.A is 0) - { - span[x].FromRgba32(transparent); - } + if (rgba32.A is 0) + { + pixel.FromRgba32(transparent); } } - }); + } + } /// /// Creates the quantized image and calculates and sets the bit depth. @@ -1595,7 +1595,7 @@ private void SanitizeAndSetEncoderOptions( /// The quantizer containing any previously derived palette. /// The background color. private IndexedImageFrame? CreateQuantizedFrame( - QuantizingImageEncoder encoder, + PngEncoder encoder, PngColorType colorType, byte bitDepth, PngMetadata metadata, @@ -1667,7 +1667,22 @@ private void SanitizeAndSetEncoderOptions( frameQuantizer.AddPaletteColors(px.GetRegion()); } - frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); + if (encoder.TransparentColorMode == PngTransparentColorMode.Clear) + { + foreach (Buffer2DRegion region in encoder.PixelSamplingStrategy.EnumeratePixelRegions(image)) + { + using Buffer2D clone = region.Buffer.CloneRegion(this.configuration, region.Rectangle); + Buffer2DRegion clonedRegion = clone.GetRegion(); + + ClearTransparentPixels(in clonedRegion, backgroundColor); + frameQuantizer.AddPaletteColors(clonedRegion); + } + } + else + { + frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, image); + } + return frameQuantizer.QuantizeFrame(frame, bounds); } diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 2eb05ea935..ffddfcbd0e 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -25,6 +25,39 @@ public static IMemoryGroup GetMemoryGroup(this Buffer2D buffer) return buffer.FastMemoryGroup.View; } + /// + /// Performs a deep clone of the buffer covering the specified . + /// + /// The element type. + /// The source buffer. + /// The configuration. + /// The rectangle to clone. + /// The . + internal static Buffer2D CloneRegion(this Buffer2D source, Configuration configuration, Rectangle rectangle) + where T : unmanaged + { + Buffer2D buffer = configuration.MemoryAllocator.Allocate2D( + rectangle.Width, + rectangle.Height, + configuration.PreferContiguousImageBuffers); + + // Optimization for when the size of the area is the same as the buffer size. + Buffer2DRegion sourceRegion = source.GetRegion(rectangle); + if (sourceRegion.IsFullBufferArea) + { + sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup); + } + else + { + for (int y = 0; y < rectangle.Height; y++) + { + sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y)); + } + } + + return buffer; + } + /// /// TODO: Does not work with multi-buffer groups, should be specific to Resize. /// Copy columns of inplace, diff --git a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp index 2b8e05b070..6ce22169e2 100644 --- a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp +++ b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d +oid sha256:6785d108cab9e3b508e98ce10cef383037471beb2a6d14a69df6069b5fbf5f3d size 9270 diff --git a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp index f7eb06c558..c69ed3130c 100644 --- a/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp +++ b/tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e063e97cd8a000de6830adcc3961a7dc41785d40cd4d83af10ca38d96e071362 +oid sha256:5d9f2745de2b6e7fc3b1403fe651f3bbba835c67a6fb410fc8a9d91a15b44328 size 9270 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png b/tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png index de42d1bfc2..401ceaa36c 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:681b0e36298cb702683fb9ffb2a82f7dfd9080b268db19a03f413809f69d0e07 -size 273269 +oid sha256:596472e74050d968479b672c1d2436b179e41a7b99fcefb53286ad47e5a4fe13 +size 273115 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png index 1656b2e9cb..0b03e4e185 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38597c6144d61960d25c74d7a465b1cdf69b7c0804a6dec68128a6c953258313 -size 52688 +oid sha256:2465dde9a5d6202194f7af3924ca24ab3151948d551549a711977d3302dbc0a3 +size 51158 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png index c6016ae358..1d27fab771 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f9191c71eea1f73aa4c55397ca26f240615c9c4a7fff9a05e6f2e046b5e4d8b -size 62323 +oid sha256:82dcdd4f28a9ffafd36a21d06aee8adb49017df2d4abeee4205d65b1ae3df35e +size 59875 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png index 40243937d3..51c93894de 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b63810145832db459bb7a6b37a028a7b778f6b6b4e6eae00e50e6e21c5a06086 -size 62199 +oid sha256:a836c8efd7aa9818cf807cf56412e78399a6568798be23d0f3f6b89552856ff1 +size 62172 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png index 83f9e067db..de9ef47037 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a67c14ef99a943706f050ff1ea0ef101429292d52bc14ed4610f8338736ff87e -size 56800 +oid sha256:78900d779181140a02a2b9fb9fa922ca854d9905c1dc7e006592a3fdc00f8dee +size 58107 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png index 22e4f4b6d6..bb62475c97 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:623dd82d372ba517b0d3357d06cffaf105d407a9090cbcbc6a76ae944ab33d67 -size 59468 +oid sha256:76f10d4280258d2941d85e795cf788977ca1e85bdc1b75b5a482b5bbdaa49d32 +size 57900 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png index 838863c158..2d99410117 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8edceef8e12c4f3d194523437045c5cf4e80c7bb95ff75f38c1f38a21872e3d0 -size 59376 +oid sha256:aba9172bb4d117ba1b0c5f32b46251d473cc06b3f697e5729da0c5768a70b5d2 +size 59104 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png index 60513e1992..bd3295e424 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1d7019e8cb170ae67496f8250446c4f6b6217378658408c3d51a95c49a4c3bc -size 63287 +oid sha256:e7d6ea824ba19632afa940b3062632d305bf3521b1795d46f3fea90abc1f0ed8 +size 64431 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png index 0d1b34d8ce..6bea03c0be 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7c03ede7ab3bd4e57e6a63e53e2e8c771e938fdc7d5dfe5c9339a2c9907c9cf -size 55550 +oid sha256:3efcf6f924d3d07cad9dbf9dddb6104c3748ac4354298acf5afde66c2321e819 +size 55358 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png index f8c998ecbd..b535b14155 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79b690b91223d1fe7ddf1b8826b4474b89644822bc8aa9adee3cf819bc095b4c -size 60979 +oid sha256:2b9f295f6b539fbeeae3c473907fa450f9b8c94017abad4bf915a8a4a2e7b612 +size 56982 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png index cc2327b23f..e0334be2d5 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e22401dddf6552cd91517c1cdd142d3b9a66a7ad5c80d2e52ae07a7f583708e -size 57657 +oid sha256:c4c45632b6cd387c929a9e0982f3943a7c3f64f27862c0b539bbf71228561f39 +size 57886 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png index e3ae6508e1..db22e17585 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:819a0ce38e27e2adfa454d8c5ad5b24e818bf8954c9f2406f608dcecf506c2c4 -size 59838 +oid sha256:bf9e8bd50b62ba62ab04a5ab2af207414183a015567080fa7cdd827016694369 +size 60458 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png index 2b897a5d6d..2d6226d096 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:007ac609ec61b39c7bdd04bc87a698f5cdc76eadd834c1457f41eb9c135c3f7b -size 60688 +oid sha256:89864a77216b51cc5b9415453ade7f7ec64c1c112546aa47ee6b4b89f9b258a3 +size 60543 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png index 10ba90ae86..4d9b22139e 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_JarvisJudiceNinke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46892c07e9a93f1df71f0e38b331a437fb9b7c52d8f40cf62780cb6bd35d3b13 -size 58963 +oid sha256:0c03c3dc0b3da69ef4f55b5ad6d162da94ad46f4e426e318695bedc7e5bb3dfd +size 58725 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png index 9608289e84..3b7c78182a 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b83345ca3de8d1fc0fbb5d8e68329b94ad79fc29b9f10a1392a97ffe9a0733e -size 58985 +oid sha256:af86b108639f833972958fd2cc7d00221982069c40cab67b5bc6b8ce1a7e826d +size 59137 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png index 79d2c5eb14..b56e3ccfd6 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Sierra3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c775a5b19ba09e1b335389e0dc12cb0c3feaff6072e904da750a676fcd6b07dc -size 59202 +oid sha256:959b49f5498e4018bfb8a5fac8a688c51b06161dc0c6559547293c613ddca760 +size 59248 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png index 8d3cf1a564..78078ac90d 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_SierraLite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c88740c0553829eaa42ca751b34cc456623a84ccdff4020949a06ef4b4802d1 -size 61137 +oid sha256:fdc28c281666e381c7ba2483d033f73c88111f13eec10cc406e07730eb5fa709 +size 60804 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png index a146f8f668..523a87b7a5 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_StevensonArce.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a4a404b0767faac952435f768867cf7bf053848e1e3ef121624f136658a107c -size 58386 +oid sha256:c80f215d4a839fb1ca722d03923b587bac6326d54d2d7a3656667e46464b4307 +size 58011 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png index edec46a92a..741ac096a3 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Stucki.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cc216ed952216d203836dc559234216614f1ed059651677cc0ea714010bd932 -size 58855 +oid sha256:38fbfc201e8ef31b879e863f7f49ac1e731c4d7dfca58a80e1e45890565af979 +size 58742 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png index e2e4147f68..91ff81d5df 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 -size 871 +oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc +size 875 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png index aa0e9a4824..91ff81d5df 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb3e3b9b3001e76505fb0e2db7ad200cad2a016c06f1993c60c3cab42c134863 -size 867 +oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc +size 875 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png index e2e4147f68..91ff81d5df 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 -size 871 +oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc +size 875 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png index e2e4147f68..91ff81d5df 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 -size 871 +oid sha256:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc +size 875 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png index e899ffb42a..537b367eaa 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca70bb0200776efd00c4ef7596d4e1f2f5fbc68e447b395b25ef2b3c732e5156 -size 44189 +oid sha256:f22d61e15a46b5ff3a01b1eed3cf7b6ee4970a86b56a66e5a44914962946792c +size 44381 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png index 543640c2e8..f623d52ac7 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8474b847b7d4a8f3e5c9793ca257ce46efcf49c473c731a9ca9c759851410b94 -size 43066 +oid sha256:ea34b188ce71a8fbd76fddf052fc1322fff62ba0acc218582b996d9b00c81671 +size 42667 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png index fec3c9b2b3..120b569fdb 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20e80e7d9e68fd85bfbc63c61953327354b0634000ec142e01a42618995fd14c -size 44391 +oid sha256:dc335434b535cc26bf16f7233bdb4606ebff3b95ae3e900e0e4b2dffcae652ca +size 44159 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png index 68a95a0540..0386f0e33f 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8af98bfcc5edef3f3ff33ee8f76f33ce2906a6677167e2b29e1dbe63b00a78d8 -size 44202 +oid sha256:be3277c05d05314653976167612ef2bb5f19f8cf3159eca5122878da0f0c0fcd +size 44391 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png index d67f02dca5..38b9f87df6 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b149ebbd550808ae46ff05b5ddcdb1fc0eb6ae0eacbe048e9a1ff24368d8f64d -size 45003 +oid sha256:9dbd000634c3aad9b31166ef070f337fc3aa2ff13a9afea88e2a4d880280b960 +size 45061 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png index 4175cf40b7..62ebd483b8 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9316cbbcb137ae6ff31646f6a5ba1d0aec100db4512509f7684187e74d16a111 -size 51074 +oid sha256:b58e70ae7f38d9f25fb701b9ab5d01a20ef61504dc11f3cf4e1d232a2e09e96e +size 51204 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png index 11d916bdc3..84176ad9b3 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08c39a43993deadebab21f1d3504027b5910a52adc437c167d77d62e5f5db46e -size 52762 +oid sha256:968bba323acfabd9b1b02001e5b37047f6ab7fb7dae8c781eed2f84771beb9c9 +size 52812 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png index a4f91b3301..5d05f354ee 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c9c47fa755d603f8c148011511ee91f32444e0d94367f9db57593e3bf30f2e0 -size 51808 +oid sha256:9e3102436373a8238c2156caeb1b6479fc4651c0018c28c148bb3965b36fe13a +size 51779 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png index ac56fa9236..82ba28be09 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d2289ed4fa0c679f0f120d260fec8ab40b1599043cc0a1fbebc6b67e238ff87 -size 51428 +oid sha256:1366d7c4e9770bceed30aecd894b6e947f822f8dd74f5d231f013167c43ad3c4 +size 51553 diff --git a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png index 9a7c7b4611..b03713b402 100644 --- a/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png +++ b/tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Ordered3x3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:366e84ab8587735455798651096d2af5f965fc325f4852dc68356e94600598b1 -size 52176 +oid sha256:3bd7d03172aa7f70afe5122ef5e582dcc370523db15661455cf512119be67b7f +size 52226 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..16f8c9e4a1 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:c4c4de6d55bea2684ff268ab98aa5c8ed3ea9b6fe8bc976b9116d947c661429e +size 930843 diff --git a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png index 7af5391f70..8683948b8f 100644 --- a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png +++ b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f934af128b85b9e8f557d71ac8b1f1473a0922d0754fc0c4ece0d0e3d8d94c39 -size 7702 +oid sha256:9c32480edf4e42228593cd34ce4906823aeea2f85542c7f80a314e68200a49b0 +size 7706 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png index 4948c7adee..d40e549366 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a51d04953c1c82d99884af62912d2271108c6bc62f18d4b32d0b5290c01fa7f7 -size 247462 +oid sha256:9a1c1376d76bbd1ee2f798ffbe368dc6e8dd8428a9f87289660e46eadc471935 +size 247122 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png index 223d3bc012..bdaa1b2f3d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_OctreeQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34eaa0696da00838e591b2c48e7797641521f7f3feb01abbd774591c4dd6f200 -size 265546 +oid sha256:a47fea2454050baa9105fcdf9992ed97dfaa1ca470c2a5b950566a94ff926be4 +size 265635 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..0f3d653f43 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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e +size 216246 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..0f3d653f43 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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e +size 216246 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..58a94f3522 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:265fa873f693e2ffbfb049161927ca6a286509c5c8c8d700182d488876975535 +size 226823 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..ec95754094 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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853 +size 220689 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..ec95754094 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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853 +size 220689 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..05cb5f7bc8 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:fa84eb63b480314a5ec2439892547d25d4fda1414aac027e397aed4df5b8ea2d +size 231247 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..52a297cdf6 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:14b8be6579cea0742be6ab1d8a44b7fc7f7acc26698692dbe445435f1fa2e48a +size 262707 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_OrderedDither.png index 76946ee06f..c85f05eb07 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_Bike_WuQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:457a0b4e27a09440ff4e13792b68fb5a9da82b7ce6129ea15a5ea8dcd99bd522 -size 274300 +oid sha256:c344c9c5c0585fcde88c71d960dac7ab2867d3d321bfb769fc80e032bf0b940f +size 274578 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png index ebb9ff6b00..ef32d86a0c 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f414473561bfa792c2e6342ff5e5dddffbdec5286932781b11a093803593b52a -size 313787 +oid sha256:2dc1e0677992a8c513d4a7353e66a1365f6b844b5e8957575c2686d0dd8439f9 +size 313198 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png index 5626fa1b83..ac940c161f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62cdce27fc46a38a16995df8ed1501f65091d69315288479b1d613b1d87c8239 -size 321123 +oid sha256:e3d3a6474191e2a5ad8de6df6e985f99244c24e77cfbbe18448b9d26ebd9baa9 +size 320811 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..f33e66693b 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:09d4a269df15a78b84c94a22788b0cdbe187852979ad59afff886a98a66282c4 +size 269323 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..f33e66693b 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:09d4a269df15a78b84c94a22788b0cdbe187852979ad59afff886a98a66282c4 +size 269323 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..ec4d4c7346 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:2f60545a9a4c34b69ad78d15361b55f9e44ab2c20ab51d9bf8db6959d3d1aa10 +size 270815 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..9df2f13dbb 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:ae0a724189324a60c9f68014f4c178add91aecf7dbe49a1ca2c01c10816927a4 +size 284288 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..9df2f13dbb 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:ae0a724189324a60c9f68014f4c178add91aecf7dbe49a1ca2c01c10816927a4 +size 284288 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..dc55c1d35e 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:0581c9cd38c34edb9835cf5a84f5245583c0f14cd25d3ff952986dd9411cff99 +size 285131 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..64560574a6 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:a446d9dba2cdcfd336847e6a475872a1e1c99f204a9aba7aed0e4da282e0e9dd +size 317073 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..e3298ba162 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:4d30407a28495414d113e82abb60ef1762cbee8fddb667df14b08a1280640274 +size 323568 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 index 82b965123d..8b9efe3364 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bed69d43856ebd4b1af4055f8d3aacabd50c361a4e1e1f9cad080d799d6b744 -size 13853 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 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 index 571b0db4b9..8b9efe3364 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c8c8393708002f06f9d8ed1ff8979db820035585c08b66ae463d94724fa64d3 -size 14330 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 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 index a1b3da6816..8b9efe3364 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fda13875f4c762a95001426487cc04c9add39821eb793168fdbe5cc18e705643 -size 14566 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 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 index 82b965123d..8b9efe3364 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bed69d43856ebd4b1af4055f8d3aacabd50c361a4e1e1f9cad080d799d6b744 -size 13853 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 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 index e0fc792026..8b9efe3364 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_ErrorDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb826afb127fe4175e6e47253b8a8313b9d10aee193c316731f34e5d327a2591 -size 14580 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png index 491847e491..dc61aff866 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37018ecc499651833208d846a0f446db94cc11eae002ab6e7ce45b3e7c09e86c -size 17734 +oid sha256:985e054407907131d536b9852c966a839dc4ca27d9d1851bae247852cda23605 +size 19028 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png index 013bb4a3b7..8009275ae0 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2f9ed902882f58704b22460bc64a7b27bc6f47fc2c822ee09f52345cc0d6ebf -size 19255 +oid sha256:5cd6a1a5e9181e033bdfcc90d1029aaa15178087d95b6cbc97ec62dc96a7903e +size 20357 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png index 31fd7a5445..6387a35b31 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aad3f26f2939f3679afa2b6165db29885fff40bbb1d171d5ffecc7861b5fac31 -size 19654 +oid sha256:c8bfc279de7255a591e2647745a3edc5564cf6ac6eeedd376551ff70eaa8f669 +size 21289 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 index 82b965123d..8b9efe3364 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bed69d43856ebd4b1af4055f8d3aacabd50c361a4e1e1f9cad080d799d6b744 -size 13853 +oid sha256:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 +size 17790 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png index e2a05b9bd5..69ca61d41c 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_OctreeQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d21029fa22dbe72cdc60b90c758cb9becd9fce03a33580d9466c1aedd323c1c -size 20000 +oid sha256:90fae259caa8e2af7c8d5a5b07afb3de6292acd7bb5a9a1ad058fc820dca1a4b +size 22343 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.25.png index 9850675bed..93ebc8ac7e 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea836214840a5da2b89dad3cd9e916413d3f9e21f9b855dc8161faa3544edcfc -size 9266 +oid sha256:4f95a03b6167e1e5174b94cbd2d4c0df6fbcf74402abcf027fb51c048d6040ca +size 9236 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.5.png index f3278c3d2f..9ff1175dfa 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:346c9e4239d917614525a99f7ae58ed0c0a22dc09d639f3a54dad1975e75ec44 -size 8833 +oid sha256:3f30a9cabe172856d45e058b41adba1580e4aac2c5455738a0fe1222e0fdb313 +size 10128 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.75.png index 77821255bb..34257a5236 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:717fe46156f3d144f31cfce066dd13532ee8721d7d3a7b8c8425c646f411e8a5 -size 11099 +oid sha256:28f444d388662a516e261dd7b5d28fc142b6089061ca9450a9990053884eee6d +size 10596 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.png index 0615793d57..170dd9482e 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e01c7276f1c4e905b1d8f4c84259f1047c0949f7a6a81f43a790bd1bd3201e3 -size 7932 +oid sha256:2e51a7ab6a24ad8466eb1c07bbd221a9b99cff2515304cd7d8b5577d7924c333 +size 8600 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_1.png index c43b5836ec..0523babb39 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_ErrorDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae18d22edc011d576d6a1e9545bc52084ca0bed55a6ce19d391d2a5f97b1843c -size 11763 +oid sha256:1eb4268feb96c5fe5e53cf9a70dc50ff54a4d229f41a9ec7a005232f707ae395 +size 12835 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.25.png index e54740610c..5d6ec0c0fa 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74b3f36e3fbac940d1f3bf90089b6b40234aa2ce3570b094534a4448c1d98aec -size 8875 +oid sha256:db9c3dee98f961011829fd5d1733c990016d8518684dea43bdc0ed8b46dc065b +size 9461 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.5.png index b08ba5be19..a13f473352 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80e60c42fa11e973e1c865ed93448d3af0503e32d7b119bfe7162738efe691db -size 9086 +oid sha256:c49ae3b99cac4592c3aba6b1bd2613b7c2887419df84135facd9caab1c67c4f2 +size 9527 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.75.png index 692c119e4a..1b4547c806 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd5a9c76ee332603877624e219d84f85fe159389e7f9e72d1fb6177289dd1fb7 -size 9777 +oid sha256:a0449b5b9a2471a6df694a1e36c8544e07d6a1ad01a78dc2de06ca1f9171a668 +size 9711 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.png index 0615793d57..170dd9482e 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e01c7276f1c4e905b1d8f4c84259f1047c0949f7a6a81f43a790bd1bd3201e3 -size 7932 +oid sha256:2e51a7ab6a24ad8466eb1c07bbd221a9b99cff2515304cd7d8b5577d7924c333 +size 8600 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_1.png index 17a810448e..5f25693d28 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WebSafePaletteQuantizer_OrderedDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:240743d5f742b872c0f66f4033ad065402372605a76cda23f4c506d254a9d127 -size 9791 +oid sha256:fff6cd72098dbdbc5553e21e68a65813c4a938213fc92dab205968d0d5cba613 +size 9776 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.25.png index 10b511a1a3..80c5b82ce0 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:074842dcbdf60690f41da31e12c290045d05ab6dc587f3f5ba29c9496871391c -size 11209 +oid sha256:88881839126c275a3d71278e4873cd05f4fe26eb7e1d1c2f5b29826d5bc60ef2 +size 11601 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.5.png index 1ed81c0d0a..dde458b3a9 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29e1ff6d454efca61852a88946e25dcf29708230bfc47c2625c4d1b2407070c6 -size 12072 +oid sha256:bcd2b1732cfd93dc307c783fd71df4736a3e99ff318a32197e472b6202118582 +size 11457 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.75.png index 30f75826eb..395b0db882 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7838c37c32134f325960312095ed8e1decbb0dd7e14a84e82637258c7ea117e -size 12826 +oid sha256:3d9f342735dd97d58952ac3f4561cdc71c0c0c9059886d5a9cc7fe87c3f5dd3f +size 12955 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.png index af9954116a..d50b18799f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0166ad5236ecdcc943d839fad092fe3899dcd4e418703846c492edb7700e4726 -size 10682 +oid sha256:b3c5370823f6f2a80a843967d541afdbb38779e2e56d629e5e011c3b35c7060d +size 10928 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_1.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_1.png index 5b8c5127c0..2cc4e0207d 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_1.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_ErrorDither_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaeee39c61b86d9ce569ca2288f998b8461a3f2169dac23cf2f750dd475d8b81 -size 14145 +oid sha256:603b127cbb2c4134a2f7cde964dcdc37ab7f8ba4e7eef01df09ac1d8aba02346 +size 14262 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.25.png index 93fa5c1de3..1188593ceb 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5908ff88ddaa6eb3faea6174d87b0182e4407b11812ad70ddcd39c6619b6a5c5 -size 12615 +oid sha256:05796c5a4b91edf69d39ec63160651b86d296d67d7a1424059741e93d5da8307 +size 12904 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.5.png index af2345fe52..3988b6dae1 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6852daae665638e38c0b7ff58b2a0de1d5df9dd771c5cbccbbb83ff78e6a1d7 -size 12741 +oid sha256:927046821aeafb5dae87199776830369207adfdb8271ba1d37b69e15af18f005 +size 13186 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.75.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.75.png index 3f91a9259c..33886fc82f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.75.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d86473ff1024fc53373b1dba49fc14283b8a323d6b85ba3e16f41ebff8288d0 -size 12845 +oid sha256:0787a1790514d8e4582901c9eedb106ada42f859853ed34b983a452487be34b5 +size 13615 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.png index af9954116a..d50b18799f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WernerPaletteQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0166ad5236ecdcc943d839fad092fe3899dcd4e418703846c492edb7700e4726 -size 10682 +oid sha256:b3c5370823f6f2a80a843967d541afdbb38779e2e56d629e5e011c3b35c7060d +size 10928 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..b09532ebba 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:2e3d86d6019c23b8fd912f43247e6b4135b4862aac872205762f89ceb9b40cca +size 13715 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 dba9232097..eaf7e8241d 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:faa91657288e6a6797d8459d41d5fecca3c0a2e8e63317ebaf47df28688d13d7 -size 13853 +oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d +size 13158 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 ea062d5be6..1c131b0017 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:79e48506430f3a9b25f484ef191fd820819c438392a4e588c2ecafb6db9a2210 -size 13775 +oid sha256:bac98b38fa3d0029341d9ad1a4325b1957c353734dec225605a07fb64662802a +size 13219 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 ae90ea9b5f..ae6bfc9fa7 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:f56c884a0e4666cd662d36ec3a0d4e751c899c0122595378154507fffc69fda4 -size 14010 +oid sha256:28881d29b70b83facf59e4dd6d4c396412f65b322503af57ee2fbbbee8031e53 +size 13474 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 dba9232097..eaf7e8241d 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:faa91657288e6a6797d8459d41d5fecca3c0a2e8e63317ebaf47df28688d13d7 -size 13853 +oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d +size 13158 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 1e1795063b..0d85aeab5d 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:4095927693b3cd49df58c0c1d7c5430255350c9ae595408a52ad83b1a65614ac -size 14269 +oid sha256:d18dda730014d92b7e5718762c457d013a5c0b1086ef076ed398d656697607ba +size 13849 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 29a3ed7ffd..cd728c99bc 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:d062d4b79ee01942776ae13467e9bcbb529a7eeb5ad7c28ff3d0ccd3d88dcde6 -size 15962 +oid sha256:83208a455e42ef4d097c3db84535a5e83f1b84d26b0294d9f374b20c4987d1df +size 15810 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.5.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.5.png index 50fa46d169..d2dc5e9629 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.5.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47b2265af41ba042904cab387bf1de4715bd4d8a318bc6c1f69bfdbff5eabe2c -size 16928 +oid sha256:f89b399b52f4d4881afaf39c468d338afbd112f6e02cb9c7a2f32467facdece1 +size 17146 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..8061d4dbdf 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:44757077f63742a2b6ed447f2b7cd840164f55dc76711836b911a350de0d59ac +size 18053 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 dba9232097..eaf7e8241d 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:faa91657288e6a6797d8459d41d5fecca3c0a2e8e63317ebaf47df28688d13d7 -size 13853 +oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d +size 13158 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..4dc0bf0eeb 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:85ecdd525ee8c6168ada7c2d0e3821bf8cc6d883b298bcb15eae71fdf30c8673 +size 18130 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png index 09c471914a..3987423556 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a40b319d264f046159722cb57599eda51de9ba3795272b3785901cdc51053fab -size 83010 +oid sha256:55253883878ff5721d084b7b280b9728e9b0979d733fcad2b73d4b78749819a5 +size 83175 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png index 34490e602d..3d0d4eccb5 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b92f3320120d53444cefc79b4684933cfe2b933dc79c2414496785743b5c8f18 -size 80808 +oid sha256:6083fbe3278a08b7a311d2aa68448b634426cc95bf6ff2dc9614eecc334776b6 +size 80773 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_ErrorDither.png index 40243937d3..51c93894de 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b63810145832db459bb7a6b37a028a7b778f6b6b4e6eae00e50e6e21c5a06086 -size 62199 +oid sha256:a836c8efd7aa9818cf807cf56412e78399a6568798be23d0f3f6b89552856ff1 +size 62172 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_NoDither.png index 5e9fa12332..c48268b955 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90db3ce2153cc9ba4d1d79e5749dc4d49e916dff8a0e121ebce9b00702cfcc8 -size 33880 +oid sha256:fdf1ebcbf951a82b4899c12c1e24eb28c84694dac52d390b54b6db8a06949ddf +size 33901 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_OrderedDither.png index 68a95a0540..0386f0e33f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8af98bfcc5edef3f3ff33ee8f76f33ce2906a6677167e2b29e1dbe63b00a78d8 -size 44202 +oid sha256:be3277c05d05314653976167612ef2bb5f19f8cf3159eca5122878da0f0c0fcd +size 44391 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_ErrorDither.png index 96c66aad72..7cfe595a16 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:828b082a1892f0200ef84254637b340b1276e1bee44e01c6b715de8838e4818f -size 35301 +oid sha256:1739c75759f8714526bbb6bff1df02aa1e327f2e22b50e3514903ec1fc672aa7 +size 35332 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_NoDither.png index 3ff151f6d0..ff91ab181f 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f70d1aa2f985dfb7227ea5fd7b4b98effc1a31c89fd05bbee9cfa8f003b9cb4e -size 34261 +oid sha256:7c1d2789714f291746e254815df3001655c4bae2c9c02a08b9a913e87bec5036 +size 34335 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..b28fef249e 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:eec1380b1eec5bf7b3ea10ba0b6c4890286d78e6f7a3d2cea6533cb6a1ccbeca +size 44835 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 747ca70c1d..5860c0eacc 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:d58c425ce5b1ca56450095a66dea24b379935b0087aec7b4102f15a99f95a017 -size 101999 +oid sha256:b979180d0e4c4c644bc39d87f17d5d5b7a7dac12cb830622f3db2cd6ef2dfdc0 +size 106317 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 de464b94cc..e72ea4b246 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:93a4822e39babba059a88536a965e4f3207e4402d2b92d7d18485fec5e9e69da -size 84378 +oid sha256:bf2021eba9edbb2295924f8394472ac0bb237f0c462c39aa32a2074ef15f9acc +size 81771 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 ce54548279..ca0c1b2949 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:35969c8dc96de4dacc3048ae760a0681278a2011993a0edbceaacc93d6fc3a67 -size 102713 +oid sha256:50338ed207a0b5576c3981bd4856f5de1551c765b4c08cd07404da0b1d572598 +size 101284 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png index 5efcaedc94..a9d92676b6 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40d012f4ecb4e36c94d086f8ec7bc199fbfd9fb30a9427a07b35df1b1e430a71 -size 95601 +oid sha256:f2f42914537c706478b010b42c5763c845450af75f7b6edee39e35b5398e465f +size 95520 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png index 916dc37566..4f0f1cffaa 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa64863f73dfd1c5daef645c54e9275136f66513a87750bee0ec8e13ac357da5 -size 79649 +oid sha256:33effef7168a64aba77288273b03ba2db21c19f51131f08ef5783d4c6c196beb +size 79876 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png index f039dd222e..58fbba51b7 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_OctreeQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f5138589c606de20ba193d4279f049ee1ecb3f1801b949d3436995bbf242cbe -size 92683 +oid sha256:9645c6b7dad1ca5aad6a7d7e0a54d576914a4da778762a34634c3c26a6810fbc +size 92356 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png index 2b897a5d6d..2d6226d096 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:007ac609ec61b39c7bdd04bc87a698f5cdc76eadd834c1457f41eb9c135c3f7b -size 60688 +oid sha256:89864a77216b51cc5b9415453ade7f7ec64c1c112546aa47ee6b4b89f9b258a3 +size 60543 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png index e40a91cbc4..7b842e1f78 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fc2f82bdbf4b204ad78f3bb54bfdea7452a2d1430814f45262fd309225f2fc0 -size 46727 +oid sha256:e2982181e3ca61a0e6e5e27a3909d7003dc784d6bf51800159dc274ce058eb49 +size 47214 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png index ac56fa9236..82ba28be09 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WebSafePaletteQuantizer_OrderedDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d2289ed4fa0c679f0f120d260fec8ab40b1599043cc0a1fbebc6b67e238ff87 -size 51428 +oid sha256:1366d7c4e9770bceed30aecd894b6e947f822f8dd74f5d231f013167c43ad3c4 +size 51553 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png index 8b79a19e05..fd27271129 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_ErrorDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2b6073b75a2de97a78d47d3b3e40c264687c5756f153d3d85bc5b2714cf85a -size 68226 +oid sha256:96abf20f6b75757da529a661a2567a74527ffe8391615e34ae8f27ceaf381dba +size 67973 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png index 8d0d2b60db..2bad1f1f25 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WernerPaletteQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1424c6c4c18feb42106e14da6b161ce3f48276d0aa6603ca60ad5caa0a5338 -size 63764 +oid sha256:be280b1be360e42b2eb2f8270d900a7f268e75a9ba1828fe0985925a7add6192 +size 64155 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 88cf83a306..4421744a88 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:513844ed95c2b50e792d3346398256846b8b280dbadf7ef3f4e11d58c1e679c0 -size 69529 +oid sha256:3a1a5f51df0ee5045de824bf8cca7aed4d89fe146280ad2697c47c86258504d4 +size 70074 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 a3eefcba20..d8a04a055d 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:32b269d62d4eebe555d5d9f12b9958b41206848504bb985dcd1ff9c81a5003c6 -size 117073 +oid sha256:156319efa8874050ed646bf3a8b7dfd2aa7eef56c9d31fad3c408b418608ff69 +size 111255 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 3b0c46ac38..691623fc88 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:12f58b00a16913cd85ffa18fcea580a59550dcc201295b060d55a870230f37f7 -size 113995 +oid sha256:58a61c1d9a1d05acd484948c3e5c0496dbc74c0060f5de71741de39eae04ffa8 +size 103875 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 328f863307..8200f1154a 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:867d7b727de278cbc01b7d2b8e968f1fc0d0a81a3e4af636ce4a6598a8709be6 -size 114630 +oid sha256:c3ab33be7ab7694401cd8a201784c42524afeca61ce194418b079acad22172a1 +size 110109 From 33e5cbfa24c65993def1c55d091a834f5f49eed5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Feb 2025 10:24:58 +1000 Subject: [PATCH 11/22] Try bumping to latest SDK --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/code-coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d9e0f1e08f..503f780bf2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -111,7 +111,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 8.0.x + 9.0.x 7.0.x 6.0.x diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index db9aca0b08..8cb7df501e 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -61,7 +61,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 8.0.x + 9.0.x 7.0.x 6.0.x From c4d314a1ff34e2ab7415fe04d97893e24cfd1d31 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Feb 2025 10:37:58 +1000 Subject: [PATCH 12/22] Revert "Try bumping to latest SDK" This reverts commit 33e5cbfa24c65993def1c55d091a834f5f49eed5. --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/code-coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 503f780bf2..d9e0f1e08f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -111,7 +111,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 9.0.x + 8.0.x 7.0.x 6.0.x diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 8cb7df501e..db9aca0b08 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -61,7 +61,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 9.0.x + 8.0.x 7.0.x 6.0.x From 67fd9de69f019e77cb6859167edabf8f97fa9526 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Feb 2025 10:39:22 +1000 Subject: [PATCH 13/22] Try casting --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 24ac45a3fa..db6a1ffc80 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1319,7 +1319,8 @@ private void EncodePixels(in Buffer2DRegion pixels, IndexedImage Span attempt = attemptBuffer.GetSpan(); for (int y = 0; y < pixels.Height; y++) { - this.CollectAndFilterPixelRow(pixels.DangerousGetRowSpan(y), ref filter, ref attempt, quantized, y); + ReadOnlySpan rowSpan = pixels.DangerousGetRowSpan(y); + this.CollectAndFilterPixelRow(rowSpan, ref filter, ref attempt, quantized, y); deflateStream.Write(filter); this.SwapScanlineBuffers(); } @@ -1367,7 +1368,7 @@ private void EncodeAdam7Pixels(in Buffer2DRegion pixels, ZlibDef // Encode data // Note: quantized parameter not used // Note: row parameter not used - this.CollectAndFilterPixelRow(block, ref filter, ref attempt, null, -1); + this.CollectAndFilterPixelRow((ReadOnlySpan)block, ref filter, ref attempt, null, -1); deflateStream.Write(filter); this.SwapScanlineBuffers(); From d33e6a9c857a28897d0b4203e248d1b18cd26561 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 26 Feb 2025 22:56:22 +1000 Subject: [PATCH 14/22] Use latest instead of preview to avoid build errors. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 26b3cc5afc..2a36eeda2f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ - preview + latest - latest + 13.0 - 13.0 + 12.0 + True + False diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 982cc7d46c..067ae69fdf 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -80,7 +80,7 @@ protected override void Dispose(bool disposing) Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")] internal readonly struct DitherProcessor : IPaletteDitherImageProcessor, IDisposable { - private readonly EuclideanPixelMap pixelMap; + private readonly PixelMap pixelMap; [MethodImpl(InliningOptions.ShortMethod)] public DitherProcessor( @@ -89,7 +89,7 @@ public DitherProcessor( float ditherScale) { this.Configuration = configuration; - this.pixelMap = new EuclideanPixelMap(configuration, palette); + this.pixelMap = PixelMapFactory.Create(configuration, palette, ColorMatchingMode.Hybrid); this.Palette = palette; this.DitherScale = ditherScale; } @@ -103,7 +103,7 @@ public DitherProcessor( [MethodImpl(InliningOptions.ShortMethod)] public TPixel GetPaletteColor(TPixel color) { - this.pixelMap.GetClosestColor(color, out TPixel match); + _ = this.pixelMap.GetClosestColor(color, out TPixel match); return match; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs new file mode 100644 index 0000000000..26fd7d5d76 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization; + +/// +/// Defines the precision level used when matching colors during quantization. +/// +public enum ColorMatchingMode +{ + /// + /// Uses a coarse caching strategy optimized for performance at the expense of exact matches. + /// This provides the fastest matching but may yield approximate results. + /// + 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. + /// + Exact +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs new file mode 100644 index 0000000000..a900d643bd --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs @@ -0,0 +1,219 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization; + +/// +/// Gets the closest color to the supplied color based upon the Euclidean distance. +/// +/// The pixel format. +/// The cache type. +/// +/// This class is not thread safe and should not be accessed in parallel. +/// Doing so will result in non-idempotent results. +/// +internal sealed class EuclideanPixelMap : PixelMap + where TPixel : unmanaged, IPixel + where TCache : struct, IColorIndexCache +{ + private Rgba32[] rgbaPalette; + private int transparentIndex; + private readonly TPixel transparentMatch; + + // Do not make readonly. It's a mutable struct. +#pragma warning disable IDE0044 // Add readonly modifier + private TCache cache; +#pragma warning restore IDE0044 // Add readonly modifier + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The color palette to map from. + /// An explicit index at which to match transparent pixels. + [RequiresPreviewFeatures] + public EuclideanPixelMap( + Configuration configuration, + ReadOnlyMemory palette, + int transparentIndex = -1) + { + this.configuration = configuration; + this.cache = TCache.Create(configuration.MemoryAllocator); + + this.Palette = palette; + this.rgbaPalette = new Rgba32[palette.Length]; + PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); + + this.transparentIndex = transparentIndex; + Unsafe.SkipInit(out this.transparentMatch); + this.transparentMatch.FromRgba32(default); + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetClosestColor(TPixel color, out TPixel match) + { + ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); + Unsafe.SkipInit(out Rgba32 rgba); + color.ToRgba32(ref rgba); + + // Check if the color is in the lookup table + if (this.cache.TryGetValue(rgba, out short index)) + { + match = Unsafe.Add(ref paletteRef, (ushort)index); + return index; + } + + return this.GetClosestColorSlow(rgba, ref paletteRef, out match); + } + + /// + public override void Clear(ReadOnlyMemory palette) + { + this.Palette = palette; + this.rgbaPalette = new Rgba32[palette.Length]; + PixelOperations.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); + this.transparentIndex = -1; + this.cache.Clear(); + } + + /// + public override void SetTransparentIndex(int index) + { + if (index != this.transparentIndex) + { + this.cache.Clear(); + } + + this.transparentIndex = index; + } + + [MethodImpl(InliningOptions.ColdPath)] + private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) + { + // Loop through the palette and find the nearest match. + int index = 0; + + if (this.transparentIndex >= 0 && rgba == default) + { + // We have explicit instructions. No need to search. + index = this.transparentIndex; + _ = this.cache.TryAdd(rgba, (short)index); + match = this.transparentMatch; + return index; + } + + float leastDistance = float.MaxValue; + for (int i = 0; i < this.rgbaPalette.Length; i++) + { + Rgba32 candidate = this.rgbaPalette[i]; + float distance = DistanceSquared(rgba, candidate); + + // If it's an exact match, exit the loop + if (distance == 0) + { + index = i; + break; + } + + if (distance < leastDistance) + { + // Less than... assign. + index = i; + leastDistance = distance; + } + } + + // 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); + + return index; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static float DistanceSquared(Rgba32 a, Rgba32 b) + { + float deltaR = a.R - b.R; + float deltaG = a.G - b.G; + float deltaB = a.B - b.B; + float deltaA = a.A - b.A; + return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); + } + + /// + public override void Dispose() => this.cache.Dispose(); +} + +/// +/// Represents a map of colors to indices. +/// +/// The pixel format. +internal abstract class PixelMap : IDisposable + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the color palette of this . + /// + public ReadOnlyMemory Palette { get; private protected set; } + + /// + /// Returns the closest color in the palette and the index of that pixel. + /// + /// The color to match. + /// The matched color. + /// + /// The index. + /// + public abstract int GetClosestColor(TPixel color, out TPixel match); + + /// + /// Clears the map, resetting it to use the given palette. + /// + /// The color palette to map from. + public abstract void Clear(ReadOnlyMemory palette); + + /// + /// Allows setting the transparent index after construction. + /// + /// An explicit index at which to match transparent pixels. + public abstract void SetTransparentIndex(int index); + + /// + public abstract void Dispose(); +} + +/// +/// A factory for creating instances. +/// +internal static class PixelMapFactory +{ + /// + /// Creates a new instance. + /// + /// The pixel format. + /// The configuration. + /// The color palette to map from. + /// The color matching mode. + /// An explicit index at which to match transparent pixels. + /// + /// The . + /// + public static PixelMap Create( + Configuration configuration, + ReadOnlyMemory palette, + ColorMatchingMode colorMatchingMode, + int transparentIndex = -1) + where TPixel : unmanaged, IPixel => colorMatchingMode switch + { + ColorMatchingMode.Hybrid => new EuclideanPixelMap(configuration, palette, transparentIndex), + ColorMatchingMode.Exact => new EuclideanPixelMap(configuration, palette, transparentIndex), + _ => new EuclideanPixelMap(configuration, palette, transparentIndex), + }; +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs deleted file mode 100644 index 5e6ef9d39c..0000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ /dev/null @@ -1,438 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization; - -/// -/// Gets the closest color to the supplied color based upon the Euclidean distance. -/// -/// The pixel format. -/// -/// This class is not thread safe and should not be accessed in parallel. -/// Doing so will result in non-idempotent results. -/// -internal sealed class EuclideanPixelMap : IDisposable - where TPixel : unmanaged, IPixel -{ - private Rgba32[] rgbaPalette; - private int transparentIndex; - private readonly TPixel transparentMatch; - - /// - /// Do not make this readonly! Struct value would be always copied on non-readonly method calls. - /// - private HybridColorDistanceCache cache; - private readonly Configuration configuration; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - /// The color palette to map from. - public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) - : this(configuration, palette, -1) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - /// The color palette to map from. - /// An explicit index at which to match transparent pixels. - public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette, int transparentIndex = -1) - { - this.configuration = configuration; - this.Palette = palette; - this.rgbaPalette = new Rgba32[palette.Length]; - this.cache = new HybridColorDistanceCache(configuration.MemoryAllocator); - PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); - - this.transparentIndex = transparentIndex; - Unsafe.SkipInit(out this.transparentMatch); - this.transparentMatch.FromRgba32(default); - } - - /// - /// Gets the color palette of this . - /// The palette memory is owned by the palette source that created it. - /// - public ReadOnlyMemory Palette { get; private set; } - - /// - /// Returns the closest color in the palette and the index of that pixel. - /// The palette contents must match the one used in the constructor. - /// - /// The color to match. - /// The matched color. - /// The index. - [MethodImpl(InliningOptions.ShortMethod)] - public int GetClosestColor(TPixel color, out TPixel match) - { - ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); - Unsafe.SkipInit(out Rgba32 rgba); - color.ToRgba32(ref rgba); - - // Check if the color is in the lookup table - if (this.cache.TryGetValue(rgba, out short index)) - { - match = Unsafe.Add(ref paletteRef, (ushort)index); - return index; - } - - return this.GetClosestColorSlow(rgba, ref paletteRef, out match); - } - - /// - /// Clears the map, resetting it to use the given palette. - /// - /// The color palette to map from. - public void Clear(ReadOnlyMemory palette) - { - this.Palette = palette; - this.rgbaPalette = new Rgba32[palette.Length]; - PixelOperations.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); - this.transparentIndex = -1; - this.cache.Clear(); - } - - /// - /// Allows setting the transparent index after construction. - /// - /// An explicit index at which to match transparent pixels. - public void SetTransparentIndex(int index) - { - if (index != this.transparentIndex) - { - this.cache.Clear(); - } - - this.transparentIndex = index; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) - { - // Loop through the palette and find the nearest match. - int index = 0; - - if (this.transparentIndex >= 0 && rgba == default) - { - // We have explicit instructions. No need to search. - index = this.transparentIndex; - this.cache.Add(rgba, (short)index); - match = this.transparentMatch; - return index; - } - - float leastDistance = float.MaxValue; - for (int i = 0; i < this.rgbaPalette.Length; i++) - { - Rgba32 candidate = this.rgbaPalette[i]; - float distance = DistanceSquared(rgba, candidate); - - // If it's an exact match, exit the loop - if (distance == 0) - { - index = i; - break; - } - - if (distance < leastDistance) - { - // Less than... assign. - index = i; - leastDistance = distance; - } - } - - // Now I have the index, pop it into the cache for next time - this.cache.Add(rgba, (short)index); - match = Unsafe.Add(ref paletteRef, (uint)index); - - return index; - } - - /// - /// Returns the Euclidean distance squared between two specified points. - /// - /// The first point. - /// The second point. - /// The distance squared. - [MethodImpl(InliningOptions.ShortMethod)] - private static float DistanceSquared(Rgba32 a, Rgba32 b) - { - Vector4 va = new(a.R, a.G, a.B, a.A); - Vector4 vb = new(b.R, b.G, b.B, b.A); - return Vector4.DistanceSquared(va, vb); - } - - public void Dispose() => this.cache.Dispose(); - - /// - /// A hybrid cache for color distance lookups that combines an exact-match dictionary with - /// a fallback coarse lookup table. - /// - /// - /// This cache uses a fallback table with 2,097,152 bins, each storing a 2-byte value - /// (approximately 4 MB total), while the exact-match dictionary is limited to 256 entries - /// and occupies roughly 4 KB. Overall, the worst-case memory usage is about 4 MB. - /// Lookups and insertions are performed in constant time (O(1)) because the fallback table - /// is accessed via direct indexing and the dictionary employs a simple hash-based bucket mechanism. - /// The design achieves extremely fast color distance lookups with a predictable, fixed memory footprint. - /// -#pragma warning disable CA1001 // Types that own disposable fields should be disposable - - // https://github.com/dotnet/roslyn-analyzers/issues/6151 - private unsafe struct HybridColorDistanceCache : IDisposable -#pragma warning restore CA1001 // Types that own disposable fields should be disposable - { - private const int IndexRBits = 5; - private const int IndexGBits = 5; - private const int IndexBBits = 5; - private const int IndexABits = 6; - private const int IndexRCount = 1 << IndexRBits; // 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 fallbackTable; - private readonly short* fallbackPointer; - private MemoryHandle fallbackHandle; - - private readonly ExactCache exactCache; - - public HybridColorDistanceCache(MemoryAllocator allocator) - { - this.fallbackTable = allocator.Allocate(TotalBins); - this.fallbackTable.GetSpan().Fill(-1); - this.fallbackHandle = this.fallbackTable.Memory.Pin(); - this.fallbackPointer = (short*)this.fallbackHandle.Pointer; - - this.exactCache = new ExactCache(allocator); - } - - [MethodImpl(InliningOptions.ShortMethod)] - public readonly void Add(Rgba32 color, short index) - { - if (this.exactCache.TryAdd(color.PackedValue, index)) - { - return; - } - - this.fallbackPointer[GetCoarseIndex(color)] = index; - } - - [MethodImpl(InliningOptions.ShortMethod)] - public readonly bool TryGetValue(Rgba32 color, out short match) - { - if (this.exactCache.TryGetValue(color.PackedValue, out match)) - { - return true; // Exact match found - } - - match = this.fallbackPointer[GetCoarseIndex(color)]; - return match > -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.exactCache.Clear(); - this.fallbackTable.GetSpan().Fill(-1); - } - - public void Dispose() - { - this.fallbackHandle.Dispose(); - this.fallbackTable.Dispose(); - this.exactCache.Dispose(); - } - } - - /// - /// A fixed-capacity dictionary with exactly 512 entries mapping a key - /// to a value. - /// - /// - /// 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 overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, - /// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are - /// typically very short; in the worst-case, the number of iterations is bounded by 256. - /// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. - /// - internal sealed unsafe class ExactCache : IDisposable - { - // Buckets array: each bucket holds the index (0-based) into the entries array - // of the first entry in the chain, or -1 if empty. - private readonly IMemoryOwner bucketsOwner; - private MemoryHandle bucketsHandle; - private short* buckets; - - // Entries array: stores up to 256 entries. - private readonly IMemoryOwner entriesOwner; - private MemoryHandle entriesHandle; - private Entry* entries; - - public const int Capacity = 512; - - public ExactCache(MemoryAllocator allocator) - { - this.Count = 0; - - // Allocate exactly 512 ints for buckets. - this.bucketsOwner = allocator.Allocate(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; - } - - public int Count { get; private set; } - - /// - /// Adds a key/value pair to the dictionary. - /// If the key already exists, the dictionary is left unchanged. - /// - /// The key to add. - /// The value to add. - public bool TryAdd(uint key, short value) - { - if (this.Count == Capacity) - { - return false; // Dictionary is full. - } - - // The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A - // (with R in the most significant byte and A in the least significant). - // To compute the bucket index: - // 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels. - // 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A). - // 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A), - // which helps to counteract situations where one or more channels have a limited range. - // 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511, - // which corresponds to our fixed bucket count of 512. - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; - - // Traverse the collision chain. - Entry* entries = this.entries; - while (i != -1) - { - Entry e = entries[i]; - if (e.Key == key) - { - // Key already exists; do not overwrite. - return false; - } - - i = e.Next; - } - - short index = (short)this.Count; - this.Count++; - - // Insert the new entry: - entries[index].Key = key; - entries[index].Value = value; - - // Link this new entry into the bucket chain. - entries[index].Next = this.buckets[bucket]; - this.buckets[bucket] = index; - return true; - } - - /// - /// Tries to retrieve the value associated with the specified key. - /// Returns true if the key is found; otherwise, returns false. - /// - /// The key to search for. - /// The value associated with the key, if found. - /// if the key is found; otherwise, . - public bool TryGetValue(uint key, out short value) - { - int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); - int i = this.buckets[bucket]; - - // If the bucket is empty, return immediately. - if (i == -1) - { - value = -1; - return false; - } - - // Traverse the chain. - Entry* entries = this.entries; - do - { - Entry e = entries[i]; - if (e.Key == key) - { - value = e.Value; - return true; - } - - i = e.Next; - } - while (i != -1); - - value = -1; - return false; - } - - /// - /// Clears the dictionary. - /// - public void Clear() - { - Span bucketSpan = this.bucketsOwner.GetSpan(); - bucketSpan.Fill(-1); - this.Count = 0; - } - - public void Dispose() - { - this.bucketsHandle.Dispose(); - this.bucketsOwner.Dispose(); - this.entriesHandle.Dispose(); - this.entriesOwner.Dispose(); - this.buckets = null; - this.entries = null; - } - - private struct Entry - { - public uint Key; // The key (packed RGBA) - public short Value; // The value; -1 means unused. - public short Next; // Index of the next entry in the chain, or -1 if none. - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs new file mode 100644 index 0000000000..6234e2e63e --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs @@ -0,0 +1,377 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization; + +/// +/// Represents a cache used for efficiently retrieving palette indices for colors. +/// +/// The type of the cache. +internal interface IColorIndexCache : IColorIndexCache + where T : struct, IColorIndexCache +{ + /// + /// Creates a new instance of the cache. + /// + /// The memory allocator to use. + /// + /// The new instance of the cache. + /// +#pragma warning disable IDE0060 // Remove unused parameter + [RequiresPreviewFeatures] + public static abstract T Create(MemoryAllocator allocator); +#pragma warning restore IDE0060 // Remove unused parameter +} + +/// +/// Represents a cache used for efficiently retrieving palette indices for colors. +/// +internal interface IColorIndexCache : IDisposable +{ + /// + /// Adds a color to the cache. + /// + /// The color to add. + /// The index of the color in the palette. + /// + /// if the color was added; otherwise, . + /// + public bool TryAdd(Rgba32 color, short value); + + /// + /// Gets the index of the color in the palette. + /// + /// The color to get the index for. + /// The index of the color in the palette. + /// + /// if the color is in the palette; otherwise, . + /// + public bool TryGetValue(Rgba32 color, out short value); + + /// + /// Clears the cache. + /// + public void Clear(); +} + +/// +/// A hybrid cache for color distance lookups that combines an exact-match dictionary with +/// a fallback coarse lookup table. +/// +/// +/// This cache uses a fallback table with 2,097,152 bins, each storing a 2-byte value +/// (approximately 4 MB total), while the exact-match dictionary is limited to 512 entries +/// and occupies roughly 4 KB. Overall, the worst-case memory usage is about 4 MB. +/// Lookups and insertions are performed in constant time (O(1)) because the fallback table +/// is accessed via direct indexing and the dictionary employs a simple hash-based bucket mechanism. +/// The design achieves extremely fast color distance lookups with a predictable, fixed memory footprint. +/// +internal unsafe struct HybridCache : IColorIndexCache +{ + private ExactCache exactCache; + private CoarseCache coarseCache; + + [RequiresPreviewFeatures] + private HybridCache(MemoryAllocator allocator) + { + this.exactCache = ExactCache.Create(allocator); + this.coarseCache = CoarseCache.Create(allocator); + } + + [RequiresPreviewFeatures] + public static HybridCache Create(MemoryAllocator allocator) => new(allocator); + + [MethodImpl(InliningOptions.ShortMethod)] + public bool TryAdd(Rgba32 color, short index) + { + if (this.exactCache.TryAdd(color, index)) + { + return true; + } + + return this.coarseCache.TryAdd(color, index); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public bool TryGetValue(Rgba32 color, out short value) + { + if (this.exactCache.TryGetValue(color, out value)) + { + return true; + } + + return this.coarseCache.TryGetValue(color, out value); + } + + public void Clear() + { + this.exactCache.Clear(); + this.coarseCache.Clear(); + } + + public void Dispose() + { + this.exactCache.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 worst-case 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; + } + + [RequiresPreviewFeatures] + 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(); + } +} + +/// +/// A fixed-capacity dictionary with exactly 512 entries mapping a key +/// to a value. +/// +/// +/// 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 overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are, +/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are +/// typically very short; in the worst-case, the number of iterations is bounded by 256. +/// This guarantees highly efficient and predictable performance for small, fixed-size color palettes. +/// +internal unsafe struct ExactCache : 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; + + // Entries array: stores up to 256 entries. + private readonly IMemoryOwner entriesOwner; + private MemoryHandle entriesHandle; + private Entry* entries; + + private int count; + + public const int Capacity = 512; + + private ExactCache(MemoryAllocator allocator) + { + this.count = 0; + + // Allocate exactly 512 ints 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; + } + + [RequiresPreviewFeatures] + public static ExactCache Create(MemoryAllocator allocator) => new(allocator); + + public bool TryAdd(Rgba32 color, short value) + { + if (this.count == Capacity) + { + return false; // Dictionary is full. + } + + uint key = color.PackedValue; + + // The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A + // (with R in the most significant byte and A in the least significant). + // To compute the bucket index: + // 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels. + // 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A). + // 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A), + // which helps to counteract situations where one or more channels have a limited range. + // 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511, + // which corresponds to our fixed bucket count of 512. + int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); + int i = this.buckets[bucket]; + + // Traverse the collision chain. + Entry* entries = this.entries; + while (i != -1) + { + Entry e = entries[i]; + if (e.Key == key) + { + // Key already exists; do not overwrite. + return false; + } + + i = e.Next; + } + + short index = (short)this.count; + this.count++; + + // Insert the new entry: + entries[index].Key = key; + entries[index].Value = value; + + // Link this new entry into the bucket chain. + entries[index].Next = this.buckets[bucket]; + this.buckets[bucket] = index; + return true; + } + + public 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]; + + // If the bucket is empty, return immediately. + if (i == -1) + { + value = -1; + return false; + } + + // Traverse the chain. + Entry* entries = this.entries; + do + { + Entry e = entries[i]; + if (e.Key == key) + { + value = e.Value; + return true; + } + + i = e.Next; + } + while (i != -1); + + value = -1; + return false; + } + + /// + /// Clears the dictionary. + /// + public void Clear() + { + Span bucketSpan = this.bucketsOwner.GetSpan(); + bucketSpan.Fill(-1); + this.count = 0; + } + + public void Dispose() + { + this.bucketsHandle.Dispose(); + this.bucketsOwner.Dispose(); + this.entriesHandle.Dispose(); + this.entriesOwner.Dispose(); + this.buckets = null; + this.entries = null; + } + + private struct Entry + { + public uint Key; // The key (packed RGBA) + public short Value; // The value; -1 means unused. + public short Next; // Index of the next entry in the chain, or -1 if none. + } +} + +internal readonly struct NullCache : IColorIndexCache +{ + [RequiresPreviewFeatures] + 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() + { + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index 84124056cd..f510b102c5 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -28,7 +28,7 @@ public struct OctreeQuantizer : IQuantizer private readonly Octree octree; private readonly IMemoryOwner paletteOwner; private ReadOnlyMemory palette; - private EuclideanPixelMap? pixelMap; + private PixelMap? pixelMap; private readonly bool isDithering; private bool isDisposed; @@ -114,7 +114,7 @@ private void ResolvePalette() this.octree.Palletize(paletteSpan, max, ref paletteIndex); ReadOnlyMemory result = this.paletteOwner.Memory[..paletteSpan.Length]; - this.pixelMap = new EuclideanPixelMap(this.Configuration, result); + this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode); this.palette = result; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs index 3df80ea9b7..e6984ec98a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; internal readonly struct PaletteQuantizer : IQuantizer where TPixel : unmanaged, IPixel { - private readonly EuclideanPixelMap pixelMap; + private readonly PixelMap pixelMap; /// /// Initializes a new instance of the struct. @@ -41,7 +41,7 @@ public PaletteQuantizer( this.Configuration = configuration; this.Options = options; - this.pixelMap = new EuclideanPixelMap(configuration, palette, transparentIndex); + this.pixelMap = PixelMapFactory.Create(configuration, palette, options.ColorMatchingMode, transparentIndex); } /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs index a6bb265a81..2d6cc49db7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -38,4 +38,9 @@ public int MaxColors get => this.maxColors; set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } + + /// + /// Gets or sets the color matching mode used for matching pixel values to palette colors. + /// + public ColorMatchingMode ColorMatchingMode { get; set; } = ColorMatchingMode.Hybrid; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index 183a2bad9a..5eebb5b6de 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -75,7 +75,7 @@ internal struct WuQuantizer : IQuantizer private ReadOnlyMemory palette; private int maxColors; private readonly Box[] colorCube; - private EuclideanPixelMap? pixelMap; + private PixelMap? pixelMap; private readonly bool isDithering; private bool isDisposed; @@ -161,7 +161,7 @@ private void ResolvePalette() // Create the pixel map if dithering is enabled. if (this.isDithering && this.pixelMap is null) { - this.pixelMap = new EuclideanPixelMap(this.Configuration, this.palette); + this.pixelMap = PixelMapFactory.Create(this.Configuration, this.palette, this.Options.ColorMatchingMode); } } diff --git a/src/ImageSharp/Properties/AssemblyInfo.cs b/src/ImageSharp/Properties/AssemblyInfo.cs deleted file mode 100644 index 334737ac17..0000000000 --- a/src/ImageSharp/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -// Redundant suppressing of SA1413 for Rider. -[assembly: - System.Diagnostics.CodeAnalysis.SuppressMessage( - "StyleCop.CSharp.MaintainabilityRules", - "SA1413:UseTrailingCommasInMultiLineInitializers", - Justification = "Follows SixLabors.ruleset")] diff --git a/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs index 048c2aadda..23a39a6720 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs @@ -23,7 +23,7 @@ public class EncodeGif // Try to get as close to System.Drawing's output as possible private readonly GifEncoder encoder = new GifEncoder { - Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4, ColorMatchingMode = ColorMatchingMode.Coarse }) }; [Params(TestImages.Bmp.Car, TestImages.Png.Rgb48Bpp)] From eb6b0ab8db34feabd59877a7f8a2e2b1a36c1c92 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 13 Mar 2025 09:58:49 +1000 Subject: [PATCH 22/22] Do not copy color tables when transcoding. --- .../Formats/Gif/GifFrameMetadata.cs | 31 +++---------------- src/ImageSharp/Formats/Gif/GifMetadata.cs | 24 +++----------- src/ImageSharp/Formats/Png/PngMetadata.cs | 30 +++--------------- .../Formats/Png/PngEncoderTests.cs | 2 +- 4 files changed, 15 insertions(+), 72 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index f8734bb5a3..2d994c14f1 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Gif; @@ -79,35 +78,15 @@ private GifFrameMetadata(GifFrameMetadata other) public IDeepCloneable DeepClone() => new GifFrameMetadata(this); internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata) - { - // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table? - int index = -1; - float background = 1f; - if (metadata.ColorTable.HasValue) - { - ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; - for (int i = 0; i < colorTable.Length; i++) - { - Vector4 vector = (Vector4)colorTable[i]; - if (vector.W < background) - { - index = i; - } - } - } - - bool hasTransparency = index >= 0; - - return new() + => new() { - LocalColorTable = metadata.ColorTable, + // Do not copy the color table or transparency data. + // This will lead to a mismatch when the image is comprised of frames + // extracted individually from a multi-frame image. ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10), - DisposalMethod = GetMode(metadata.DisposalMode), - HasTransparency = hasTransparency, - TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue, + DisposalMethod = GetMode(metadata.DisposalMode) }; - } private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch { diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index 1331edee89..088d4d088d 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -73,28 +73,12 @@ private GifMetadata(GifMetadata other) public IDeepCloneable DeepClone() => new GifMetadata(this); internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) - { - int index = 0; - Color background = metadata.BackgroundColor; - if (metadata.ColorTable.HasValue) + => new() { - ReadOnlySpan colorTable = metadata.ColorTable.Value.Span; - for (int i = 0; i < colorTable.Length; i++) - { - if (background == colorTable[i]) - { - index = i; - break; - } - } - } - - return new() - { - GlobalColorTable = metadata.ColorTable, + // Do not copy the color table or bit depth. + // This will lead to a mismatch when the image is comprised of frames + // extracted individually from a multi-frame image. ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local, RepeatCount = metadata.RepeatCount, - BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255), }; - } } diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 0d2515c2a2..296465b773 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -93,32 +93,12 @@ private PngMetadata(PngMetadata other) public IDeepCloneable DeepClone() => new PngMetadata(this); internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata) - { - // Should the conversion be from a format that uses a 24bit palette entries (gif) - // we need to clone and adjust the color table to allow for transparency. - Color[]? colorTable = metadata.ColorTable?.ToArray(); - if (colorTable != null) + => new() { - for (int i = 0; i < colorTable.Length; i++) - { - ref Color c = ref colorTable[i]; - if (c == metadata.BackgroundColor) - { - // Png treats background as fully empty - c = Color.Transparent; - break; - } - } - } - - return new() - { - ColorType = colorTable != null ? PngColorType.Palette : null, - BitDepth = colorTable != null - ? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8) - : null, - ColorTable = colorTable, + // Do not copy the color table or bit depth. + // This will lead to a mismatch when the image is comprised of frames + // extracted individually from a multi-frame image. + ColorType = metadata.ColorTable != null ? PngColorType.Palette : null, RepeatCount = metadata.RepeatCount, }; - } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 4f4c6bd1b7..57c8ff667c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -484,7 +484,7 @@ public void Encode_APng(TestImageProvider provider) } [Theory] - [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.613F)] + [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.7629F)] [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32, 1.06F)] public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider, float percentage) where TPixel : unmanaged, IPixel