From cf3faaaadf854f74d09638a0fe34daa9ce7bf2af Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 17:03:55 +1000 Subject: [PATCH 01/73] Remove IImageDecoderInternals redirect --- src/ImageSharp/Advanced/AotCompilerTools.cs | 25 ++-- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 17 +-- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 17 +-- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- .../Formats/IImageDecoderInternals.cs | 50 ------- src/ImageSharp/Formats/ImageDecoder.cs | 14 +- src/ImageSharp/Formats/ImageDecoderCore.cs | 127 ++++++++++++++++++ .../Formats/ImageDecoderUtilities.cs | 81 ----------- .../Formats/Jpeg/JpegDecoderCore.cs | 16 +-- src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 16 +-- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 18 +-- src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 13 +- src/ImageSharp/Formats/Tga/TgaDecoderCore.cs | 17 +-- .../Decompressors/WebpTiffCompression.cs | 4 +- .../Formats/Tiff/TiffDecoderCore.cs | 15 +-- .../Formats/Webp/WebpDecoderCore.cs | 41 +++--- .../Formats/Jpg/JpegDecoderTests.cs | 4 +- .../Formats/Jpg/Utils/JpegFixture.cs | 14 +- 18 files changed, 222 insertions(+), 269 deletions(-) delete mode 100644 src/ImageSharp/Formats/IImageDecoderInternals.cs create mode 100644 src/ImageSharp/Formats/ImageDecoderCore.cs delete mode 100644 src/ImageSharp/Formats/ImageDecoderUtilities.cs diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index f36f3d09b4..61ec85b47a 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; @@ -195,39 +196,41 @@ private static void AotCompileImageProcessingContextFactory() => default(DefaultImageOperationsProviderFactory).CreateImageProcessingContext(default, default, default); /// - /// This method pre-seeds the all in the AoT compiler. + /// This method pre-seeds the all core encoders in the AoT compiler. /// /// The pixel format. [Preserve] private static void AotCompileImageEncoderInternals() where TPixel : unmanaged, IPixel { - default(WebpEncoderCore).Encode(default, default, default); default(BmpEncoderCore).Encode(default, default, default); default(GifEncoderCore).Encode(default, default, default); default(JpegEncoderCore).Encode(default, default, default); default(PbmEncoderCore).Encode(default, default, default); default(PngEncoderCore).Encode(default, default, default); + default(QoiEncoderCore).Encode(default, default, default); default(TgaEncoderCore).Encode(default, default, default); default(TiffEncoderCore).Encode(default, default, default); + default(WebpEncoderCore).Encode(default, default, default); } /// - /// This method pre-seeds the all in the AoT compiler. + /// This method pre-seeds the all in the AoT compiler. /// /// The pixel format. [Preserve] private static void AotCompileImageDecoderInternals() where TPixel : unmanaged, IPixel { - default(WebpDecoderCore).Decode(default, default); - default(BmpDecoderCore).Decode(default, default); - default(GifDecoderCore).Decode(default, default); - default(JpegDecoderCore).Decode(default, default); - default(PbmDecoderCore).Decode(default, default); - default(PngDecoderCore).Decode(default, default); - default(TgaDecoderCore).Decode(default, default); - default(TiffDecoderCore).Decode(default, default); + default(BmpDecoderCore).Decode(default, default, default); + default(GifDecoderCore).Decode(default, default, default); + default(JpegDecoderCore).Decode(default, default, default); + default(PbmDecoderCore).Decode(default, default, default); + default(PngDecoderCore).Decode(default, default, default); + default(QoiDecoderCore).Decode(default, default, default); + default(TgaDecoderCore).Decode(default, default, default); + default(TiffDecoderCore).Decode(default, default, default); + default(WebpDecoderCore).Decode(default, default, default); } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 863fed359c..5dc30575d5 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// A useful decoding source example can be found at /// -internal sealed class BmpDecoderCore : IImageDecoderInternals +internal sealed class BmpDecoderCore : ImageDecoderCore { /// /// The default mask for the red part of the color for 16 bit rgb bitmaps. @@ -104,22 +104,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals /// /// The options. public BmpDecoderCore(BmpDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.rleSkippedPixelHandling = options.RleSkippedPixelHandling; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { Image? image = null; try @@ -205,7 +198,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); @@ -1369,6 +1362,8 @@ private void ReadInfoHeader(BufferedReadStream stream) this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; + + this.Dimensions = new(this.infoHeader.Width, this.infoHeader.Height); } /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index aecbbbbc71..b1d357f86d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Performs the gif decoding operation. /// -internal sealed class GifDecoderCore : IImageDecoderInternals +internal sealed class GifDecoderCore : ImageDecoderCore { /// /// The temp buffer used to reduce allocations. @@ -94,8 +94,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// /// The decoder options. public GifDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.skipMetadata = options.SkipMetadata; this.maxFrames = options.MaxFrames; @@ -103,14 +103,7 @@ public GifDecoderCore(DecoderOptions options) } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; Image? image = null; @@ -181,7 +174,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageFrameMetadata? previousFrame = null; @@ -287,6 +280,8 @@ private void ReadImageDescriptor(BufferedReadStream stream) { GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); } + + this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height); } /// diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 1215768e4b..1daa713cbc 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// /// Implements the GIF encoding protocol. /// -internal sealed class GifEncoderCore : IImageEncoderInternals +internal sealed class GifEncoderCore { /// /// Used for allocating memory during processing operations. diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs deleted file mode 100644 index 06fb597640..0000000000 --- a/src/ImageSharp/Formats/IImageDecoderInternals.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Abstraction for shared internals for XXXDecoderCore implementations to be used with . -/// -internal interface IImageDecoderInternals -{ - /// - /// Gets the general decoder options. - /// - DecoderOptions Options { get; } - - /// - /// Gets the dimensions of the image being decoded. - /// - Size Dimensions { get; } - - /// - /// Decodes the image from the specified stream. - /// - /// The pixel format. - /// The stream, where the image should be decoded from. Cannot be null. - /// The token to monitor for cancellation requests. - /// is null. - /// The decoded image. - /// - /// Cancellable synchronous method. In case of cancellation, - /// an shall be thrown which will be handled on the call site. - /// - Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel; - - /// - /// Reads the raw image information from the specified stream. - /// - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The . - /// - /// Cancellable synchronous method. In case of cancellation, - /// an shall be thrown which will be handled on the call site. - /// - ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); -} diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index ebb45d7013..549a28d409 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -189,7 +189,7 @@ internal static T WithSeekableStream( throw new NotSupportedException("Cannot read from the stream."); } - T PeformActionAndResetPosition(Stream s, long position) + T PerformActionAndResetPosition(Stream s, long position) { T result = action(s); @@ -206,7 +206,7 @@ T PeformActionAndResetPosition(Stream s, long position) if (stream.CanSeek) { - return PeformActionAndResetPosition(stream, stream.Position); + return PerformActionAndResetPosition(stream, stream.Position); } Configuration configuration = options.Configuration; @@ -231,7 +231,7 @@ internal static Task WithSeekableMemoryStreamAsync( throw new NotSupportedException("Cannot read from the stream."); } - Task PeformActionAndResetPosition(Stream s, long position, CancellationToken ct) + Task PerformActionAndResetPosition(Stream s, long position, CancellationToken ct) { try { @@ -263,15 +263,15 @@ Task PeformActionAndResetPosition(Stream s, long position, CancellationToken // code below to copy the stream to an in-memory buffer before invoking the action. if (stream is MemoryStream ms) { - return PeformActionAndResetPosition(ms, ms.Position, cancellationToken); + return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } if (stream is ChunkedMemoryStream cms) { - return PeformActionAndResetPosition(cms, cms.Position, cancellationToken); + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); } - return CopyToMemoryStreamAndActionAsync(options, stream, PeformActionAndResetPosition, cancellationToken); + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } private static async Task CopyToMemoryStreamAndActionAsync( @@ -282,7 +282,7 @@ private static async Task CopyToMemoryStreamAndActionAsync( { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs new file mode 100644 index 0000000000..adf0107da0 --- /dev/null +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -0,0 +1,127 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// The base class for all stateful image decoders. +/// +internal abstract class ImageDecoderCore +{ + /// + /// Initializes a new instance of the class. + /// + /// The general decoder options. + protected ImageDecoderCore(DecoderOptions options) + => this.Options = options; + + /// + /// Gets the general decoder options. + /// + public DecoderOptions Options { get; } + + /// + /// Gets or sets the dimensions of the image being decoded. + /// + public Size Dimensions { get; protected internal set; } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public ImageInfo Identify( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + { + using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); + + try + { + return this.Identify(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + } + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The shared configuration. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. + BufferedReadStream bufferedReadStream = + stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); + + try + { + return this.Decode(bufferedReadStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(this.Dimensions, ex); + } + catch (Exception) + { + throw; + } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + protected abstract ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The token to monitor for cancellation requests. + /// is null. + /// The decoded image. + /// + /// Cancellable synchronous method. In case of cancellation, an shall + /// be thrown which will be handled on the call site. + /// + protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; +} diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs deleted file mode 100644 index a1abd7dc30..0000000000 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Utility methods for . -/// -internal static class ImageDecoderUtilities -{ - internal static ImageInfo Identify( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - { - using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); - - try - { - return decoder.Identify(bufferedReadStream, cancellationToken); - } - catch (InvalidMemoryOperationException ex) - { - throw new InvalidImageContentException(decoder.Dimensions, ex); - } - catch (Exception) - { - throw; - } - } - - internal static Image Decode( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); - - internal static Image Decode( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - Func largeImageExceptionFactory, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. - BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); - - try - { - return decoder.Decode(bufferedReadStream, cancellationToken); - } - catch (InvalidMemoryOperationException ex) - { - throw largeImageExceptionFactory(ex, decoder.Dimensions); - } - catch (Exception) - { - throw; - } - finally - { - if (bufferedReadStream != stream) - { - bufferedReadStream.Dispose(); - } - } - } - - private static InvalidImageContentException DefaultLargeImageExceptionFactory( - InvalidMemoryOperationException memoryOperationException, - Size dimensions) => - new(dimensions, memoryOperationException); -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index ccace190f9..b0bc66008a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// Originally ported from /// with additional fixes for both performance and common encoding errors. /// -internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals +internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData { /// /// Whether the image has an EXIF marker. @@ -117,8 +117,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals /// /// The decoder options. public JpegDecoderCore(JpegDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.resizeMode = options.ResizeMode; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -130,12 +130,6 @@ public JpegDecoderCore(JpegDecoderOptions options) // Refers to assembly's static data segment, no allocation occurs. private static ReadOnlySpan SupportedPrecisions => new byte[] { 8, 12 }; - /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => this.Frame.PixelSize; - /// /// Gets the frame /// @@ -198,8 +192,7 @@ public static JpegFileMarker FindNextFileMarker(BufferedReadStream stream) } /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { using SpectralConverter spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); this.ParseStream(stream, spectralConverter, cancellationToken); @@ -216,7 +209,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ParseStream(stream, spectralConverter: null, cancellationToken); this.InitExifProfile(); @@ -1216,6 +1209,7 @@ private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, } this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); + this.Dimensions = new(frameWidth, frameHeight); this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; remaining -= length; diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 3fe339865b..73a5085c98 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Performs the PBM decoding operation. /// -internal sealed class PbmDecoderCore : IImageDecoderInternals +internal sealed class PbmDecoderCore : ImageDecoderCore { private int maxPixelValue; @@ -52,20 +52,13 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals /// /// The decoder options. public PbmDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => this.pixelSize; - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); @@ -83,7 +76,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); @@ -179,6 +172,7 @@ private void ProcessHeader(BufferedReadStream stream) } this.pixelSize = new Size(width, height); + this.Dimensions = this.pixelSize; this.metadata = new ImageMetadata(); PbmMetadata meta = this.metadata.GetPbmMetadata(); meta.Encoding = this.encoding; diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index dd5e16d7be..1eeac42b83 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Performs the png decoding operation. /// -internal sealed class PngDecoderCore : IImageDecoderInternals +internal sealed class PngDecoderCore : ImageDecoderCore { /// /// The general decoder options. @@ -130,8 +130,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals /// /// The decoder options. public PngDecoderCore(PngDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.configuration = options.GeneralOptions.Configuration; this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -141,8 +141,8 @@ public PngDecoderCore(PngDecoderOptions options) } internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.colorMetadataOnly = colorMetadataOnly; this.maxFrames = options.GeneralOptions.MaxFrames; this.skipMetadata = true; @@ -153,14 +153,7 @@ internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly) } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.header.Width, this.header.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageMetadata metadata = new(); @@ -335,7 +328,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { uint frameCount = 0; ImageMetadata metadata = new(); @@ -1339,6 +1332,7 @@ private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) pngMetadata.InterlaceMethod = this.header.InterlaceMethod; this.pngColorType = this.header.ColorType; + this.Dimensions = new(this.header.Width, this.header.Height); } /// diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index deb0a37f05..8552e164d8 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi; -internal class QoiDecoderCore : IImageDecoderInternals +internal class QoiDecoderCore : ImageDecoderCore { /// /// The global configuration. @@ -31,19 +31,14 @@ internal class QoiDecoderCore : IImageDecoderInternals private QoiHeader header; public QoiDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } - public DecoderOptions Options { get; } - - public Size Dimensions { get; } - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { // Process the header to get metadata this.ProcessHeader(stream); @@ -68,7 +63,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ProcessHeader(stream); PixelTypeInfo pixelType = new(8 * (int)this.header.Channels); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 34f4c2bcf1..e2dd919d29 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// /// Performs the tga decoding operation. /// -internal sealed class TgaDecoderCore : IImageDecoderInternals +internal sealed class TgaDecoderCore : ImageDecoderCore { /// /// General configuration options. @@ -52,21 +52,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals /// /// The options. public TgaDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new(this.fileHeader.Width, this.fileHeader.Height); - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { try { @@ -653,7 +646,7 @@ private void ReadRle(BufferedReadStream stream, int width, int height, B } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadFileHeader(stream); return new ImageInfo( @@ -933,6 +926,8 @@ private TgaImageOrigin ReadFileHeader(BufferedReadStream stream) stream.Read(buffer, 0, TgaFileHeader.Size); this.fileHeader = TgaFileHeader.Parse(buffer); + this.Dimensions = new Size(this.fileHeader.Width, this.fileHeader.Height); + this.metadata = new ImageMetadata(); this.tgaMetadata = this.metadata.GetTgaMetadata(); this.tgaMetadata.BitsPerPixel = (TgaBitsPerPixel)this.fileHeader.PixelDepth; diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index 416472e830..76d0bb6418 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -32,8 +32,8 @@ public WebpTiffCompression(DecoderOptions options, MemoryAllocator memoryAllocat /// protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { - using WebpDecoderCore decoder = new(new WebpDecoderOptions()); - using Image image = decoder.Decode(stream, cancellationToken); + using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = this.options }); + using Image image = decoder.Decode(this.options.Configuration, stream, cancellationToken); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index aed6d4ec66..96e6d2a89d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Performs the tiff decoding operation. /// -internal class TiffDecoderCore : IImageDecoderInternals +internal class TiffDecoderCore : ImageDecoderCore { /// /// General configuration options. @@ -60,8 +60,8 @@ internal class TiffDecoderCore : IImageDecoderInternals /// /// The decoder options. public TiffDecoderCore(DecoderOptions options) + : base(options) { - this.Options = options; this.configuration = options.Configuration; this.skipMetadata = options.SkipMetadata; this.maxFrames = options.MaxFrames; @@ -154,14 +154,7 @@ public TiffDecoderCore(DecoderOptions options) public TiffPredictor Predictor { get; set; } /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions { get; private set; } - - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { List> frames = new(); List framesMetadata = new(); @@ -215,7 +208,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 21a25860c7..781b8246ff 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Performs the webp decoding operation. /// -internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable +internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable { /// /// General configuration options. @@ -61,8 +61,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// /// The decoder options. public WebpDecoderCore(WebpDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.backgroundColorHandling = options.BackgroundColorHandling; this.configuration = options.GeneralOptions.Configuration; this.skipMetadata = options.GeneralOptions.SkipMetadata; @@ -70,15 +70,8 @@ public WebpDecoderCore(WebpDecoderOptions options) this.memoryAllocator = this.configuration.MemoryAllocator; } - /// - public DecoderOptions Options { get; } - - /// - public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { Image? image = null; try @@ -136,7 +129,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken } /// - public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { ReadImageHeader(stream, stackalloc byte[4]); @@ -186,36 +179,39 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); + WebpImageInfo webpImageInfo; WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: webpMetadata.FileFormat = WebpFileFormatType.Lossy; - return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + break; case WebpChunkType.Vp8L: webpMetadata.FileFormat = WebpFileFormatType.Lossless; - return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + break; case WebpChunkType.Vp8X: - WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8XHeader(stream, buffer, features); while (stream.Position < stream.Length) { chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); if (chunkType == WebpChunkType.Vp8) { webpMetadata.FileFormat = WebpFileFormatType.Lossy; - webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, buffer, features); } else if (chunkType == WebpChunkType.Vp8L) { webpMetadata.FileFormat = WebpFileFormatType.Lossless; - webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); + webpImageInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); } else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType)) { bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer); if (isAnimationChunk) { - return webpInfos; + break; } } else @@ -226,12 +222,17 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad } } - return webpInfos; + break; default: WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); - return - new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + + // This return will never be reached, because throw helper will throw an exception. + webpImageInfo = new(); + break; } + + this.Dimensions = new Size((int)webpImageInfo.Width, (int)webpImageInfo.Height); + return webpImageInfo; } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 2fe4282607..97be5d8383 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -67,11 +67,11 @@ private static bool SkipTest(ITestImageProvider provider) public void ParseStream_BasicPropertiesAreCorrect() { JpegDecoderOptions options = new(); + Configuration configuration = options.GeneralOptions.Configuration; byte[] bytes = TestFile.Create(TestImages.Jpeg.Progressive.Progress).Bytes; using MemoryStream ms = new(bytes); - using BufferedReadStream bufferedStream = new(Configuration.Default, ms); using JpegDecoderCore decoder = new(options); - using Image image = decoder.Decode(bufferedStream, cancellationToken: default); + using Image image = decoder.Decode(configuration, ms, cancellationToken: default); // I don't know why these numbers are different. All I know is that the decoder works // and spectral data is exactly correct also. diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 978978989e..a3fbe4018e 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -5,7 +5,6 @@ using System.Text; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -216,18 +215,17 @@ internal static bool CompareBlocks(Span a, Span b, float tolerance internal static JpegDecoderCore ParseJpegStream(string testFileName, bool metaDataOnly = false) { byte[] bytes = TestFile.Create(testFileName).Bytes; - using var ms = new MemoryStream(bytes); - using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); - - JpegDecoderOptions options = new(); - var decoder = new JpegDecoderCore(options); + using MemoryStream ms = new(bytes); + JpegDecoderOptions decoderOptions = new(); + Configuration configuration = decoderOptions.GeneralOptions.Configuration; + JpegDecoderCore decoder = new(decoderOptions); if (metaDataOnly) { - decoder.Identify(bufferedStream, cancellationToken: default); + decoder.Identify(configuration, ms, default); } else { - using Image image = decoder.Decode(bufferedStream, cancellationToken: default); + using Image image = decoder.Decode(configuration, ms, default); } return decoder; From e61585afe28c8a0b1fdba86662159c842c5e8517 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 17:04:18 +1000 Subject: [PATCH 02/73] Remove IImageEncoderInternals interface --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 3 +-- .../Formats/IImageEncoderInternals.cs | 22 ------------------- src/ImageSharp/Formats/ImageEncoder.cs | 1 - .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs | 3 +-- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs | 10 +++++++-- src/ImageSharp/Formats/Tga/TgaEncoderCore.cs | 3 +-- .../Formats/Tiff/TiffEncoderCore.cs | 3 +-- .../Formats/Webp/WebpEncoderCore.cs | 2 +- 10 files changed, 15 insertions(+), 36 deletions(-) delete mode 100644 src/ImageSharp/Formats/IImageEncoderInternals.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 076d1adf00..187170f898 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -17,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// /// Image encoder for writing an image to a stream as a Windows bitmap. /// -internal sealed class BmpEncoderCore : IImageEncoderInternals +internal sealed class BmpEncoderCore { /// /// The amount to pad each row by. diff --git a/src/ImageSharp/Formats/IImageEncoderInternals.cs b/src/ImageSharp/Formats/IImageEncoderInternals.cs deleted file mode 100644 index 131949c968..0000000000 --- a/src/ImageSharp/Formats/IImageEncoderInternals.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Abstraction for shared internals for ***DecoderCore implementations. -/// -internal interface IImageEncoderInternals -{ - /// - /// Encodes the image. - /// - /// The image. - /// The stream. - /// The token to monitor for cancellation requests. - /// The pixel type. - void Encode(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel; -} diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index 4acd29e81c..deb527f698 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 95f7fde32c..4477df35cd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; /// /// Image encoder for writing an image to a stream as a jpeg. /// -internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals +internal sealed unsafe partial class JpegEncoderCore { /// /// The available encodable frame configs. diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index b6e31a3c28..843f1880e6 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Text; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm; @@ -10,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Image encoder for writing an image to a stream as a PGM, PBM, PPM or PAM bitmap. /// -internal sealed class PbmEncoderCore : IImageEncoderInternals +internal sealed class PbmEncoderCore { private const byte NewLine = (byte)'\n'; private const byte Space = (byte)' '; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 802e4dd6a4..794b306fc1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Png; /// /// Performs the png encoding operation. /// -internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable +internal sealed class PngEncoderCore : IDisposable { /// /// The maximum block size, defaults at 64k for uncompressed blocks. diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs index 53f67e765d..88d87a3825 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi; /// /// Image encoder for writing an image to a stream as a QOi image /// -internal class QoiEncoderCore : IImageEncoderInternals +internal class QoiEncoderCore { /// /// The encoder with options @@ -41,7 +41,13 @@ public QoiEncoderCore(QoiEncoder encoder, Configuration configuration) this.memoryAllocator = configuration.MemoryAllocator; } - /// + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index bbb476c017..c1c3d23b1f 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -5,7 +5,6 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -15,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; /// /// Image encoder for writing an image to a stream as a truevision targa image. /// -internal sealed class TgaEncoderCore : IImageEncoderInternals +internal sealed class TgaEncoderCore { /// /// Used for allocating memory during processing operations. diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 149f23f1bf..59b4eac0bc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. #nullable disable -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Constants; @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// /// Performs the TIFF encoding operation. /// -internal sealed class TiffEncoderCore : IImageEncoderInternals +internal sealed class TiffEncoderCore { private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian ? TiffConstants.ByteOrderLittleEndianShort diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d29759f9a1..5d904380bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image encoder for writing an image to a stream in the Webp format. /// -internal sealed class WebpEncoderCore : IImageEncoderInternals +internal sealed class WebpEncoderCore { /// /// Used for allocating memory during processing operations. From dc6871cfc350da37b8f47acc2a680443392939b7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 3 Jul 2024 22:02:23 +1000 Subject: [PATCH 03/73] Add explicit AOT seeding for SpectralConverter --- src/ImageSharp/Advanced/AotCompilerTools.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 61ec85b47a..0d5faabe18 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -10,11 +10,13 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -130,6 +132,7 @@ private static void Seed() AotCompileImageDecoderInternals(); AotCompileImageEncoders(); AotCompileImageDecoders(); + AotCompileSpectralConverter(); AotCompileImageProcessors(); AotCompileGenericImageProcessors(); AotCompileResamplers(); @@ -269,6 +272,17 @@ private static void AotCompileImageDecoders() AotCompileImageDecoder(); } + [Preserve] + private static void AotCompileSpectralConverter() + where TPixel : unmanaged, IPixel + { + default(SpectralConverter).GetPixelBuffer(default); + default(GrayJpegSpectralConverter).GetPixelBuffer(default); + default(RgbJpegSpectralConverter).GetPixelBuffer(default); + default(TiffJpegSpectralConverter).GetPixelBuffer(default); + default(TiffOldJpegSpectralConverter).GetPixelBuffer(default); + } + /// /// This method pre-seeds the in the AoT compiler. /// From e1555fd4ba6a972b7b0121b22bf502ebb49a9606 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 5 Aug 2024 23:46:53 +1000 Subject: [PATCH 04/73] Fix transform bounds calculations --- .../Processing/AffineTransformBuilder.cs | 38 +-- .../Linear/LinearTransformUtility.cs | 4 +- .../Transforms/Linear/RotateProcessor.cs | 5 +- .../Transforms/Linear/SkewProcessor.cs | 5 +- .../Processors/Transforms/TransformUtils.cs | 274 +++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 39 +-- .../Transforms/AffineTransformTests.cs | 18 ++ .../Transforms/ProjectiveTransformTests.cs | 10 +- 8 files changed, 191 insertions(+), 202 deletions(-) diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 59264698bd..e8c628ff12 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a rotation matrix using the given rotation angle in degrees @@ -31,8 +30,7 @@ public AffineTransformBuilder PrependRotationDegrees(float degrees) /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -68,9 +66,7 @@ public AffineTransformBuilder AppendRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), - size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -145,9 +141,7 @@ public AffineTransformBuilder AppendScale(Vector2 scales) /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -156,9 +150,7 @@ public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -187,9 +179,7 @@ public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size), - size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -198,9 +188,7 @@ public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size), - size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -267,7 +255,7 @@ public AffineTransformBuilder AppendTranslation(Vector2 position) public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -283,7 +271,7 @@ public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -340,13 +328,13 @@ public Size GetTransformedSize(Rectangle sourceRectangle) // Translate the origin matrix to cater for source rectangle offsets. Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix3x2 matrix) @@ -357,17 +345,15 @@ private static void CheckDegenerate(Matrix3x2 matrix) } } - private AffineTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private AffineTransformBuilder Append(Func transformFactory, Func boundsFactory) + private AffineTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs index b5eb202c18..1f68e32744 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs @@ -43,7 +43,7 @@ public static float GetSamplingRadius(in TResampler sampler, int sou /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeStart(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max); + => Numerics.Clamp((int)MathF.Floor(center - radius), min, max); /// /// Gets the end position (inclusive) for a sampling range given @@ -56,5 +56,5 @@ public static int GetRangeStart(float radius, float center, int min, int max) /// The . [MethodImpl(InliningOptions.ShortMethod)] public static int GetRangeEnd(float radius, float center, int min, int max) - => Numerics.Clamp((int)MathF.Floor(center + radius), min, max); + => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index 6580636a24..aee7fd7816 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -29,14 +29,13 @@ public RotateProcessor(float degrees, Size sourceSize) public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), - TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor - private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 97b18de6c8..085d2bbec1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -31,7 +31,6 @@ public SkewProcessor(float degreesX, float degreesY, Size sourceSize) public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), - TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize), sampler, sourceSize) { @@ -40,8 +39,8 @@ public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size so } // Helper constructor: - private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) + private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 70112ab5a8..787e7fc3e3 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -68,6 +68,11 @@ public static bool IsNaN(Matrix4x4 matrix) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) { + // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases, + // such as when the point is transformed behind the camera in a perspective projection. + // However, in many 2D contexts, negative w values are not meaningful and could cause issues + // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure + // we don't divide by a very small or negative number, effectively treating any negative w as epsilon. const float epsilon = 0.0000001F; Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); @@ -81,9 +86,7 @@ public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. @@ -93,33 +96,7 @@ public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size. - /// - /// The amount of rotation, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); - - /// - /// Creates a centered rotation bounds matrix using the given rotation in radians and the source size. - /// - /// The amount of rotation, in radians. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateRotation(radians, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -130,9 +107,7 @@ public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size si /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -143,78 +118,28 @@ public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float d /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); - - /// - /// Creates a centered skew bounds matrix from the give angles in degrees and the source size. - /// - /// The X angle, in degrees. - /// The Y angle, in degrees. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty)); - - /// - /// Creates a centered skew bounds matrix from the give angles in radians and the source size. - /// - /// The X angle, in radians. - /// The Y angle, in radians. - /// The source image size. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewBoundsMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredBoundsMatrix( - new Rectangle(Point.Empty, size), - Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty)); + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); /// /// Gets the centered transform matrix based upon the source rectangle. /// - /// The source image bounds. - /// The transformation matrix. - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) - { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); - - // We invert the matrix to handle the transformation from screen to world space. - // This ensures scaling matrices are correct. - Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - - // Centered transforms must be 0 based so we offset the bounds width and height. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F); - - // Translate back to world space. - Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); - - return centered; - } - - /// - /// Gets the centered bounds matrix based upon the source rectangle. - /// - /// The source image bounds. /// The transformation matrix. + /// The source image size. /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredBoundsMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) { - Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); + Size destinationSize = GetUnboundedTransformedSize(matrix, size); // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. Matrix3x2.Invert(matrix, out Matrix3x2 inverted); - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); + // The source size is provided using the coordinate space of the source image. + // however the transform should always be applied in the pixel space. + // To account for this we offset by the size - 1 to translate to the pixel space. + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -236,6 +161,12 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner { Matrix4x4 matrix = Matrix4x4.Identity; + // The source size is provided using the Coordinate/Geometric space of the source image. + // However, the transform should always be applied in the Discrete/Pixel space to ensure + // that the transformation fully encompasses all pixels without clipping at the edges. + // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. + // size -= new Size(1, 1); + /* * SkMatrix is laid out in the following manner: * @@ -345,52 +276,101 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner } /// - /// Returns the rectangle bounds relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) - { - Rectangle transformed = GetTransformedRectangle(rectangle, matrix); - return new Rectangle(0, 0, transformed.Width, transformed.Height); - } + public static Size GetTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, true); /// - /// Returns the rectangle relative to the source for the given transformation matrix. + /// Returns the size relative to the source for the given transformation matrix. /// - /// The source rectangle. /// The transformation matrix. + /// The source size. /// - /// The . + /// The . /// - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size GetTransformedSize(Matrix4x4 matrix, Size size) { - if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) + Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); + + if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) { - return rectangle; + return size; } - Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); - Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); - Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); - Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + // Check if the matrix involves only affine transformations by inspecting the relevant components. + // We want to use pixel space for calculations only if the transformation is purely 2D and does not include + // any perspective effects, non-standard scaling, or unusual translations that could distort the image. + // The conditions are as follows: + bool usePixelSpace = + + // 1. Ensure there's no perspective distortion: + // M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0. + (matrix.M34 == 0) && + + // 2. Ensure standard affine transformation without any unusual depth or perspective scaling: + // M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth + // scaling or perspective, which suggests a more complex transformation. + (matrix.M44 == 1) && + + // 3. Ensure no unusual translation in the x-direction: + // M14 represents translation in the x-direction that might be part of a more complex transformation. + // For standard affine transformations, M14 should be 0. + (matrix.M14 == 0) && - return GetBoundingRectangle(tl, tr, bl, br); + // 4. Ensure no unusual translation in the y-direction: + // M24 represents translation in the y-direction that might be part of a more complex transformation. + // For standard affine transformations, M24 should be 0. + (matrix.M24 == 0); + + // Define an offset size to translate between pixel space and coordinate space. + // When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates. + // When not using pixel space, use SizeF.Empty as the offset. + + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + + // Apply the offset relative to the scale + SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty; + + // Subtract the offset size to translate to the appropriate space (pixel or coordinate). + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the correct space. + return Size.Ceiling(ConstrainSize(bounds) + offsetSize); + } + + return size; } /// /// Returns the size relative to the source for the given transformation matrix. /// + /// The transformation matrix. /// The source size. + /// + /// The . + /// + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) + => GetTransformedSize(matrix, size, false); + + /// + /// Returns the size relative to the source for the given transformation matrix. + /// /// The transformation matrix. + /// The source size. + /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - public static Size GetTransformedSize(Size size, Matrix3x2 matrix) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -399,9 +379,20 @@ public static Size GetTransformedSize(Size size, Matrix3x2 matrix) return size; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + // Define an offset size to translate between coordinate space and pixel space. + // Compute scaling factors from the matrix + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + SizeF offsetSize = new(scaleX, scaleY); + + // Subtract the offset size to translate to the pixel space. + if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) + { + // Add the offset size back to translate the transformed bounds to the coordinate space. + return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize); + } - return ConstrainSize(rectangle); + return size; } /// @@ -409,46 +400,52 @@ public static Size GetTransformedSize(Size size, Matrix3x2 matrix) /// /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) { - if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) { - return rectangle; + bounds = default; + return false; } - Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); - Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); - Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); - Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); + Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); - return GetBoundingRectangle(tl, tr, bl, br); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } /// - /// Returns the size relative to the source for the given transformation matrix. + /// Returns the rectangle relative to the source for the given transformation matrix. /// - /// The source size. + /// The source rectangle. /// The transformation matrix. + /// The resulting bounding rectangle. /// - /// The . + /// if the transformation was successful; otherwise, . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Size GetTransformedSize(Size size, Matrix4x4 matrix) + private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) { - Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); - - if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) { - return size; + bounds = default; + return false; } - Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix); + Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); + Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); + Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); + Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); - return ConstrainSize(rectangle); + bounds = GetBoundingRectangle(tl, tr, bl, br); + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -482,6 +479,11 @@ private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); - return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); + // Clamp the values to the nearest whole pixel. + return Rectangle.FromLTRB( + (int)Math.Floor(left), + (int)Math.Floor(top), + (int)Math.Ceiling(right), + (int)Math.Ceiling(bottom)); } } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 0387adebb9..06e6f0e71a 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing; public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); - private readonly List> boundsMatrixFactories = new(); /// /// Prepends a matrix that performs a tapering projective transform. @@ -22,9 +21,7 @@ public class ProjectiveTransformBuilder /// The amount to taper. /// The . public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Prepend( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Appends a matrix that performs a tapering projective transform. @@ -34,9 +31,7 @@ public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corne /// The amount to taper. /// The . public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) - => this.Append( - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction), - size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); + => this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); /// /// Prepends a centered rotation matrix using the given rotation in degrees. @@ -52,9 +47,7 @@ public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -88,9 +81,7 @@ public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)), - size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -174,9 +165,7 @@ internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float deg /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -214,9 +203,7 @@ internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degr /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append( - size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)), - size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -283,7 +270,7 @@ public ProjectiveTransformBuilder AppendTranslation(Vector2 position) public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Prepend(_ => matrix, _ => matrix); + return this.Prepend(_ => matrix); } /// @@ -299,7 +286,7 @@ public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) { CheckDegenerate(matrix); - return this.Append(_ => matrix, _ => matrix); + return this.Append(_ => matrix); } /// @@ -357,13 +344,13 @@ public Size GetTransformedSize(Rectangle sourceRectangle) // Translate the origin matrix to cater for source rectangle offsets. Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); - foreach (Func factory in this.boundsMatrixFactories) + foreach (Func factory in this.transformMatrixFactories) { matrix *= factory(size); CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(size, matrix); + return TransformUtils.GetTransformedSize(matrix, size); } private static void CheckDegenerate(Matrix4x4 matrix) @@ -374,17 +361,15 @@ private static void CheckDegenerate(Matrix4x4 matrix) } } - private ProjectiveTransformBuilder Prepend(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Prepend(Func transformFactory) { this.transformMatrixFactories.Insert(0, transformFactory); - this.boundsMatrixFactories.Insert(0, boundsFactory); return this; } - private ProjectiveTransformBuilder Append(Func transformFactory, Func boundsFactory) + private ProjectiveTransformBuilder Append(Func transformFactory) { this.transformMatrixFactories.Add(transformFactory); - this.boundsMatrixFactories.Add(boundsFactory); return this; } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index 895cf60fd3..05604ac6e6 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -231,6 +231,24 @@ public void Issue1911() Assert.Equal(100, image.Height); } + [Theory] + [WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)] + public void Issue2753(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + AffineTransformBuilder builder = + new AffineTransformBuilder().AppendRotationDegrees(270, new Vector2(3.5f, 3.5f)); + image.Mutate(x => x.BackgroundColor(Color.Red)); + image.Mutate(x => x = x.Transform(builder)); + + image.DebugSave(provider); + + Assert.Equal(4, image.Width); + Assert.Equal(8, image.Height); + } + [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] public void Identity(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 21eda034ea..128df01a06 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -128,11 +128,11 @@ public void PerspectiveTransformMatchesCSS(TestImageProvider pro using (Image image = provider.GetImage()) { #pragma warning disable SA1117 // Parameters should be on same line or separate lines - var matrix = new Matrix4x4( - 0.260987f, -0.434909f, 0, -0.0022184f, - 0.373196f, 0.949882f, 0, -0.000312129f, - 0, 0, 1, 0, - 52, 165, 0, 1); + Matrix4x4 matrix = new( + 0.260987f, -0.434909f, 0, -0.0022184f, + 0.373196f, 0.949882f, 0, -0.000312129f, + 0, 0, 1, 0, + 52, 165, 0, 1); #pragma warning restore SA1117 // Parameters should be on same line or separate lines ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() From c579547a4f0987d5defbf425be4301475c4a52d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 13 Aug 2024 22:29:47 +1000 Subject: [PATCH 05/73] Separate transform spaces --- .../Processing/AffineTransformBuilder.cs | 36 ++++++++-- .../Transforms/Linear/RotateProcessor.cs | 4 +- .../Transforms/Linear/SkewProcessor.cs | 4 +- .../Processors/Transforms/TransformUtils.cs | 70 ++++++++++--------- .../Processing/ProjectiveTransformBuilder.cs | 30 ++++++-- src/ImageSharp/Processing/TransformSpace.cs | 26 +++++++ .../Transforms/TransformBuilderTestBase.cs | 5 +- ...tPattern100x50_R(50)_S(1,1)_T(-20,-10).png | 4 +- ..._TestPattern100x50_R(50)_S(1,1)_T(0,0).png | 4 +- ...estPattern100x50_R(50)_S(1,1)_T(20,10).png | 4 +- ...ttern100x50_R(50)_S(1.1,1.3)_T(30,-20).png | 4 +- ...tPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png | 4 +- ...d_Rgba32_TestPattern96x96_R(50)_S(0.8).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- .../DrawImageTests/DrawTransformed.png | 4 +- ...sCSS_Rgba32_Solid290x154_(0,0,255,255).png | 4 +- ...pler_Rgba32_TestPattern150x150_Bicubic.png | 4 +- ...hSampler_Rgba32_TestPattern150x150_Box.png | 4 +- ...r_Rgba32_TestPattern150x150_CatmullRom.png | 4 +- ...pler_Rgba32_TestPattern150x150_Hermite.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos2.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos3.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos5.png | 4 +- ...ler_Rgba32_TestPattern150x150_Lanczos8.png | 4 +- ...2_TestPattern150x150_MitchellNetravali.png | 4 +- ...a32_TestPattern150x150_NearestNeighbor.png | 4 +- ...ler_Rgba32_TestPattern150x150_Robidoux.png | 4 +- ...gba32_TestPattern150x150_RobidouxSharp.png | 4 +- ...mpler_Rgba32_TestPattern150x150_Spline.png | 4 +- ...ler_Rgba32_TestPattern150x150_Triangle.png | 4 +- ...ampler_Rgba32_TestPattern150x150_Welch.png | 4 +- ...2_Solid30x30_(255,0,0,255)_Bottom-Both.png | 4 +- ...id30x30_(255,0,0,255)_Bottom-LeftOrTop.png | 4 +- ...x30_(255,0,0,255)_Bottom-RightOrBottom.png | 4 +- ...a32_Solid30x30_(255,0,0,255)_Left-Both.png | 4 +- ...olid30x30_(255,0,0,255)_Left-LeftOrTop.png | 4 +- ...30x30_(255,0,0,255)_Left-RightOrBottom.png | 4 +- ...32_Solid30x30_(255,0,0,255)_Right-Both.png | 4 +- ...lid30x30_(255,0,0,255)_Right-LeftOrTop.png | 4 +- ...0x30_(255,0,0,255)_Right-RightOrBottom.png | 4 +- ...ba32_Solid30x30_(255,0,0,255)_Top-Both.png | 4 +- ...Solid30x30_(255,0,0,255)_Top-LeftOrTop.png | 4 +- ...d30x30_(255,0,0,255)_Top-RightOrBottom.png | 4 +- ...sions_Rgba32_TestPattern100x100_0.0001.png | 4 +- ...Dimensions_Rgba32_TestPattern100x100_0.png | 4 +- ...imensions_Rgba32_TestPattern100x100_57.png | 4 +- ...otate_WithAngle_TestPattern100x50_-170.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_-50.png | 4 +- ...Rotate_WithAngle_TestPattern100x50_170.png | 4 +- .../Rotate_WithAngle_TestPattern100x50_50.png | 4 +- ...otate_WithAngle_TestPattern50x100_-170.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_-50.png | 4 +- ...Rotate_WithAngle_TestPattern50x100_170.png | 4 +- .../Rotate_WithAngle_TestPattern50x100_50.png | 4 +- ...lType_Bgra32_TestPattern100x50_-20_-10.png | 4 +- ...xelType_Bgra32_TestPattern100x50_20_10.png | 4 +- ...elType_Rgb24_TestPattern100x50_-20_-10.png | 4 +- ...ixelType_Rgb24_TestPattern100x50_20_10.png | 4 +- 75 files changed, 262 insertions(+), 185 deletions(-) create mode 100644 src/ImageSharp/Processing/TransformSpace.cs diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index e8c628ff12..4ac9546f39 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -13,6 +13,28 @@ public class AffineTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public AffineTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the affine transform. + /// + public AffineTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the affine transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a rotation matrix using the given rotation angle in degrees /// and the image center point as rotation center. @@ -30,7 +52,7 @@ public AffineTransformBuilder PrependRotationDegrees(float degrees) /// The . public AffineTransformBuilder PrependRotationRadians(float radians) => this.Prepend( - size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Prepends a rotation matrix using the given rotation in degrees at the given origin. @@ -66,7 +88,7 @@ public AffineTransformBuilder AppendRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public AffineTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size)); + => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); /// /// Appends a rotation matrix using the given rotation in degrees at the given origin. @@ -141,7 +163,7 @@ public AffineTransformBuilder AppendScale(Vector2 scales) /// The Y angle, in degrees. /// The . public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Prepends a centered skew matrix from the give angles in radians. @@ -150,7 +172,7 @@ public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) /// The Y angle, in radians. /// The . public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -179,7 +201,7 @@ public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, /// The Y angle, in degrees. /// The . public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size)); + => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); /// /// Appends a centered skew matrix from the give angles in radians. @@ -188,7 +210,7 @@ public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) /// The Y angle, in radians. /// The . public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)); + => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); /// /// Appends a skew matrix using the given angles in degrees at the given origin. @@ -334,7 +356,7 @@ public Size GetTransformedSize(Rectangle sourceRectangle) CheckDegenerate(matrix); } - return TransformUtils.GetTransformedSize(matrix, size); + return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace); } private static void CheckDegenerate(Matrix3x2 matrix) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs index aee7fd7816..0af2b268a1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs @@ -28,14 +28,14 @@ public RotateProcessor(float degrees, Size sourceSize) /// The source image size public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), + TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel), sampler, sourceSize) => this.Degrees = degrees; // Helper constructor private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) - : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize)) + : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs index 085d2bbec1..0bbc8e0f60 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs @@ -30,7 +30,7 @@ public SkewProcessor(float degreesX, float degreesY, Size sourceSize) /// The source image size public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) : this( - TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize), + TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel), sampler, sourceSize) { @@ -40,7 +40,7 @@ public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size so // Helper constructor: private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) - : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize)) + : base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel)) { } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 787e7fc3e3..62ea5e830d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -83,20 +83,22 @@ public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) /// /// The amount of rotation, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace) + => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace); /// /// Creates a centered rotation transform matrix using the given rotation in radians and the source size. /// /// The amount of rotation, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size); + public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in degrees and the source size. @@ -104,10 +106,11 @@ public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size /// The X angle, in degrees. /// The Y angle, in degrees. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace) + => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace); /// /// Creates a centered skew transform matrix from the give angles in radians and the source size. @@ -115,21 +118,25 @@ public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float d /// The X angle, in radians. /// The Y angle, in radians. /// The source image size. + /// The to use when creating the centered matrix. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size) - => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size); + public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace) + => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace); /// /// Gets the centered transform matrix based upon the source rectangle. /// /// The transformation matrix. /// The source image size. + /// + /// The to use when creating the centered matrix. + /// /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size) + public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace) { - Size destinationSize = GetUnboundedTransformedSize(matrix, size); + Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace); // We invert the matrix to handle the transformation from screen to world space. // This ensures scaling matrices are correct. @@ -138,8 +145,10 @@ public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size siz // The source size is provided using the coordinate space of the source image. // however the transform should always be applied in the pixel space. // To account for this we offset by the size - 1 to translate to the pixel space. - Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F); - Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F); + float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F; + + Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F); + Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F); // Translate back to world space. Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); @@ -161,12 +170,6 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner { Matrix4x4 matrix = Matrix4x4.Identity; - // The source size is provided using the Coordinate/Geometric space of the source image. - // However, the transform should always be applied in the Discrete/Pixel space to ensure - // that the transformation fully encompasses all pixels without clipping at the edges. - // To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space. - // size -= new Size(1, 1); - /* * SkMatrix is laid out in the following manner: * @@ -280,11 +283,10 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - public static Size GetTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, true); + /// The to use when calculating the size. + /// The . + public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, true); /// /// Returns the size relative to the source for the given transformation matrix. @@ -355,22 +357,22 @@ public static Size GetTransformedSize(Matrix4x4 matrix, Size size) /// /// The transformation matrix. /// The source size. - /// - /// The . - /// - private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size) - => GetTransformedSize(matrix, size, false); + /// The to use when calculating the size. + /// The . + private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) + => GetTransformedSize(matrix, size, transformSpace, false); /// /// Returns the size relative to the source for the given transformation matrix. /// /// The transformation matrix. /// The source size. + /// The to use when calculating the size. /// Whether to constrain the size to ensure that the dimensions are positive. /// /// The . /// - private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain) + private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -381,9 +383,13 @@ private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constra // Define an offset size to translate between coordinate space and pixel space. // Compute scaling factors from the matrix - float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) - float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) - SizeF offsetSize = new(scaleX, scaleY); + SizeF offsetSize = SizeF.Empty; + if (transformSpace == TransformSpace.Pixel) + { + float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2) + float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2) + offsetSize = new(scaleX, scaleY); + } // Subtract the offset size to translate to the pixel space. if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds)) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 06e6f0e71a..9027ee7266 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -13,6 +13,28 @@ public class ProjectiveTransformBuilder { private readonly List> transformMatrixFactories = new(); + /// + /// Initializes a new instance of the class. + /// + public ProjectiveTransformBuilder() + : this(TransformSpace.Pixel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to use when applying the projective transform. + /// + public ProjectiveTransformBuilder(TransformSpace transformSpace) + => this.TransformSpace = transformSpace; + + /// + /// Gets the to use when applying the projective transform. + /// + public TransformSpace TransformSpace { get; } + /// /// Prepends a matrix that performs a tapering projective transform. /// @@ -47,7 +69,7 @@ public ProjectiveTransformBuilder PrependRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder PrependRotationRadians(float radians) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -81,7 +103,7 @@ public ProjectiveTransformBuilder AppendRotationDegrees(float degrees) /// The amount of rotation, in radians. /// The . public ProjectiveTransformBuilder AppendRotationRadians(float radians) - => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); /// /// Appends a centered rotation matrix using the given rotation in degrees at the given origin. @@ -165,7 +187,7 @@ internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float deg /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) - => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Prepends a skew matrix using the given angles in degrees at the given origin. @@ -203,7 +225,7 @@ internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degr /// The Y angle, in radians. /// The . public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) - => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size))); + => this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); /// /// Appends a skew matrix using the given angles in degrees at the given origin. diff --git a/src/ImageSharp/Processing/TransformSpace.cs b/src/ImageSharp/Processing/TransformSpace.cs new file mode 100644 index 0000000000..bca676bd88 --- /dev/null +++ b/src/ImageSharp/Processing/TransformSpace.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing; + +/// +/// Represents the different spaces used in transformation operations. +/// +public enum TransformSpace +{ + /// + /// Coordinate space is a continuous, mathematical grid where objects and positions + /// are defined with precise, often fractional values. This space allows for fine-grained + /// transformations like scaling, rotation, and translation with high precision. + /// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries. + /// + Coordinate, + + /// + /// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen. + /// In this space, positions are defined by whole numbers, with no fractional values. + /// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3). + /// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen. + /// + Pixel +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs index 9d256efc1c..f046c2503c 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Tests.Processing.Transforms; @@ -97,7 +98,7 @@ public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenter this.AppendRotationDegrees(builder, degrees); // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness - Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size); + Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); @@ -151,7 +152,7 @@ public void AppendSkewDegrees_WithoutSpecificSkewCenter_SkewIsCenteredAroundImag this.AppendSkewDegrees(builder, degreesX, degreesY); - Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size); + Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size, TransformSpace.Pixel); var position = new Vector2(x, y); var expected = Vector2.Transform(position, matrix); diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png index 2ba0560e65..480c07da48 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b926c8335eca5530d8704739cecae0799cc651139daedb1f88ac85b0ee1bd5d -size 9484 +oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729 +size 9747 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png index d01fcb4a49..227f546515 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:322c7e061f8565efdc19642e27353ec3073ee43d8c17fbef8c13be3bb60d11dc -size 10190 +oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db +size 10561 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png index 171080746b..b93742a858 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:544e7bac188d0869f98ec075fa0e73ab831e4dafe40c1520dce194df6a53c9b8 -size 12737 +oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0 +size 13227 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png index 07c65142a4..57c3b02ba7 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ccc08769974e4088702d2c95fd274af7e02095955953b424a6313d656a77735 -size 19974 +oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3 +size 20778 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png index 84fffa468f..b3bfc7ee51 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1179b300b35d526bab148833ab6240f1207b8ade36674b1f47cc5a2d47a084c -size 10603 +oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a +size 10943 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index 1f1d530517..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f666fe67ee4a1c7152fc6190affba95ea4cbd857d96bac0968e5f1fd89792d32 -size 13486 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 0ce7ad5625..63214687d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b05406a1d95f0709a7aaab7c1f57ba161b7907b76746f61788cfe527796a489 -size 4131 +oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735 +size 4165 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index e93934b48d..a295c016d5 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be52d36cc8f616a781c8b1416ca0bf6207b9acd580e9c06e1ee5ad434d48ab38 -size 13481 +oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 +size 13536 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 2a68a381d4..ef37b3e2d6 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c99b8f0fb5d10a273a90946767f93ab6cd2dd1942f9829d695987db30dccfa -size 12488 +oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a +size 12559 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index 08f89da07e..93a0ce6c54 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af2c0201c59065a500ae985e9b7ca164e5bcb4ce2d8d8305103398830472e07c -size 14206 +oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2 +size 14223 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index 85d4871036..c2ca6bf57b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cef17988c4a3a667dede3dd86ed61d0507a84e5b846f52459683fd04e5a396a -size 17297 +oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272 +size 17927 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 347cbf089f..ade9a1ccd8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9699a81572c03c2bc47d8bbdd1d64df26f87df3d4ad59fb6f164f6e82786d91d -size 18853 +oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc +size 18945 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 69fe0b1355..cf04e20363 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fb1f59c5393debdff9bd4b7a6c222b7a0686e6d5ef24363e3d5c94ba9b5bc27 -size 20725 +oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af +size 20844 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 0fa8cf0c06..6be0fc0ff8 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ffa8ca6a60de9fe26a191edc2127886c61c072c1aa2b91fe3125512fe40e1b3 -size 13848 +oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b +size 13857 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 36e409ecb9..0064e973ff 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abf1d0f323795c0aaff0ff8b488d9866c5b2f7c64aad83701cb1f60e22668b0e -size 4161 +oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f +size 4154 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index f8f102e4c8..5dd0c52255 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0f3c41248138bd501ae844e5b54fb9f49e5d22bab9b2ef0a0654034708b99f -size 14027 +oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07 +size 13991 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index fc46cad7c0..a6e120e904 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a3f839d64984b9fda4125c6643f4699add6f95373a2194c5726ed3740565a47 -size 13725 +oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b +size 13744 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 58e879d4e3..d32c11d44c 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ac143bc73612cecfffbec049a7b68234e7bf7581e680c3f996a977c6d671cc1 -size 14865 +oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca +size 14889 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index fa25146d9c..72782b0b99 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb9dab20d5a03c0adde05a9b741d9e1b0fb8c3d79054a8bc5788de496e5c7f8 -size 12420 +oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f +size 12374 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index dc8ea690c2..6cedab729b 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f56ee78cc2fd698ac8ea84912648f0b49d4b4d66b439f6976447c56a44c2998 -size 16909 +oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328 +size 17027 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index 4b9953b670..7368a3b007 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd3b29b530e221618f65cd5e493b21fe3c27804fde7664636b7bb002f72abbb2 -size 3663 +oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921 +size 854 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 5f4911e47c..da66b26768 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a35757fef08a6fd9b37e719d5be7a82d5ff79f0395e082f697d9ebe9c7f03cc8 -size 5748 +oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a +size 5520 diff --git a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png index 17238cf2f7..7e693a5839 100644 --- a/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png +++ b/tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00836a98742d177a2376af32d8d858fcf9f26a4da5a311dd5faf5cd80f233c0b -size 184397 +oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d +size 184124 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png index e7ed4a95f5..f2e87f8509 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/PerspectiveTransformMatchesCSS_Rgba32_Solid290x154_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d40d0715f695dc55dc2e435cab2c999b657b59ead2e0cc8a95edf7cea7782750 -size 6842 +oid sha256:455b17bc432490435c2453424d17b92b77d036dcbc2b2ab06b960a398bd3136b +size 11119 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png index 5518047e4d..f9aa1ffe03 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d502e2d6b0eb5f12ed3262eb4654927cc937574ae1de61a1d89f6672592017 -size 11828 +oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c +size 2757 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png index a83ec12e3e..3826753d53 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac88c089bb5106d9dc9ed8e764f6495b74e1cd98d1df61319934ed78a7578866 -size 13233 +oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 +size 10951 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png index 29014117e3..2f9109ba38 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f1e69bfd1f4e9479839d4bddfb2ecc68ff8cda9b15055427406691df94a16db -size 9583 +oid sha256:57698b6666029a55edf8bd35a7ba96f68d224988cf01308a3af1c6606ae9d0b1 +size 10174 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png index d417cad9c1..7dfec78983 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd1d0350ca49ff1726c2a20769693698168497944413c653da6edcb6bc9a39e5 -size 17344 +oid sha256:fc7c9da04142a679887c714c43f1838eba0092a869140d234fce3412673207c6 +size 13575 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png index cfd3cd48ef..6e3b97f2df 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dc8ed5f721d2d8ae81f1b70308511737b0e649a6b05a011342efce29fa5b1cb -size 23022 +oid sha256:d8b973f41f8afa39b94c71b307b7eb393953e2d083d56e1f0e8f43d6ab1f342a +size 16821 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png index 2578c537e8..6986c03912 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9514b25bbc875eb00b5c0dc4241f973d466075914a9ec4c4b64ba251323eb5a -size 22321 +oid sha256:122c1501e09516244f0db36e1cca373ff68514a18e84f57ed3072d52d6112e36 +size 17022 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png index 1f6609c6f0..76b53fabfb 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09138a5ec45480a72370eba3015e7dee33360712745cb2f00e36a5994c1e48a7 -size 24090 +oid sha256:12181516bce69c9302f15bba928fd530796449599cb9744a2411cc796788ee3b +size 18066 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png index 7e4f16695a..ae4242a42b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed80155afab90710ebab4babbbb787402ceac4f9dee1ff270361e5e8e495c73a -size 13867 +oid sha256:1eb5accc5ada5b963ecef6ac15bfb1845f481e51aef63e06a522ea73bbeab945 +size 11194 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png index 3297f99e21..efb6a2deed 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96fa0ebee48290be808d5c7dec561bbcab49e5222667e175ffff01b3a175c586 -size 2308 +oid sha256:0418f0ea38ec19b407f2b5046d7ff0ed207189ad71db1e50e82c419d83620543 +size 2759 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png index 48c51e7c3c..976be43a3b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c3ce34e6bbd8e35ec979acc28510342dadf0bae3d91a9e1b8291cdff638465 -size 13873 +oid sha256:1233a9ab2c4b0b17b0248c3d40050c466330c22095287dfbdb8bf7dfbda4ff1f +size 11212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png index 379b25ca0e..04fb2e87e0 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87676e4bf55e71815acc46bdeb3384d659fe6cb3ecc7b730fd9ecc8be00d181f -size 13847 +oid sha256:e2912d4e42c7b76d9ff48a49921d6472e351662597d69b88bc3708683c7933e3 +size 11221 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png index 4bcd2dc604..b35d68aaf8 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34a62df6b4e9756a7dfef965b669b522e2338b0c2e267d35e4a37fcd9d8a55c3 -size 14818 +oid sha256:51b05c38647e0c1d88cc722e4109a882305073a065d2a27ccd3bee82f727127d +size 11775 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png index 009fc4fa60..64b9c6aba4 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60cf2a3fae30bee16131092590e2a8fe139070b6cf98c3b98698e34ededb711f -size 11996 +oid sha256:b260e816b23a43d7efb7507897ba2f5dbb6a596dd67a5abd4c9a0c005e926ee0 +size 9748 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png index 568625cd6c..29b95bf525 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2140365f533762cfc4411105be04a13f7d85739d955152c3e1951c2c69c7d67 -size 19934 +oid sha256:50b03d627bb53048f5e56380500f116da4c503f5bb6a1b1d3c0d67ee4256d8f6 +size 15977 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png index 92a99f0360..54dca26397 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57bd912acfd243882163775196e0b02c7d0374e81319cb29902cd560b5c6053 -size 394 +oid sha256:96454548849147d7c7f0ca507c8521a7d5eaa7771f9f383cc836858870b52c57 +size 280 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png index cc89f5114b..41f94c9c7b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8827b81929abd9000fcfd3ce747b82fec4c778bcd29bb12abaaed5f8b0dfc945 -size 228 +oid sha256:e94d224fdb284b6f1ba21b8caa66174edd7e6a3027f9dd03f4757e08296e6508 +size 212 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png index 61128d7b83..49cd1c8375 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5819f0deb160a7c7bf090919a010a2f8a3c074b3b718f56fe56f1f6eae1fcdcd -size 227 +oid sha256:d1162be9fa1f31bee8d3cba05c1422a1945621a412be11cce13d376efd5c679c +size 173 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png index 846fe440ef..59f928178a 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1381429147add3a9d3342bb2d618a47a1a5db997a384582a288705f68f5f937a -size 343 +oid sha256:0ed262e9b885af773a4a40a4506e678630670e208bf7f9ec10307e943b166bed +size 258 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png index 7b0692ffe2..57ee3dc2ff 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e59892f86d307e28457ebd621fec84b8ab839cdc11afe38e978420c4173058e4 -size 236 +oid sha256:3a24f2cfc225d01294b8bbc5ca7d7f1738fb0b79217046eb9edf04e4c4c01851 +size 201 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png index 4c0d43feaf..7e47f43ff9 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d25a9407bb5a3c8ba7126c9082dfbc5f29c5b52fc46e1a21e1c8968ac9f1c11 -size 230 +oid sha256:938186fb3d0f468176988a9530efd22e66241a1361fff027005ec8a8ae323ff3 +size 197 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png index a832badcb0..0f756e7813 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:398d1ca71075c541fdb88eb63f5889edea2259fdd0df643bed77e64baa246ceb -size 317 +oid sha256:4bc4b8ea7e7f10676d8de612fe6bc5144e100b95ff3fe7a1e3d4066a7684ce4d +size 239 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png index bdef30001b..b2d420886b 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62546cd019fa11a43d254936c12b5adcbe8eebe6ae012bc1024949df6b28303f -size 220 +oid sha256:345337f7dffa48d95251503ee2ae8e91db98b5cbe06b579d73c38a018c781544 +size 182 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png index 3012731317..4f0ad9d045 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e444aff3db9c5a1149de73f3b0ed18e48c8dc372be13c3555dcda0ec4ad893 -size 221 +oid sha256:de4e2b71dade9dfb750a2c614a684963d6962958db79145c87fd23d9f0f8c005 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png index dd6b926db4..78bdb8bbbe 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bdfa8eb65b5e54b9ccffb11acb2a3b7a5a434f1f22a2cdf792d892fd411f711 -size 399 +oid sha256:8d8b651663366e7543211635f337c229e2f88f1142886ea3a9b69587daaada97 +size 288 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png index fb98036ec4..7015a05571 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11a1af248350450454ba1cb415357e58f905de402a921b213a305990e8f57c3 -size 245 +oid sha256:8ab8df31f1716c05bb8687f79c7d1154f6cc6f65e3917abe60ecc42d0df173dc +size 215 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png index 0e4ef4c427..67a765e8db 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9cffb16fbff28901a3daf391469d32bf36f3c293aa870d169967f17611dea92 -size 241 +oid sha256:1a1671da9ea7702a37a866fabfb3ca0d746233ee108594198f23cb563af43ae6 +size 180 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png index e8efa8a980..9d7dc06c95 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39c25539c3c9b8926bf65c041df693a60617bbe8653bb72357bde5ab6342c59c -size 3618 +oid sha256:f5aef9cd3b8bfd9859e5d2401e82f89d89407ab2834b09c43f0a3229c735e92b +size 724 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png index ce6e8ce9fa..bef0fad79e 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 -size 3564 +oid sha256:c86a0ceb875e02b58084fd95e5c439791af313e1fb273baf00b35187a2678d2f +size 657 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png index 99a74e400a..4b2bb99d96 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b1fc95fdf07c7443147205afffb157aa82f94818cfbb833a615c42f584fbda0 -size 5070 +oid sha256:1e7dedec16ccd66543a1d44052a104957ba62099ba2f2ccc72285c233c2ae3fa +size 4411 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png index a9289abd0d..65bb77977b 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801067dfb19b2a9a1fbd14b771e780b475c21f3ccdc1e709bbc20d62061ad1d1 -size 8782 +oid sha256:3594547265b23603b1a76ff6bc6f0eab4af55d6e0070e53356123dfc7ae256f8 +size 9034 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png index 43fcd5df5e..7c54b1b074 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccbdd3dcdbc888923e85495fbd7837478cca813be6ecece63ee645bdf39d436f -size 10325 +oid sha256:5ae9ef073f3338b71d2a40fcf2e89d9b6ab62204d6de9b6a1f75f4705ee197f0 +size 10704 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png index 9a7f9866da..b6e930224e 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60d1be2ffa6d50f97561b92efe8d0c0337f9c121582e38c9ab9af75be8eed32d -size 8539 +oid sha256:994dda7da034595aa77d107652bea06c86077d24ef8a6883b18f1f509bb19928 +size 8906 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png index d141a2e28c..d1ea99cf90 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f79389f79d91ac6749f21c27a592edfd2cff6efbb1d46296a26ae60d4e721f8 -size 10103 +oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 +size 10470 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png index 1d27e23958..2f3f0f17fe 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75fb59ea2947efb1abf73cd824e54b6e8f6271343bd2fdadb005b29476988921 -size 9423 +oid sha256:29c5f48f1ece0b12854b4c44fba84fdfc9ac5751cdf564a32478dcdaed43b2a4 +size 9798 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png index 628d0c889c..5242a9d985 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2d94791af40e001fc44b23eeecac7a606492c747b22ede0cdc7069ef121cb8 -size 11193 +oid sha256:c7de58474c3f386c4ec31a9088d561a513f82c08d1157132d735169b847b9680 +size 11579 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png index 0e927cfbd0..2af9d2fc27 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:016de6e82b6fb03fd55168ea7fc12ab245d0e0387ca7c32d3ef1158a85a8facd -size 9330 +oid sha256:3ef9b7051d7a5733dfe2534fddefdc28dfbc49d087355f46c4d945b04f0e3936 +size 9672 diff --git a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png index ac4d473624..83c02764fa 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png +++ b/tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9219cc118fe7195b730cbe2e6407cde54e6f4c7930a71b7418bc7c273aa4120c -size 11050 +oid sha256:825770c9b2e9f265d834eab6b40604df5508bf9bc5b4f82f5d3effd6d5a26935 +size 11434 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png index 147f9c9897..d6dba3f889 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b243340f372220033b349a96cbd7ecd732395fa15e4d1ed62048d2031c42794 -size 8398 +oid sha256:1e283463b0f450dd72cf303acccf3dd1ff7a31fe401ff0f288d67c4baefca240 +size 8742 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png index d1252cb2c4..76bb244d52 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdd2745aba2f09bb4a09e881339fe62774ce5658902aa9d83b3a1e0718260084 -size 8694 +oid sha256:485d9d9ef955a04af43d17e6bc3952e9bf65a9752b6cf8ba9cbbe8f772f05a18 +size 8995 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png index 235e95d8fc..c1c1d814fd 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281bd00550ab1cf9b05011750e6de330cdd42a8644ecf3b7c176bd5c6e94c59b -size 6098 +oid sha256:d3d749ac365764051ea16bc39d1aff84c06faf282359805b58bb97c9eed7f0bb +size 6400 diff --git a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png index 3b63f58f36..27608881ed 100644 --- a/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png +++ b/tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e755ed61d7eab2c297f4b6592366b54ac801cbdb3d920741dfdd04dfaf73f9b9 -size 6086 +oid sha256:8d82f2a15502b0a29aa4df1077ec90c88f9211f283fdc0edd7b059ed9b387441 +size 6334 From 911480d6ebc43e006aa1a0a5038afcb9c9642e04 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 16 Oct 2024 19:10:37 +1000 Subject: [PATCH 06/73] Fix #2779 buffer overrun --- src/ImageSharp/Image.WrapMemory.cs | 9 +++--- .../Image/ImageTests.WrapMemory.cs | 30 +++++++++++++------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index d8cea246fe..03bec8bc6a 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -50,7 +50,7 @@ public static Image WrapMemory( { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); - Guard.IsTrue(pixelMemory.Length >= width * height, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(pixelMemory.Length >= (long)width * height, nameof(pixelMemory), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemory); return new Image(configuration, memorySource, width, height, metadata); @@ -145,7 +145,7 @@ public static Image WrapMemory( { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); - Guard.IsTrue(pixelMemoryOwner.Memory.Length >= width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(pixelMemoryOwner.Memory.Length >= (long)width * height, nameof(pixelMemoryOwner), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(pixelMemoryOwner); return new Image(configuration, memorySource, width, height, metadata); @@ -232,7 +232,7 @@ public static Image WrapMemory( ByteMemoryManager memoryManager = new(byteMemory); - Guard.IsTrue(memoryManager.Memory.Length >= width * height, nameof(byteMemory), "The length of the input memory is less than the specified image size"); + Guard.IsTrue(memoryManager.Memory.Length >= (long)width * height, nameof(byteMemory), "The length of the input memory is less than the specified image size"); MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); return new Image(configuration, memorySource, width, height, metadata); @@ -422,10 +422,11 @@ public static unsafe Image WrapMemory( Guard.IsFalse(pointer == null, nameof(pointer), "Pointer must be not null"); Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(metadata, nameof(metadata)); + Guard.MustBeLessThanOrEqualTo(height * (long)width, int.MaxValue, "Total amount of pixels exceeds int.MaxValue"); UnmanagedMemoryManager memoryManager = new(pointer, width * height); - Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes, memoryManager.Memory.Span.Length, nameof(bufferSizeInBytes)); + Guard.MustBeGreaterThanOrEqualTo(bufferSizeInBytes / sizeof(TPixel), memoryManager.Memory.Span.Length, nameof(bufferSizeInBytes)); MemoryGroup memorySource = MemoryGroup.Wrap(memoryManager.Memory); return new Image(configuration, memorySource, width, height, metadata); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 9aaefa41ef..46ae146942 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -294,8 +294,11 @@ public void WrapSystemDrawingBitmap_FromBytes_WhenObserved() } } - [Fact] - public unsafe void WrapMemory_Throws_OnTooLessWrongSize() + [Theory] + [InlineData(20, 5, 5)] + [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] + public unsafe void WrapMemory_Throws_OnTooLessWrongSize(int size, int width, int height) { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); @@ -306,7 +309,7 @@ public unsafe void WrapMemory_Throws_OnTooLessWrongSize() { try { - using var image = Image.WrapMemory(cfg, ptr, 24, 5, 5, metaData); + using var image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData); } catch (Exception e) { @@ -317,24 +320,30 @@ public unsafe void WrapMemory_Throws_OnTooLessWrongSize() Assert.IsType(thrownException); } - [Fact] - public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect() + [Theory] + [InlineData(25, 5, 5)] + [InlineData(26, 5, 5)] + [InlineData(2, 1, 1)] + [InlineData(1024, 32, 32)] + [InlineData(2048, 32, 32)] + public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect(int size, int width, int height) { var cfg = Configuration.CreateDefaultInstance(); var metaData = new ImageMetadata(); - var array = new Rgba32[25]; + var array = new Rgba32[size]; fixed (void* ptr = array) { - using (var image = Image.WrapMemory(cfg, ptr, 25, 5, 5, metaData)) + using (var image = Image.WrapMemory(cfg, ptr, size * sizeof(Rgba32), width, height, metaData)) { Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); Span imageSpan = imageMem.Span; + Span sourceSpan = array.AsSpan(0, width * height); ref Rgba32 pixel0 = ref imageSpan[0]; - Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); + Assert.True(Unsafe.AreSame(ref sourceSpan[0], ref pixel0)); ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; - Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1)); + Assert.True(Unsafe.AreSame(ref sourceSpan[sourceSpan.Length - 1], ref pixel_1)); Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); @@ -395,6 +404,7 @@ public unsafe void WrapSystemDrawingBitmap_FromPointer() [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_MemoryOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; @@ -430,6 +440,7 @@ private class TestMemoryOwner : IMemoryOwner [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_IMemoryOwnerOfT_InvalidSize(int size, int height, int width) { var array = new Rgba32[size]; @@ -476,6 +487,7 @@ public void WrapMemory_IMemoryOwnerOfT_ValidSize(int size, int height, int width [InlineData(0, 5, 5)] [InlineData(20, 5, 5)] [InlineData(1023, 32, 32)] + [InlineData(65536, 65537, 65536)] public void WrapMemory_IMemoryOwnerOfByte_InvalidSize(int size, int height, int width) { var array = new byte[size * Unsafe.SizeOf()]; From b8fcdda44a3ca0ab32b70cc59fc1d72c2038ff20 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 21 Oct 2024 20:19:10 +1000 Subject: [PATCH 07/73] Backport - WEBP : Use Correct Width With AlphaDecoder --- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 7 ++++--- .../Formats/Webp/Lossless/LosslessUtils.cs | 11 +++++++---- .../Formats/Webp/Lossless/WebpLosslessDecoder.cs | 7 ++++++- .../Formats/WebP/WebpEncoderTests.cs | 15 +++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/issues/Issue2801.webp | 3 +++ 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 tests/Images/Input/Webp/issues/Issue2801.webp diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index 63571617fb..ca419e619e 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -181,7 +181,7 @@ public void Decode() else { this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); - this.ExtractAlphaRows(this.Vp8LDec); + this.ExtractAlphaRows(this.Vp8LDec, this.Width); } } @@ -255,14 +255,15 @@ public void ExtractPalettedAlphaRows(int lastRow) /// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet. /// /// The VP8L decoder. - private void ExtractAlphaRows(Vp8LDecoder dec) + /// The image width. + private void ExtractAlphaRows(Vp8LDecoder dec, int width) { int numRowsToProcess = dec.Height; - int width = dec.Width; Span input = dec.Pixels.Memory.Span; Span output = this.Alpha.Memory.Span; // Extract alpha (which is stored in the green plane). + // the final width (!= dec->width_) int pixelCount = width * numRowsToProcess; WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); ExtractGreen(input, output, pixelCount); diff --git a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs index 024adb7c23..5287f0b753 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -269,7 +269,11 @@ private static void SubtractGreenFromBlueAndRedScalar(Span pixelData) /// /// The transform data contains color table size and the entries in the color table. /// The pixel data to apply the reverse transform on. - public static void ColorIndexInverseTransform(Vp8LTransform transform, Span pixelData) + /// The resulting pixel data with the reversed transformation data. + public static void ColorIndexInverseTransform( + Vp8LTransform transform, + Span pixelData, + Span outputSpan) { int bitsPerPixel = 8 >> transform.Bits; int width = transform.XSize; @@ -282,7 +286,6 @@ public static void ColorIndexInverseTransform(Vp8LTransform transform, Span>= bitsPerPixel; } } - decodedPixelData.AsSpan().CopyTo(pixelData); + outputSpan.CopyTo(pixelData); } else { diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index e4c2a7ddf6..0f366cb1e7 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -684,6 +684,7 @@ public static void ApplyInverseTransforms(Vp8LDecoder decoder, Span pixelD List transforms = decoder.Transforms; for (int i = transforms.Count - 1; i >= 0; i--) { + // TODO: Review these 1D allocations. They could conceivably exceed limits. Vp8LTransform transform = transforms[i]; switch (transform.TransformType) { @@ -701,7 +702,11 @@ public static void ApplyInverseTransforms(Vp8LDecoder decoder, Span pixelD LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); break; case Vp8LTransformType.ColorIndexingTransform: - LosslessUtils.ColorIndexInverseTransform(transform, pixelData); + using (IMemoryOwner output = memoryAllocator.Allocate(pixelData.Length, AllocationOptions.Clean)) + { + LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan()); + } + break; } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d1d83ffb9a..031a9ba059 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -514,6 +514,21 @@ public void WebpDecoder_CanDecode_Issue2763(TestImageProvider pr image.VerifyEncoder(provider, "webp", string.Empty, encoder); } + // https://github.com/SixLabors/ImageSharp/issues/2801 + [Theory] + [WithFile(Lossy.Issue2801, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Issue2801(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)); + } + public static void RunEncodeLossy_WithPeakImage() { TestImageProvider provider = TestImageProvider.File(TestImageLossyFullPath); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 847aac3478..a0e951e70d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -824,6 +824,7 @@ public static class Lossy public const string Issue2257 = "Webp/issues/Issue2257.webp"; public const string Issue2670 = "Webp/issues/Issue2670.webp"; public const string Issue2763 = "Webp/issues/Issue2763.png"; + public const string Issue2801 = "Webp/issues/Issue2801.webp"; } } diff --git a/tests/Images/Input/Webp/issues/Issue2801.webp b/tests/Images/Input/Webp/issues/Issue2801.webp new file mode 100644 index 0000000000..a3b5fee6e0 --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2801.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e90a0d853ddf70d823d8da44eb6c57081e955b1fb7f436a1fd88ca5e5c75a003 +size 261212 From 1e58db2205cf5b05b40ad988e5449c4e0da9d605 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 22 Oct 2024 17:25:35 +1000 Subject: [PATCH 08/73] Remove ChunkedMemoryStream --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- .../Sections/GifXmpApplicationExtension.cs | 10 +- src/ImageSharp/Formats/ImageDecoder.cs | 12 +- src/ImageSharp/Formats/ImageEncoder.cs | 7 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 585 ------------------ src/ImageSharp/Image.FromStream.cs | 5 +- .../Formats/WebP/WebpEncoderTests.cs | 19 + .../IO/ChunkedMemoryStreamTests.cs | 373 ----------- .../Image/NonSeekableStream.cs | 6 +- 9 files changed, 36 insertions(+), 983 deletions(-) delete mode 100644 src/ImageSharp/IO/ChunkedMemoryStream.cs delete mode 100644 tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 68f4e5fa2d..c45450a47b 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -317,7 +317,7 @@ private void ReadApplicationExtension(BufferedReadStream stream) bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp && !this.skipMetadata) { - GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream); if (extension.Data.Length > 0) { this.metadata!.XmpProfile = new XmpProfile(extension.Data); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 1c1127c3be..8bd8497eea 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif; @@ -26,11 +25,10 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// Reads the XMP metadata from the specified stream. /// /// The stream to read from. - /// The memory allocator. /// The XMP metadata - public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) + public static GifXmpApplicationExtension Read(Stream stream) { - byte[] xmpBytes = ReadXmpData(stream, allocator); + byte[] xmpBytes = ReadXmpData(stream); // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 @@ -71,9 +69,9 @@ public int WriteTo(Span buffer) return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) + private static byte[] ReadXmpData(Stream stream) { - using ChunkedMemoryStream bytes = new(allocator); + using MemoryStream bytes = new(); // XMP data doesn't have a fixed length nor is there an indicator of the length. // So we simply read one byte at a time until we hit the 0x0 value at the end diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 549a28d409..03cfa27cfb 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -210,7 +209,7 @@ T PerformActionAndResetPosition(Stream s, long position) } Configuration configuration = options.Configuration; - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + using MemoryStream memoryStream = new(); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -266,11 +265,6 @@ Task PerformActionAndResetPosition(Stream s, long position, CancellationToken return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } - if (stream is ChunkedMemoryStream cms) - { - return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); - } - return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } @@ -282,9 +276,11 @@ private static async Task CopyToMemoryStreamAndActionAsync( { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + + await using MemoryStream memoryStream = new(); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; + return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); } diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index deb527f698..34d34c3637 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -48,8 +47,8 @@ private void EncodeWithSeekableStream(Image image, Stream stream } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); - this.Encode(image, stream, cancellationToken); + using MemoryStream ms = new(); + this.Encode(image, ms, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -65,7 +64,7 @@ private async Task EncodeWithSeekableStreamAsync(Image image, St } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await using MemoryStream ms = new(); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs deleted file mode 100644 index 2534548141..0000000000 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ /dev/null @@ -1,585 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.IO; - -/// -/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized. -/// Chunks are allocated by the assigned via the constructor -/// and is designed to take advantage of buffer pooling when available. -/// -internal sealed class ChunkedMemoryStream : Stream -{ - // The memory allocator. - private readonly MemoryAllocator allocator; - - // Data - private MemoryChunk? memoryChunk; - - // The total number of allocated chunks - private int chunkCount; - - // The length of the largest contiguous buffer that can be handled by the allocator. - private readonly int allocatorCapacity; - - // Has the stream been disposed. - private bool isDisposed; - - // Current chunk to write to - private MemoryChunk? writeChunk; - - // Offset into chunk to write to - private int writeOffset; - - // Current chunk to read from - private MemoryChunk? readChunk; - - // Offset into chunk to read from - private int readOffset; - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - public ChunkedMemoryStream(MemoryAllocator allocator) - { - this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); - this.allocator = allocator; - } - - /// - public override bool CanRead => !this.isDisposed; - - /// - public override bool CanSeek => !this.isDisposed; - - /// - public override bool CanWrite => !this.isDisposed; - - /// - public override long Length - { - get - { - this.EnsureNotDisposed(); - - int length = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - MemoryChunk? next = chunk.Next; - if (next != null) - { - length += chunk.Length; - } - else - { - length += this.writeOffset; - } - - chunk = next; - } - - return length; - } - } - - /// - public override long Position - { - get - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - return 0; - } - - int pos = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != this.readChunk && chunk is not null) - { - pos += chunk.Length; - chunk = chunk.Next; - } - - pos += this.readOffset; - - return pos; - } - - set - { - this.EnsureNotDisposed(); - - if (value < 0) - { - ThrowArgumentOutOfRange(nameof(value)); - } - - // Back up current position in case new position is out of range - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = null; - this.readOffset = 0; - - int leftUntilAtPos = (int)value; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - if ((leftUntilAtPos < chunk.Length) - || ((leftUntilAtPos == chunk.Length) - && (chunk.Next is null))) - { - // The desired position is in this chunk - this.readChunk = chunk; - this.readOffset = leftUntilAtPos; - break; - } - - leftUntilAtPos -= chunk.Length; - chunk = chunk.Next; - } - - if (this.readChunk is null) - { - // Position is out of range - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - } - } - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override long Seek(long offset, SeekOrigin origin) - { - this.EnsureNotDisposed(); - - switch (origin) - { - case SeekOrigin.Begin: - this.Position = offset; - break; - - case SeekOrigin.Current: - this.Position += offset; - break; - - case SeekOrigin.End: - this.Position = this.Length + offset; - break; - default: - ThrowInvalidSeek(); - break; - } - - return this.Position; - } - - /// - public override void SetLength(long value) - => throw new NotSupportedException(); - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - try - { - this.isDisposed = true; - if (disposing) - { - ReleaseMemoryChunks(this.memoryChunk); - } - - this.memoryChunk = null; - this.writeChunk = null; - this.readChunk = null; - this.chunkCount = 0; - } - finally - { - base.Dispose(disposing); - } - } - - /// - public override void Flush() - { - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(byte[] buffer, int offset, int count) - { - Guard.NotNull(buffer, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); - Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); - - const string bufferMessage = "Offset subtracted from the buffer length is less than count."; - Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - - return this.ReadImpl(buffer.AsSpan(offset, count)); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(Span buffer) => this.ReadImpl(buffer); - - private int ReadImpl(Span buffer) - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - int bytesRead = 0; - int offset = 0; - int count = buffer.Length; - while (count > 0) - { - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - } - - int readCount = Math.Min(count, chunkSize - this.readOffset); - chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); - offset += readCount; - count -= readCount; - this.readOffset += readCount; - bytesRead += readCount; - } - - return bytesRead; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int ReadByte() - { - this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - return -1; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - } - - return chunkBuffer.GetSpan()[this.readOffset++]; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(byte[] buffer, int offset, int count) - { - Guard.NotNull(buffer, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); - Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); - - const string bufferMessage = "Offset subtracted from the buffer length is less than count."; - Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - - this.WriteImpl(buffer.AsSpan(offset, count)); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); - - private void WriteImpl(ReadOnlySpan buffer) - { - this.EnsureNotDisposed(); - - if (this.memoryChunk is null) - { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; - } - - Guard.NotNull(this.writeChunk); - - Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); - int chunkSize = this.writeChunk.Length; - int count = buffer.Length; - int offset = 0; - while (count > 0) - { - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer.GetSpan(); - chunkSize = this.writeChunk.Length; - } - - int copyCount = Math.Min(count, chunkSize - this.writeOffset); - buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); - - offset += copyCount; - count -= copyCount; - this.writeOffset += copyCount; - } - } - - /// - public override void WriteByte(byte value) - { - this.EnsureNotDisposed(); - - if (this.memoryChunk is null) - { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; - } - - Guard.NotNull(this.writeChunk); - - IMemoryOwner chunkBuffer = this.writeChunk.Buffer; - int chunkSize = this.writeChunk.Length; - - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer; - } - - chunkBuffer.GetSpan()[this.writeOffset++] = value; - } - - /// - /// Copy entire buffer into an array. - /// - /// The . - public byte[] ToArray() - { - int length = (int)this.Length; // This will throw if stream is closed - byte[] copy = new byte[this.Length]; - - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - this.Read(copy, 0, length); - - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - - return copy; - } - - /// - /// Write remainder of this stream to another stream. - /// - /// The stream to write to. - public void WriteTo(Stream stream) - { - this.EnsureNotDisposed(); - - Guard.NotNull(stream, nameof(stream)); - - if (this.readChunk is null) - { - if (this.memoryChunk is null) - { - return; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - // Following code mirrors Read() logic (readChunk/readOffset should - // point just past last byte of last chunk when done) - // loop until end of chunks is found - while (true) - { - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - } - - int writeCount = chunkSize - this.readOffset; - stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); - this.readOffset = chunkSize; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureNotDisposed() - { - if (this.isDisposed) - { - ThrowDisposed(); - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private MemoryChunk AllocateMemoryChunk() - { - // Tweak our buffer sizes to take the minimum of the provided buffer sizes - // or the allocator buffer capacity which provides us with the largest - // available contiguous buffer size. - IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); - - return new MemoryChunk(buffer) - { - Next = null, - Length = buffer.Length() - }; - } - - private static void ReleaseMemoryChunks(MemoryChunk? chunk) - { - while (chunk != null) - { - chunk.Dispose(); - chunk = chunk.Next; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetChunkSize(int i) - { - // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. - // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 -#pragma warning disable IDE1006 // Naming Styles - const int _128K = 1 << 17; - const int _4M = 1 << 22; - return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; -#pragma warning restore IDE1006 // Naming Styles - } - - private sealed class MemoryChunk : IDisposable - { - private bool isDisposed; - - public MemoryChunk(IMemoryOwner buffer) => this.Buffer = buffer; - - public IMemoryOwner Buffer { get; } - - public MemoryChunk? Next { get; set; } - - public int Length { get; init; } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - this.Buffer.Dispose(); - } - - this.isDisposed = true; - } - } - - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 63f9e64f6c..c73d2880a2 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -301,7 +300,7 @@ internal static T WithSeekableStream( return action(stream); } - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + using MemoryStream memoryStream = new(); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -343,7 +342,7 @@ internal static async Task WithSeekableStreamAsync( return await action(stream, cancellationToken).ConfigureAwait(false); } - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await using MemoryStream memoryStream = new(); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d1d83ffb9a..dd94606084 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -529,6 +529,25 @@ public static void RunEncodeLossy_WithPeakImage() [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public void CanSave_NonSeekableStream(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + using MemoryStream memoryStream = new(); + using NonSeekableStream nonSeekable = new(memoryStream); + + image.Save(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs deleted file mode 100644 index 1803cfddb9..0000000000 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - -namespace SixLabors.ImageSharp.Tests.IO; - -/// -/// Tests for the class. -/// -public class ChunkedMemoryStreamTests -{ - /// - /// The default length in bytes of each buffer chunk when allocating large buffers. - /// - private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb - - /// - /// The default length in bytes of each buffer chunk when allocating small buffers. - /// - private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb - - private readonly MemoryAllocator allocator; - - public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; - - [Fact] - public void MemoryStream_GetPositionTest_Negative() - { - using var ms = new ChunkedMemoryStream(this.allocator); - long iCurrentPos = ms.Position; - for (int i = -1; i > -6; i--) - { - Assert.Throws(() => ms.Position = i); - Assert.Equal(ms.Position, iCurrentPos); - } - } - - [Fact] - public void MemoryStream_ReadTest_Negative() - { - var ms2 = new ChunkedMemoryStream(this.allocator); - - Assert.Throws(() => ms2.Read(null, 0, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, -1)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 2, 0)); - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 2)); - - ms2.Dispose(); - - Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 1)); - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i], cms.ReadByte()); - } - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteBufferTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - byte[] buffer = new byte[2]; - for (int i = 0; i < expected.Length; i += 2) - { - cms.Read(buffer); - Assert.Equal(expected[i], buffer[0]); - Assert.Equal(expected[i + 1], buffer[1]); - } - } - - [Theory] - [InlineData(DefaultSmallChunkSize)] - [InlineData((int)(DefaultSmallChunkSize * 1.5))] - [InlineData(DefaultSmallChunkSize * 4)] - [InlineData((int)(DefaultSmallChunkSize * 5.5))] - [InlineData(DefaultSmallChunkSize * 16)] - public void MemoryStream_ReadByteBufferSpanTest(int length) - { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); - - ms.CopyTo(cms); - cms.Position = 0; - byte[] expected = ms.ToArray(); - Span buffer = new byte[2]; - for (int i = 0; i < expected.Length; i += 2) - { - cms.Read(buffer); - Assert.Equal(expected[i], buffer[0]); - Assert.Equal(expected[i + 1], buffer[1]); - } - } - - [Fact] - public void MemoryStream_WriteToTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - // [] Write to memoryStream, check the memoryStream - ms2.Write(bytArr, 0, bytArr.Length); - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - - // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - ms2.Write(bytArr, 0, bytArr.Length); - ms2.WriteTo(ms3); - ms3.Position = 0; - bytArrRet = new byte[(int)ms3.Length]; - ms3.Read(bytArrRet, 0, (int)ms3.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteToSpanTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - // [] Write to memoryStream, check the memoryStream - ms2.Write(bytArr, 0, bytArr.Length); - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - - // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) - { - Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - ms2.Write(bytArr, 0, bytArr.Length); - ms2.WriteTo(ms3); - ms3.Position = 0; - bytArrRet = new byte[(int)ms3.Length]; - ms3.Read(bytArrRet, 0, (int)ms3.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteByteTests() - { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - - for (int i = 0; i < bytArr.Length; i++) - { - ms2.WriteByte(bytArr[i]); - } - - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } - } - } - - [Fact] - public void MemoryStream_WriteToTests_Negative() - { - using var ms2 = new ChunkedMemoryStream(this.allocator); - Assert.Throws(() => ms2.WriteTo(null)); - - ms2.Write(new byte[] { 1 }, 0, 1); - var readonlyStream = new MemoryStream(new byte[1028], false); - Assert.Throws(() => ms2.WriteTo(readonlyStream)); - - readonlyStream.Dispose(); - - // [] Pass in a closed stream - Assert.Throws(() => ms2.WriteTo(readonlyStream)); - } - - [Fact] - public void MemoryStream_CopyTo_Invalid() - { - ChunkedMemoryStream memoryStream; - const string bufferSize = nameof(bufferSize); - using (memoryStream = new ChunkedMemoryStream(this.allocator)) - { - const string destination = nameof(destination); - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null)); - - // Validate the destination parameter first. - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); - Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); - - // Then bufferSize. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); - } - - // After the Stream is disposed, we should fail on all CopyTos. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. - Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); - - ChunkedMemoryStream disposedStream = memoryStream; - - // We should throw first for the source being disposed... - Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); - - // Then for the destination being disposed. - memoryStream = new ChunkedMemoryStream(this.allocator); - Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); - memoryStream.Dispose(); - } - - [Theory] - [MemberData(nameof(CopyToData))] - public void CopyTo(Stream source, byte[] expected) - { - using var destination = new ChunkedMemoryStream(this.allocator); - source.CopyTo(destination); - Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. - Assert.Equal(expected, destination.ToArray()); - } - - public static IEnumerable GetAllTestImages() - { - IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) - .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); - - var result = new List(); - foreach (string path in allImageFiles) - { - result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); - } - - return result; - } - - public static IEnumerable AllTestImages = GetAllTestImages(); - - [Theory] - [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] - public void DecoderIntegrationTest(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - if (!TestEnvironment.Is64BitProcess) - { - return; - } - - Image expected; - try - { - expected = provider.GetImage(); - } - catch - { - // The image is invalid - return; - } - - string fullPath = Path.Combine( - TestEnvironment.InputImagesDirectoryFullPath, - ((TestImageProvider.FileProvider)provider).FilePath); - - using FileStream fs = File.OpenRead(fullPath); - using var nonSeekableStream = new NonSeekableStream(fs); - - var actual = Image.Load(nonSeekableStream); - - ImageComparer.Exact.VerifySimilarity(expected, actual); - } - - public static IEnumerable CopyToData() - { - // Stream is positioned @ beginning of data - byte[] data1 = new byte[] { 1, 2, 3 }; - var stream1 = new MemoryStream(data1); - - yield return new object[] { stream1, data1 }; - - // Stream is positioned in the middle of data - byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; - var stream2 = new MemoryStream(data2) { Position = 1 }; - - yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; - - // Stream is positioned after end of data - byte[] data3 = data2; - var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; - - yield return new object[] { stream3, Array.Empty() }; - } - - private MemoryStream CreateTestStream(int length) - { - byte[] buffer = new byte[length]; - var random = new Random(); - random.NextBytes(buffer); - - return new MemoryStream(buffer); - } -} diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 4b1f6e1568..2941490e9a 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ public NonSeekableStream(Stream dataStream) public override bool CanSeek => false; - public override bool CanWrite => false; + public override bool CanWrite => this.dataStream.CanWrite; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => throw new NotImplementedException(); + => this.dataStream.Write(buffer, offset, count); } From 1a150780fd5dc79194e02568503a12b36cbc42cb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 22 Oct 2024 19:37:44 +1000 Subject: [PATCH 09/73] Update BufferedStreams.cs --- .../General/IO/BufferedStreams.cs | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs index 2a926d1cd8..a7b22e7ab8 100644 --- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs @@ -19,12 +19,8 @@ public class BufferedStreams private MemoryStream stream4; private MemoryStream stream5; private MemoryStream stream6; - private ChunkedMemoryStream chunkedMemoryStream1; - private ChunkedMemoryStream chunkedMemoryStream2; private BufferedReadStream bufferedStream1; private BufferedReadStream bufferedStream2; - private BufferedReadStream bufferedStream3; - private BufferedReadStream bufferedStream4; private BufferedReadStreamWrapper bufferedStreamWrap1; private BufferedReadStreamWrapper bufferedStreamWrap2; @@ -39,18 +35,8 @@ public void CreateStreams() this.stream6 = new MemoryStream(this.buffer); this.stream6 = new MemoryStream(this.buffer); - this.chunkedMemoryStream1 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); - this.chunkedMemoryStream1.Write(this.buffer); - this.chunkedMemoryStream1.Position = 0; - - this.chunkedMemoryStream2 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); - this.chunkedMemoryStream2.Write(this.buffer); - this.chunkedMemoryStream2.Position = 0; - this.bufferedStream1 = new BufferedReadStream(Configuration.Default, this.stream3); this.bufferedStream2 = new BufferedReadStream(Configuration.Default, this.stream4); - this.bufferedStream3 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream1); - this.bufferedStream4 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream2); this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5); this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6); } @@ -60,12 +46,8 @@ public void DestroyStreams() { this.bufferedStream1?.Dispose(); this.bufferedStream2?.Dispose(); - this.bufferedStream3?.Dispose(); - this.bufferedStream4?.Dispose(); this.bufferedStreamWrap1?.Dispose(); this.bufferedStreamWrap2?.Dispose(); - this.chunkedMemoryStream1?.Dispose(); - this.chunkedMemoryStream2?.Dispose(); this.stream1?.Dispose(); this.stream2?.Dispose(); this.stream3?.Dispose(); @@ -104,21 +86,6 @@ public int BufferedReadStreamRead() return r; } - [Benchmark] - public int BufferedReadStreamChunkedRead() - { - int r = 0; - BufferedReadStream reader = this.bufferedStream3; - byte[] b = this.chunk2; - - for (int i = 0; i < reader.Length / 2; i++) - { - r += reader.Read(b, 0, 2); - } - - return r; - } - [Benchmark] public int BufferedReadStreamWrapRead() { @@ -162,20 +129,6 @@ public int BufferedReadStreamReadByte() return r; } - [Benchmark] - public int BufferedReadStreamChunkedReadByte() - { - int r = 0; - BufferedReadStream reader = this.bufferedStream4; - - for (int i = 0; i < reader.Length; i++) - { - r += reader.ReadByte(); - } - - return r; - } - [Benchmark] public int BufferedReadStreamWrapReadByte() { From c418bb0def340b1a2c25d09e27b0404842f58227 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 09:50:03 +1000 Subject: [PATCH 10/73] Revert "Remove ChunkedMemoryStream" This reverts commit 1e58db2205cf5b05b40ad988e5449c4e0da9d605. --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- .../Sections/GifXmpApplicationExtension.cs | 10 +- src/ImageSharp/Formats/ImageDecoder.cs | 12 +- src/ImageSharp/Formats/ImageEncoder.cs | 7 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 585 ++++++++++++++++++ src/ImageSharp/Image.FromStream.cs | 5 +- .../Formats/WebP/WebpEncoderTests.cs | 19 - .../IO/ChunkedMemoryStreamTests.cs | 373 +++++++++++ .../Image/NonSeekableStream.cs | 6 +- 9 files changed, 983 insertions(+), 36 deletions(-) create mode 100644 src/ImageSharp/IO/ChunkedMemoryStream.cs create mode 100644 tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index c45450a47b..68f4e5fa2d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -317,7 +317,7 @@ private void ReadApplicationExtension(BufferedReadStream stream) bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp && !this.skipMetadata) { - GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream); + GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator); if (extension.Data.Length > 0) { this.metadata!.XmpProfile = new XmpProfile(extension.Data); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 8bd8497eea..1c1127c3be 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif; @@ -25,10 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// Reads the XMP metadata from the specified stream. /// /// The stream to read from. + /// The memory allocator. /// The XMP metadata - public static GifXmpApplicationExtension Read(Stream stream) + public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) { - byte[] xmpBytes = ReadXmpData(stream); + byte[] xmpBytes = ReadXmpData(stream, allocator); // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 @@ -69,9 +71,9 @@ public int WriteTo(Span buffer) return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream) + private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) { - using MemoryStream bytes = new(); + using ChunkedMemoryStream bytes = new(allocator); // XMP data doesn't have a fixed length nor is there an indicator of the length. // So we simply read one byte at a time until we hit the 0x0 value at the end diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 03cfa27cfb..549a28d409 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -209,7 +210,7 @@ T PerformActionAndResetPosition(Stream s, long position) } Configuration configuration = options.Configuration; - using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -265,6 +266,11 @@ Task PerformActionAndResetPosition(Stream s, long position, CancellationToken return PerformActionAndResetPosition(ms, ms.Position, cancellationToken); } + if (stream is ChunkedMemoryStream cms) + { + return PerformActionAndResetPosition(cms, cms.Position, cancellationToken); + } + return CopyToMemoryStreamAndActionAsync(options, stream, PerformActionAndResetPosition, cancellationToken); } @@ -276,11 +282,9 @@ private static async Task CopyToMemoryStreamAndActionAsync( { long position = stream.CanSeek ? stream.Position : 0; Configuration configuration = options.Configuration; - - await using MemoryStream memoryStream = new(); + await using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; - return await action(memoryStream, position, cancellationToken).ConfigureAwait(false); } diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index 34d34c3637..deb527f698 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats; @@ -47,8 +48,8 @@ private void EncodeWithSeekableStream(Image image, Stream stream } else { - using MemoryStream ms = new(); - this.Encode(image, ms, cancellationToken); + using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + this.Encode(image, stream, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -64,7 +65,7 @@ private async Task EncodeWithSeekableStreamAsync(Image image, St } else { - await using MemoryStream ms = new(); + using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs new file mode 100644 index 0000000000..2534548141 --- /dev/null +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -0,0 +1,585 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.IO; + +/// +/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized. +/// Chunks are allocated by the assigned via the constructor +/// and is designed to take advantage of buffer pooling when available. +/// +internal sealed class ChunkedMemoryStream : Stream +{ + // The memory allocator. + private readonly MemoryAllocator allocator; + + // Data + private MemoryChunk? memoryChunk; + + // The total number of allocated chunks + private int chunkCount; + + // The length of the largest contiguous buffer that can be handled by the allocator. + private readonly int allocatorCapacity; + + // Has the stream been disposed. + private bool isDisposed; + + // Current chunk to write to + private MemoryChunk? writeChunk; + + // Offset into chunk to write to + private int writeOffset; + + // Current chunk to read from + private MemoryChunk? readChunk; + + // Offset into chunk to read from + private int readOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + public ChunkedMemoryStream(MemoryAllocator allocator) + { + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); + this.allocator = allocator; + } + + /// + public override bool CanRead => !this.isDisposed; + + /// + public override bool CanSeek => !this.isDisposed; + + /// + public override bool CanWrite => !this.isDisposed; + + /// + public override long Length + { + get + { + this.EnsureNotDisposed(); + + int length = 0; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != null) + { + MemoryChunk? next = chunk.Next; + if (next != null) + { + length += chunk.Length; + } + else + { + length += this.writeOffset; + } + + chunk = next; + } + + return length; + } + } + + /// + public override long Position + { + get + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + return 0; + } + + int pos = 0; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != this.readChunk && chunk is not null) + { + pos += chunk.Length; + chunk = chunk.Next; + } + + pos += this.readOffset; + + return pos; + } + + set + { + this.EnsureNotDisposed(); + + if (value < 0) + { + ThrowArgumentOutOfRange(nameof(value)); + } + + // Back up current position in case new position is out of range + MemoryChunk? backupReadChunk = this.readChunk; + int backupReadOffset = this.readOffset; + + this.readChunk = null; + this.readOffset = 0; + + int leftUntilAtPos = (int)value; + MemoryChunk? chunk = this.memoryChunk; + while (chunk != null) + { + if ((leftUntilAtPos < chunk.Length) + || ((leftUntilAtPos == chunk.Length) + && (chunk.Next is null))) + { + // The desired position is in this chunk + this.readChunk = chunk; + this.readOffset = leftUntilAtPos; + break; + } + + leftUntilAtPos -= chunk.Length; + chunk = chunk.Next; + } + + if (this.readChunk is null) + { + // Position is out of range + this.readChunk = backupReadChunk; + this.readOffset = backupReadOffset; + } + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override long Seek(long offset, SeekOrigin origin) + { + this.EnsureNotDisposed(); + + switch (origin) + { + case SeekOrigin.Begin: + this.Position = offset; + break; + + case SeekOrigin.Current: + this.Position += offset; + break; + + case SeekOrigin.End: + this.Position = this.Length + offset; + break; + default: + ThrowInvalidSeek(); + break; + } + + return this.Position; + } + + /// + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + try + { + this.isDisposed = true; + if (disposing) + { + ReleaseMemoryChunks(this.memoryChunk); + } + + this.memoryChunk = null; + this.writeChunk = null; + this.readChunk = null; + this.chunkCount = 0; + } + finally + { + base.Dispose(disposing); + } + } + + /// + public override void Flush() + { + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + return this.ReadImpl(buffer.AsSpan(offset, count)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(Span buffer) => this.ReadImpl(buffer); + + private int ReadImpl(Span buffer) + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return 0; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + int bytesRead = 0; + int offset = 0; + int count = buffer.Length; + while (count > 0) + { + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + break; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + } + + int readCount = Math.Min(count, chunkSize - this.readOffset); + chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); + offset += readCount; + count -= readCount; + this.readOffset += readCount; + bytesRead += readCount; + } + + return bytesRead; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int ReadByte() + { + this.EnsureNotDisposed(); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return 0; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + return -1; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + } + + return chunkBuffer.GetSpan()[this.readOffset++]; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(byte[] buffer, int offset, int count) + { + Guard.NotNull(buffer, nameof(buffer)); + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); + + const string bufferMessage = "Offset subtracted from the buffer length is less than count."; + Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); + + this.WriteImpl(buffer.AsSpan(offset, count)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); + + private void WriteImpl(ReadOnlySpan buffer) + { + this.EnsureNotDisposed(); + + if (this.memoryChunk is null) + { + this.memoryChunk = this.AllocateMemoryChunk(); + this.writeChunk = this.memoryChunk; + this.writeOffset = 0; + } + + Guard.NotNull(this.writeChunk); + + Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); + int chunkSize = this.writeChunk.Length; + int count = buffer.Length; + int offset = 0; + while (count > 0) + { + if (this.writeOffset == chunkSize) + { + // Allocate a new chunk if the current one is full + this.writeChunk.Next = this.AllocateMemoryChunk(); + this.writeChunk = this.writeChunk.Next; + this.writeOffset = 0; + chunkBuffer = this.writeChunk.Buffer.GetSpan(); + chunkSize = this.writeChunk.Length; + } + + int copyCount = Math.Min(count, chunkSize - this.writeOffset); + buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); + + offset += copyCount; + count -= copyCount; + this.writeOffset += copyCount; + } + } + + /// + public override void WriteByte(byte value) + { + this.EnsureNotDisposed(); + + if (this.memoryChunk is null) + { + this.memoryChunk = this.AllocateMemoryChunk(); + this.writeChunk = this.memoryChunk; + this.writeOffset = 0; + } + + Guard.NotNull(this.writeChunk); + + IMemoryOwner chunkBuffer = this.writeChunk.Buffer; + int chunkSize = this.writeChunk.Length; + + if (this.writeOffset == chunkSize) + { + // Allocate a new chunk if the current one is full + this.writeChunk.Next = this.AllocateMemoryChunk(); + this.writeChunk = this.writeChunk.Next; + this.writeOffset = 0; + chunkBuffer = this.writeChunk.Buffer; + } + + chunkBuffer.GetSpan()[this.writeOffset++] = value; + } + + /// + /// Copy entire buffer into an array. + /// + /// The . + public byte[] ToArray() + { + int length = (int)this.Length; // This will throw if stream is closed + byte[] copy = new byte[this.Length]; + + MemoryChunk? backupReadChunk = this.readChunk; + int backupReadOffset = this.readOffset; + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + this.Read(copy, 0, length); + + this.readChunk = backupReadChunk; + this.readOffset = backupReadOffset; + + return copy; + } + + /// + /// Write remainder of this stream to another stream. + /// + /// The stream to write to. + public void WriteTo(Stream stream) + { + this.EnsureNotDisposed(); + + Guard.NotNull(stream, nameof(stream)); + + if (this.readChunk is null) + { + if (this.memoryChunk is null) + { + return; + } + + this.readChunk = this.memoryChunk; + this.readOffset = 0; + } + + IMemoryOwner chunkBuffer = this.readChunk.Buffer; + int chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + + // Following code mirrors Read() logic (readChunk/readOffset should + // point just past last byte of last chunk when done) + // loop until end of chunks is found + while (true) + { + if (this.readOffset == chunkSize) + { + // Exit if no more chunks are currently available + if (this.readChunk.Next is null) + { + break; + } + + this.readChunk = this.readChunk.Next; + this.readOffset = 0; + chunkBuffer = this.readChunk.Buffer; + chunkSize = this.readChunk.Length; + if (this.readChunk.Next is null) + { + chunkSize = this.writeOffset; + } + } + + int writeCount = chunkSize - this.readOffset; + stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); + this.readOffset = chunkSize; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureNotDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private MemoryChunk AllocateMemoryChunk() + { + // Tweak our buffer sizes to take the minimum of the provided buffer sizes + // or the allocator buffer capacity which provides us with the largest + // available contiguous buffer size. + IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); + + return new MemoryChunk(buffer) + { + Next = null, + Length = buffer.Length() + }; + } + + private static void ReleaseMemoryChunks(MemoryChunk? chunk) + { + while (chunk != null) + { + chunk.Dispose(); + chunk = chunk.Next; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 +#pragma warning disable IDE1006 // Naming Styles + const int _128K = 1 << 17; + const int _4M = 1 << 22; + return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; +#pragma warning restore IDE1006 // Naming Styles + } + + private sealed class MemoryChunk : IDisposable + { + private bool isDisposed; + + public MemoryChunk(IMemoryOwner buffer) => this.Buffer = buffer; + + public IMemoryOwner Buffer { get; } + + public MemoryChunk? Next { get; set; } + + public int Length { get; init; } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.Buffer.Dispose(); + } + + this.isDisposed = true; + } + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index c73d2880a2..63f9e64f6c 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -300,7 +301,7 @@ internal static T WithSeekableStream( return action(stream); } - using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; @@ -342,7 +343,7 @@ internal static async Task WithSeekableStreamAsync( return await action(stream, cancellationToken).ConfigureAwait(false); } - await using MemoryStream memoryStream = new(); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 96cdfe8539..031a9ba059 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -544,25 +544,6 @@ public static void RunEncodeLossy_WithPeakImage() [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); - [Theory] - [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] - public void CanSave_NonSeekableStream(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - WebpEncoder encoder = new(); - - using MemoryStream seekable = new(); - image.Save(seekable, encoder); - - using MemoryStream memoryStream = new(); - using NonSeekableStream nonSeekable = new(memoryStream); - - image.Save(nonSeekable, encoder); - - Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); - } - private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs new file mode 100644 index 0000000000..1803cfddb9 --- /dev/null +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -0,0 +1,373 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.IO; + +/// +/// Tests for the class. +/// +public class ChunkedMemoryStreamTests +{ + /// + /// The default length in bytes of each buffer chunk when allocating large buffers. + /// + private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb + + /// + /// The default length in bytes of each buffer chunk when allocating small buffers. + /// + private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb + + private readonly MemoryAllocator allocator; + + public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; + + [Fact] + public void MemoryStream_GetPositionTest_Negative() + { + using var ms = new ChunkedMemoryStream(this.allocator); + long iCurrentPos = ms.Position; + for (int i = -1; i > -6; i--) + { + Assert.Throws(() => ms.Position = i); + Assert.Equal(ms.Position, iCurrentPos); + } + } + + [Fact] + public void MemoryStream_ReadTest_Negative() + { + var ms2 = new ChunkedMemoryStream(this.allocator); + + Assert.Throws(() => ms2.Read(null, 0, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, -1)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 2, 0)); + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 2)); + + ms2.Dispose(); + + Assert.Throws(() => ms2.Read(new byte[] { 1 }, 0, 1)); + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], cms.ReadByte()); + } + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteBufferTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + byte[] buffer = new byte[2]; + for (int i = 0; i < expected.Length; i += 2) + { + cms.Read(buffer); + Assert.Equal(expected[i], buffer[0]); + Assert.Equal(expected[i + 1], buffer[1]); + } + } + + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + public void MemoryStream_ReadByteBufferSpanTest(int length) + { + using MemoryStream ms = this.CreateTestStream(length); + using var cms = new ChunkedMemoryStream(this.allocator); + + ms.CopyTo(cms); + cms.Position = 0; + byte[] expected = ms.ToArray(); + Span buffer = new byte[2]; + for (int i = 0; i < expected.Length; i += 2) + { + cms.Read(buffer); + Assert.Equal(expected[i], buffer[0]); + Assert.Equal(expected[i + 1], buffer[1]); + } + } + + [Fact] + public void MemoryStream_WriteToTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + // [] Write to memoryStream, check the memoryStream + ms2.Write(bytArr, 0, bytArr.Length); + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + + // [] Write to memoryStream, check the memoryStream + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (var ms3 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); + ms3.Position = 0; + bytArrRet = new byte[(int)ms3.Length]; + ms3.Read(bytArrRet, 0, (int)ms3.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteToSpanTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + Span bytArrRet; + Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + // [] Write to memoryStream, check the memoryStream + ms2.Write(bytArr, 0, bytArr.Length); + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + + // [] Write to memoryStream, check the memoryStream + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (var ms3 = new ChunkedMemoryStream(this.allocator)) + { + Span bytArrRet; + Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); + ms3.Position = 0; + bytArrRet = new byte[(int)ms3.Length]; + ms3.Read(bytArrRet, 0, (int)ms3.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteByteTests() + { + using (var ms2 = new ChunkedMemoryStream(this.allocator)) + { + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + + for (int i = 0; i < bytArr.Length; i++) + { + ms2.WriteByte(bytArr[i]); + } + + using var readonlyStream = new ChunkedMemoryStream(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); + } + } + } + + [Fact] + public void MemoryStream_WriteToTests_Negative() + { + using var ms2 = new ChunkedMemoryStream(this.allocator); + Assert.Throws(() => ms2.WriteTo(null)); + + ms2.Write(new byte[] { 1 }, 0, 1); + var readonlyStream = new MemoryStream(new byte[1028], false); + Assert.Throws(() => ms2.WriteTo(readonlyStream)); + + readonlyStream.Dispose(); + + // [] Pass in a closed stream + Assert.Throws(() => ms2.WriteTo(readonlyStream)); + } + + [Fact] + public void MemoryStream_CopyTo_Invalid() + { + ChunkedMemoryStream memoryStream; + const string bufferSize = nameof(bufferSize); + using (memoryStream = new ChunkedMemoryStream(this.allocator)) + { + const string destination = nameof(destination); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null)); + + // Validate the destination parameter first. + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); + + // Then bufferSize. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + } + + // After the Stream is disposed, we should fail on all CopyTos. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + + ChunkedMemoryStream disposedStream = memoryStream; + + // We should throw first for the source being disposed... + Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); + + // Then for the destination being disposed. + memoryStream = new ChunkedMemoryStream(this.allocator); + Assert.Throws(() => memoryStream.CopyTo(disposedStream, 1)); + memoryStream.Dispose(); + } + + [Theory] + [MemberData(nameof(CopyToData))] + public void CopyTo(Stream source, byte[] expected) + { + using var destination = new ChunkedMemoryStream(this.allocator); + source.CopyTo(destination); + Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. + Assert.Equal(expected, destination.ToArray()); + } + + public static IEnumerable GetAllTestImages() + { + IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) + .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); + + var result = new List(); + foreach (string path in allImageFiles) + { + result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); + } + + return result; + } + + public static IEnumerable AllTestImages = GetAllTestImages(); + + [Theory] + [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] + public void DecoderIntegrationTest(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + Image expected; + try + { + expected = provider.GetImage(); + } + catch + { + // The image is invalid + return; + } + + string fullPath = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + ((TestImageProvider.FileProvider)provider).FilePath); + + using FileStream fs = File.OpenRead(fullPath); + using var nonSeekableStream = new NonSeekableStream(fs); + + var actual = Image.Load(nonSeekableStream); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + + public static IEnumerable CopyToData() + { + // Stream is positioned @ beginning of data + byte[] data1 = new byte[] { 1, 2, 3 }; + var stream1 = new MemoryStream(data1); + + yield return new object[] { stream1, data1 }; + + // Stream is positioned in the middle of data + byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; + var stream2 = new MemoryStream(data2) { Position = 1 }; + + yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; + + // Stream is positioned after end of data + byte[] data3 = data2; + var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; + + yield return new object[] { stream3, Array.Empty() }; + } + + private MemoryStream CreateTestStream(int length) + { + byte[] buffer = new byte[length]; + var random = new Random(); + random.NextBytes(buffer); + + return new MemoryStream(buffer); + } +} diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 2941490e9a..4b1f6e1568 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ public NonSeekableStream(Stream dataStream) public override bool CanSeek => false; - public override bool CanWrite => this.dataStream.CanWrite; + public override bool CanWrite => false; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => this.dataStream.Write(buffer, offset, count); + => throw new NotImplementedException(); } From 48645f8b4772a4d3dc07bcc5d96105fe979f94e7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 12:18:16 +1000 Subject: [PATCH 11/73] Rewrite ChunkedMemoryStream --- src/ImageSharp/Formats/ImageEncoder.cs | 4 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 628 ++++++++---------- .../Formats/WebP/WebpEncoderTests.cs | 38 ++ .../IO/ChunkedMemoryStreamTests.cs | 93 +-- .../Image/NonSeekableStream.cs | 6 +- 5 files changed, 361 insertions(+), 408 deletions(-) diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index deb527f698..27a4f11cdd 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -49,7 +49,7 @@ private void EncodeWithSeekableStream(Image image, Stream stream else { using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); - this.Encode(image, stream, cancellationToken); + this.Encode(image, ms, cancellationToken); ms.Position = 0; ms.CopyTo(stream, configuration.StreamProcessingBufferSize); } @@ -65,7 +65,7 @@ private async Task EncodeWithSeekableStreamAsync(Image image, St } else { - using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); + await using ChunkedMemoryStream ms = new(configuration.MemoryAllocator); await DoEncodeAsync(ms); ms.Position = 0; await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 2534548141..f178764603 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Collections; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; @@ -12,44 +13,24 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -internal sealed class ChunkedMemoryStream : Stream +/// Provides an in-memory stream composed of non-contiguous chunks. +public class ChunkedMemoryStream : Stream { - // The memory allocator. - private readonly MemoryAllocator allocator; + private readonly MemoryChunkBuffer memoryChunkBuffer; + private readonly byte[] singleReadBuffer = new byte[1]; - // Data - private MemoryChunk? memoryChunk; - - // The total number of allocated chunks - private int chunkCount; - - // The length of the largest contiguous buffer that can be handled by the allocator. - private readonly int allocatorCapacity; - - // Has the stream been disposed. + private long length; + private long position; + private int currentChunk; + private int currentChunkIndex; private bool isDisposed; - // Current chunk to write to - private MemoryChunk? writeChunk; - - // Offset into chunk to write to - private int writeOffset; - - // Current chunk to read from - private MemoryChunk? readChunk; - - // Offset into chunk to read from - private int readOffset; - /// /// Initializes a new instance of the class. /// /// The memory allocator. public ChunkedMemoryStream(MemoryAllocator allocator) - { - this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); - this.allocator = allocator; - } + => this.memoryChunkBuffer = new(allocator); /// public override bool CanRead => !this.isDisposed; @@ -66,25 +47,7 @@ public override long Length get { this.EnsureNotDisposed(); - - int length = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - MemoryChunk? next = chunk.Next; - if (next != null) - { - length += chunk.Length; - } - else - { - length += this.writeOffset; - } - - chunk = next; - } - - return length; + return this.length; } } @@ -94,93 +57,35 @@ public override long Position get { this.EnsureNotDisposed(); - - if (this.readChunk is null) - { - return 0; - } - - int pos = 0; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != this.readChunk && chunk is not null) - { - pos += chunk.Length; - chunk = chunk.Next; - } - - pos += this.readOffset; - - return pos; + return this.position; } set { this.EnsureNotDisposed(); - - if (value < 0) - { - ThrowArgumentOutOfRange(nameof(value)); - } - - // Back up current position in case new position is out of range - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = null; - this.readOffset = 0; - - int leftUntilAtPos = (int)value; - MemoryChunk? chunk = this.memoryChunk; - while (chunk != null) - { - if ((leftUntilAtPos < chunk.Length) - || ((leftUntilAtPos == chunk.Length) - && (chunk.Next is null))) - { - // The desired position is in this chunk - this.readChunk = chunk; - this.readOffset = leftUntilAtPos; - break; - } - - leftUntilAtPos -= chunk.Length; - chunk = chunk.Next; - } - - if (this.readChunk is null) - { - // Position is out of range - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; - } + this.SetPosition(value); } } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Flush() + { + } + + /// public override long Seek(long offset, SeekOrigin origin) { this.EnsureNotDisposed(); - switch (origin) + this.Position = origin switch { - case SeekOrigin.Begin: - this.Position = offset; - break; - - case SeekOrigin.Current: - this.Position += offset; - break; - - case SeekOrigin.End: - this.Position = this.Length + offset; - break; - default: - ThrowInvalidSeek(); - break; - } + SeekOrigin.Begin => (int)offset, + SeekOrigin.Current => (int)(this.Position + offset), + SeekOrigin.End => (int)(this.Length + offset), + _ => throw new ArgumentOutOfRangeException(nameof(offset)), + }; - return this.Position; + return this.position; } /// @@ -188,41 +93,23 @@ public override void SetLength(long value) => throw new NotSupportedException(); /// - protected override void Dispose(bool disposing) + public override int ReadByte() { - if (this.isDisposed) - { - return; - } - - try - { - this.isDisposed = true; - if (disposing) - { - ReleaseMemoryChunks(this.memoryChunk); - } - - this.memoryChunk = null; - this.writeChunk = null; - this.readChunk = null; - this.chunkCount = 0; - } - finally + this.EnsureNotDisposed(); + if (this.position >= this.length) { - base.Dispose(disposing); + return -1; } - } - /// - public override void Flush() - { + _ = this.Read(this.singleReadBuffer, 0, 1); + return this.singleReadBuffer[^1]; } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int Read(byte[] buffer, int offset, int count) { + this.EnsureNotDisposed(); + Guard.NotNull(buffer, nameof(buffer)); Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); @@ -230,111 +117,63 @@ public override int Read(byte[] buffer, int offset, int count) const string bufferMessage = "Offset subtracted from the buffer length is less than count."; Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - return this.ReadImpl(buffer.AsSpan(offset, count)); + return this.Read(buffer.AsSpan(offset, count)); } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int Read(Span buffer) => this.ReadImpl(buffer); - - private int ReadImpl(Span buffer) + public override int Read(Span buffer) { this.EnsureNotDisposed(); - if (this.readChunk is null) + int offset = 0; + int count = buffer.Length; + int bytesRead = 0; + long bytesToRead = this.length - this.position; + if (bytesToRead > count) { - if (this.memoryChunk is null) - { - return 0; - } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; + bytesToRead = count; } - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) + if (bytesToRead <= 0) { - chunkSize = this.writeOffset; + // Already at the end of the stream, nothing to read + return 0; } - int bytesRead = 0; - int offset = 0; - int count = buffer.Length; - while (count > 0) + while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { - if (this.readOffset == chunkSize) + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToRead, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; } - int readCount = Math.Min(count, chunkSize - this.readOffset); - chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); - offset += readCount; - count -= readCount; - this.readOffset += readCount; - bytesRead += readCount; - } - - return bytesRead; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int ReadByte() - { - this.EnsureNotDisposed(); + // Read n bytes from the current chunk + chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n).CopyTo(buffer.Slice(offset, n)); + bytesToRead -= n; + offset += n; + bytesRead += n; - if (this.readChunk is null) - { - if (this.memoryChunk is null) + if (moveToNextChunk) { - return 0; + this.currentChunkIndex = 0; + this.currentChunk++; } - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - } - - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } - - if (this.readOffset == chunkSize) - { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) + else { - return -1; + this.currentChunkIndex += n; } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; } - return chunkBuffer.GetSpan()[this.readOffset++]; + this.position += bytesRead; + return bytesRead; } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Write(byte[] buffer, int offset, int count) { Guard.NotNull(buffer, nameof(buffer)); @@ -344,157 +183,200 @@ public override void Write(byte[] buffer, int offset, int count) const string bufferMessage = "Offset subtracted from the buffer length is less than count."; Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); - this.WriteImpl(buffer.AsSpan(offset, count)); + this.Write(buffer.AsSpan(offset, count)); } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Write(ReadOnlySpan buffer) => this.WriteImpl(buffer); - - private void WriteImpl(ReadOnlySpan buffer) + public override void Write(ReadOnlySpan buffer) { this.EnsureNotDisposed(); - if (this.memoryChunk is null) + int offset = 0; + int count = buffer.Length; + int bytesWritten = 0; + long bytesToWrite = this.memoryChunkBuffer.Length - this.position; + + // Ensure we have enough capacity to write the data. + while (bytesToWrite < count) { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; + this.memoryChunkBuffer.Expand(); + bytesToWrite = this.memoryChunkBuffer.Length - this.position; } - Guard.NotNull(this.writeChunk); + if (bytesToWrite > count) + { + bytesToWrite = count; + } - Span chunkBuffer = this.writeChunk.Buffer.GetSpan(); - int chunkSize = this.writeChunk.Length; - int count = buffer.Length; - int offset = 0; - while (count > 0) + while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { - if (this.writeOffset == chunkSize) + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToWrite, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer.GetSpan(); - chunkSize = this.writeChunk.Length; + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; } - int copyCount = Math.Min(count, chunkSize - this.writeOffset); - buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); + // Write n bytes to the current chunk + buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.currentChunkIndex, n)); + bytesToWrite -= n; + offset += n; + bytesWritten += n; - offset += copyCount; - count -= copyCount; - this.writeOffset += copyCount; + if (moveToNextChunk) + { + this.currentChunkIndex = 0; + this.currentChunk++; + } + else + { + this.currentChunkIndex += n; + } } + + this.position += bytesWritten; + this.length += bytesWritten; } - /// - public override void WriteByte(byte value) + /// + /// Writes the entire contents of this memory stream to another stream. + /// + /// The stream to write this memory stream to. + /// is . + /// The current or target stream is closed. + public void WriteTo(Stream stream) { + Guard.NotNull(stream, nameof(stream)); this.EnsureNotDisposed(); - if (this.memoryChunk is null) + this.Position = 0; + + int bytesRead = 0; + long bytesToRead = this.length - this.position; + if (bytesToRead <= 0) { - this.memoryChunk = this.AllocateMemoryChunk(); - this.writeChunk = this.memoryChunk; - this.writeOffset = 0; + // Already at the end of the stream, nothing to read + return; } - Guard.NotNull(this.writeChunk); + while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + { + bool moveToNextChunk = false; + MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + int n = (int)Math.Min(bytesToRead, int.MaxValue); + int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + if (n >= remainingBytesInCurrentChunk) + { + n = remainingBytesInCurrentChunk; + moveToNextChunk = true; + } - IMemoryOwner chunkBuffer = this.writeChunk.Buffer; - int chunkSize = this.writeChunk.Length; + // Read n bytes from the current chunk + stream.Write(chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n)); + bytesToRead -= n; + bytesRead += n; - if (this.writeOffset == chunkSize) - { - // Allocate a new chunk if the current one is full - this.writeChunk.Next = this.AllocateMemoryChunk(); - this.writeChunk = this.writeChunk.Next; - this.writeOffset = 0; - chunkBuffer = this.writeChunk.Buffer; + if (moveToNextChunk) + { + this.currentChunkIndex = 0; + this.currentChunk++; + } + else + { + this.currentChunkIndex += n; + } } - chunkBuffer.GetSpan()[this.writeOffset++] = value; + this.position += bytesRead; } /// - /// Copy entire buffer into an array. + /// Writes the stream contents to a byte array, regardless of the property. /// - /// The . + /// A new . public byte[] ToArray() { - int length = (int)this.Length; // This will throw if stream is closed - byte[] copy = new byte[this.Length]; - - MemoryChunk? backupReadChunk = this.readChunk; - int backupReadOffset = this.readOffset; - - this.readChunk = this.memoryChunk; - this.readOffset = 0; - this.Read(copy, 0, length); - - this.readChunk = backupReadChunk; - this.readOffset = backupReadOffset; + this.EnsureNotDisposed(); + long position = this.position; + byte[] copy = new byte[this.length]; + this.Position = 0; + this.Read(copy, 0, copy.Length); + this.Position = position; return copy; } - /// - /// Write remainder of this stream to another stream. - /// - /// The stream to write to. - public void WriteTo(Stream stream) + /// + protected override void Dispose(bool disposing) { - this.EnsureNotDisposed(); - - Guard.NotNull(stream, nameof(stream)); + if (this.isDisposed) + { + return; + } - if (this.readChunk is null) + try { - if (this.memoryChunk is null) + this.isDisposed = true; + if (disposing) { - return; + this.memoryChunkBuffer.Dispose(); } - this.readChunk = this.memoryChunk; - this.readOffset = 0; + this.currentChunk = 0; + this.currentChunkIndex = 0; + this.position = 0; + this.length = 0; + } + finally + { + base.Dispose(disposing); + } + } + + private void SetPosition(long value) + { + long newPosition = value; + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); } - IMemoryOwner chunkBuffer = this.readChunk.Buffer; - int chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) + this.position = newPosition; + + // Find the current chunk & current chunk index + int currentChunkIndex = 0; + long offset = newPosition; + + // If the new position is greater than the length of the stream, set the position to the end of the stream + if (offset > 0 && offset >= this.memoryChunkBuffer.Length) { - chunkSize = this.writeOffset; + this.currentChunk = this.memoryChunkBuffer.ChunkCount - 1; + this.currentChunkIndex = this.memoryChunkBuffer[this.currentChunk].Length - 1; + return; } - // Following code mirrors Read() logic (readChunk/readOffset should - // point just past last byte of last chunk when done) - // loop until end of chunks is found - while (true) + // Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk + // from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk. + while (offset != 0) { - if (this.readOffset == chunkSize) + int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length; + if (offset < chunkLength) { - // Exit if no more chunks are currently available - if (this.readChunk.Next is null) - { - break; - } - - this.readChunk = this.readChunk.Next; - this.readOffset = 0; - chunkBuffer = this.readChunk.Buffer; - chunkSize = this.readChunk.Length; - if (this.readChunk.Next is null) - { - chunkSize = this.writeOffset; - } + // Found the correct chunk and the corresponding index + break; } - int writeCount = chunkSize - this.readOffset; - stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); - this.readOffset = chunkSize; + offset -= chunkLength; + currentChunkIndex++; } + + this.currentChunk = currentChunkIndex; + + // Safe to cast here as we know the offset is less than the chunk length. + this.currentChunkIndex = (int)offset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -507,48 +389,82 @@ private void EnsureNotDisposed() } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed."); - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); + private sealed class MemoryChunkBuffer : IEnumerable, IDisposable + { + private readonly List memoryChunks = new(); + private readonly MemoryAllocator allocator; + private readonly int allocatorCapacity; + private bool isDisposed; - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); + public MemoryChunkBuffer(MemoryAllocator allocator) + { + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); + this.allocator = allocator; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private MemoryChunk AllocateMemoryChunk() - { - // Tweak our buffer sizes to take the minimum of the provided buffer sizes - // or the allocator buffer capacity which provides us with the largest - // available contiguous buffer size. - IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); + public int ChunkCount => this.memoryChunks.Count; + + public long Length { get; private set; } - return new MemoryChunk(buffer) + public MemoryChunk this[int index] => this.memoryChunks[index]; + + public void Expand() { - Next = null, - Length = buffer.Length() - }; - } + IMemoryOwner buffer = + this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount))); - private static void ReleaseMemoryChunks(MemoryChunk? chunk) - { - while (chunk != null) + MemoryChunk chunk = new(buffer) + { + Length = buffer.Length() + }; + + this.memoryChunks.Add(chunk); + this.Length += chunk.Length; + } + + public void Dispose() { - chunk.Dispose(); - chunk = chunk.Next; + this.Dispose(true); + GC.SuppressFinalize(this); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetChunkSize(int i) - { - // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. - // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 -#pragma warning disable IDE1006 // Naming Styles - const int _128K = 1 << 17; - const int _4M = 1 << 22; - return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; -#pragma warning restore IDE1006 // Naming Styles + public IEnumerator GetEnumerator() + => ((IEnumerable)this.memoryChunks).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this.memoryChunks).GetEnumerator(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the + // same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 + const int b128K = 1 << 17; + const int b4M = 1 << 22; + return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M; + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + foreach (MemoryChunk chunk in this.memoryChunks) + { + chunk.Dispose(); + } + + this.memoryChunks.Clear(); + } + + this.Length = 0; + this.isDisposed = true; + } + } } private sealed class MemoryChunk : IDisposable @@ -559,8 +475,6 @@ private sealed class MemoryChunk : IDisposable public IMemoryOwner Buffer { get; } - public MemoryChunk? Next { get; set; } - public int Length { get; init; } private void Dispose(bool disposing) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 031a9ba059..c6751e2a66 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -544,6 +544,44 @@ public static void RunEncodeLossy_WithPeakImage() [Fact] public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic); + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public void CanSave_NonSeekableStream(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + using MemoryStream memoryStream = new(); + using NonSeekableStream nonSeekable = new(memoryStream); + + image.Save(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + public async Task CanSave_NonSeekableStream_Async(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + WebpEncoder encoder = new(); + + await using MemoryStream seekable = new(); + image.Save(seekable, encoder); + + await using MemoryStream memoryStream = new(); + await using NonSeekableStream nonSeekable = new(memoryStream); + + await image.SaveAsync(nonSeekable, encoder); + + Assert.True(seekable.ToArray().SequenceEqual(memoryStream.ToArray())); + } + private static ImageComparer GetComparer(int quality) { float tolerance = 0.01f; // ~1.0% diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 1803cfddb9..8d7ea9a33e 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -30,7 +30,7 @@ public class ChunkedMemoryStreamTests [Fact] public void MemoryStream_GetPositionTest_Negative() { - using var ms = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream ms = new(this.allocator); long iCurrentPos = ms.Position; for (int i = -1; i > -6; i--) { @@ -42,7 +42,7 @@ public void MemoryStream_GetPositionTest_Negative() [Fact] public void MemoryStream_ReadTest_Negative() { - var ms2 = new ChunkedMemoryStream(this.allocator); + ChunkedMemoryStream ms2 = new(this.allocator); Assert.Throws(() => ms2.Read(null, 0, 0)); Assert.Throws(() => ms2.Read(new byte[] { 1 }, -1, 0)); @@ -63,8 +63,8 @@ public void MemoryStream_ReadTest_Negative() [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -84,8 +84,8 @@ public void MemoryStream_ReadByteTest(int length) [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -107,8 +107,8 @@ public void MemoryStream_ReadByteBufferTest(int length) [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferSpanTest(int length) { - using MemoryStream ms = this.CreateTestStream(length); - using var cms = new ChunkedMemoryStream(this.allocator); + using MemoryStream ms = CreateTestStream(length); + using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); cms.Position = 0; @@ -125,7 +125,7 @@ public void MemoryStream_ReadByteBufferSpanTest(int length) [Fact] public void MemoryStream_WriteToTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) { byte[] bytArrRet; byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -133,7 +133,7 @@ public void MemoryStream_WriteToTests() // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); - using var readonlyStream = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream readonlyStream = new(this.allocator); ms2.WriteTo(readonlyStream); readonlyStream.Flush(); readonlyStream.Position = 0; @@ -146,8 +146,8 @@ public void MemoryStream_WriteToTests() } // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) + using (ChunkedMemoryStream ms3 = new(this.allocator)) { byte[] bytArrRet; byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -167,7 +167,7 @@ public void MemoryStream_WriteToTests() [Fact] public void MemoryStream_WriteToSpanTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) { Span bytArrRet; Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; @@ -175,10 +175,12 @@ public void MemoryStream_WriteToSpanTests() // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); - using var readonlyStream = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream readonlyStream = new(this.allocator); ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); for (int i = 0; i < bytArr.Length; i++) @@ -188,13 +190,14 @@ public void MemoryStream_WriteToSpanTests() } // [] Write to memoryStream, check the memoryStream - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - using (var ms3 = new ChunkedMemoryStream(this.allocator)) + using (ChunkedMemoryStream ms2 = new(this.allocator)) + using (ChunkedMemoryStream ms3 = new(this.allocator)) { Span bytArrRet; Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; ms2.Write(bytArr, 0, bytArr.Length); + ms2.WriteTo(ms3); ms3.Position = 0; bytArrRet = new byte[(int)ms3.Length]; @@ -209,37 +212,35 @@ public void MemoryStream_WriteToSpanTests() [Fact] public void MemoryStream_WriteByteTests() { - using (var ms2 = new ChunkedMemoryStream(this.allocator)) - { - byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + using ChunkedMemoryStream ms2 = new(this.allocator); + byte[] bytArrRet; + byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; - for (int i = 0; i < bytArr.Length; i++) - { - ms2.WriteByte(bytArr[i]); - } + for (int i = 0; i < bytArr.Length; i++) + { + ms2.WriteByte(bytArr[i]); + } - using var readonlyStream = new ChunkedMemoryStream(this.allocator); - ms2.WriteTo(readonlyStream); - readonlyStream.Flush(); - readonlyStream.Position = 0; - bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); - for (int i = 0; i < bytArr.Length; i++) - { - Assert.Equal(bytArr[i], bytArrRet[i]); - } + using ChunkedMemoryStream readonlyStream = new(this.allocator); + ms2.WriteTo(readonlyStream); + readonlyStream.Flush(); + readonlyStream.Position = 0; + bytArrRet = new byte[(int)readonlyStream.Length]; + readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + for (int i = 0; i < bytArr.Length; i++) + { + Assert.Equal(bytArr[i], bytArrRet[i]); } } [Fact] public void MemoryStream_WriteToTests_Negative() { - using var ms2 = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream ms2 = new(this.allocator); Assert.Throws(() => ms2.WriteTo(null)); ms2.Write(new byte[] { 1 }, 0, 1); - var readonlyStream = new MemoryStream(new byte[1028], false); + MemoryStream readonlyStream = new(new byte[1028], false); Assert.Throws(() => ms2.WriteTo(readonlyStream)); readonlyStream.Dispose(); @@ -286,7 +287,7 @@ public void MemoryStream_CopyTo_Invalid() [MemberData(nameof(CopyToData))] public void CopyTo(Stream source, byte[] expected) { - using var destination = new ChunkedMemoryStream(this.allocator); + using ChunkedMemoryStream destination = new(this.allocator); source.CopyTo(destination); Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end. Assert.Equal(expected, destination.ToArray()); @@ -297,10 +298,10 @@ public static IEnumerable GetAllTestImages() IEnumerable allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) .Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); - var result = new List(); + List result = new(); foreach (string path in allImageFiles) { - result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); + result.Add(path[TestEnvironment.InputImagesDirectoryFullPath.Length..]); } return result; @@ -334,9 +335,9 @@ public void DecoderIntegrationTest(TestImageProvider provider) ((TestImageProvider.FileProvider)provider).FilePath); using FileStream fs = File.OpenRead(fullPath); - using var nonSeekableStream = new NonSeekableStream(fs); + using NonSeekableStream nonSeekableStream = new(fs); - var actual = Image.Load(nonSeekableStream); + Image actual = Image.Load(nonSeekableStream); ImageComparer.Exact.VerifySimilarity(expected, actual); } @@ -345,27 +346,27 @@ public static IEnumerable CopyToData() { // Stream is positioned @ beginning of data byte[] data1 = new byte[] { 1, 2, 3 }; - var stream1 = new MemoryStream(data1); + MemoryStream stream1 = new(data1); yield return new object[] { stream1, data1 }; // Stream is positioned in the middle of data byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; - var stream2 = new MemoryStream(data2) { Position = 1 }; + MemoryStream stream2 = new(data2) { Position = 1 }; yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; // Stream is positioned after end of data byte[] data3 = data2; - var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; + MemoryStream stream3 = new(data3) { Position = data3.Length + 1 }; yield return new object[] { stream3, Array.Empty() }; } - private MemoryStream CreateTestStream(int length) + private static MemoryStream CreateTestStream(int length) { byte[] buffer = new byte[length]; - var random = new Random(); + Random random = new(); random.NextBytes(buffer); return new MemoryStream(buffer); diff --git a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs index 4b1f6e1568..2941490e9a 100644 --- a/tests/ImageSharp.Tests/Image/NonSeekableStream.cs +++ b/tests/ImageSharp.Tests/Image/NonSeekableStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Tests; @@ -14,7 +14,7 @@ public NonSeekableStream(Stream dataStream) public override bool CanSeek => false; - public override bool CanWrite => false; + public override bool CanWrite => this.dataStream.CanWrite; public override bool CanTimeout => this.dataStream.CanTimeout; @@ -91,5 +91,5 @@ public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) - => throw new NotImplementedException(); + => this.dataStream.Write(buffer, offset, count); } From f96f8ca285693e70fe095d953bc8f930bb7a81ba Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 12:23:34 +1000 Subject: [PATCH 12/73] Revert "Update BufferedStreams.cs" This reverts commit 1a150780fd5dc79194e02568503a12b36cbc42cb. --- .../General/IO/BufferedStreams.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs index a7b22e7ab8..2a926d1cd8 100644 --- a/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/General/IO/BufferedStreams.cs @@ -19,8 +19,12 @@ public class BufferedStreams private MemoryStream stream4; private MemoryStream stream5; private MemoryStream stream6; + private ChunkedMemoryStream chunkedMemoryStream1; + private ChunkedMemoryStream chunkedMemoryStream2; private BufferedReadStream bufferedStream1; private BufferedReadStream bufferedStream2; + private BufferedReadStream bufferedStream3; + private BufferedReadStream bufferedStream4; private BufferedReadStreamWrapper bufferedStreamWrap1; private BufferedReadStreamWrapper bufferedStreamWrap2; @@ -35,8 +39,18 @@ public void CreateStreams() this.stream6 = new MemoryStream(this.buffer); this.stream6 = new MemoryStream(this.buffer); + this.chunkedMemoryStream1 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); + this.chunkedMemoryStream1.Write(this.buffer); + this.chunkedMemoryStream1.Position = 0; + + this.chunkedMemoryStream2 = new ChunkedMemoryStream(Configuration.Default.MemoryAllocator); + this.chunkedMemoryStream2.Write(this.buffer); + this.chunkedMemoryStream2.Position = 0; + this.bufferedStream1 = new BufferedReadStream(Configuration.Default, this.stream3); this.bufferedStream2 = new BufferedReadStream(Configuration.Default, this.stream4); + this.bufferedStream3 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream1); + this.bufferedStream4 = new BufferedReadStream(Configuration.Default, this.chunkedMemoryStream2); this.bufferedStreamWrap1 = new BufferedReadStreamWrapper(this.stream5); this.bufferedStreamWrap2 = new BufferedReadStreamWrapper(this.stream6); } @@ -46,8 +60,12 @@ public void DestroyStreams() { this.bufferedStream1?.Dispose(); this.bufferedStream2?.Dispose(); + this.bufferedStream3?.Dispose(); + this.bufferedStream4?.Dispose(); this.bufferedStreamWrap1?.Dispose(); this.bufferedStreamWrap2?.Dispose(); + this.chunkedMemoryStream1?.Dispose(); + this.chunkedMemoryStream2?.Dispose(); this.stream1?.Dispose(); this.stream2?.Dispose(); this.stream3?.Dispose(); @@ -86,6 +104,21 @@ public int BufferedReadStreamRead() return r; } + [Benchmark] + public int BufferedReadStreamChunkedRead() + { + int r = 0; + BufferedReadStream reader = this.bufferedStream3; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; + } + [Benchmark] public int BufferedReadStreamWrapRead() { @@ -129,6 +162,20 @@ public int BufferedReadStreamReadByte() return r; } + [Benchmark] + public int BufferedReadStreamChunkedReadByte() + { + int r = 0; + BufferedReadStream reader = this.bufferedStream4; + + for (int i = 0; i < reader.Length; i++) + { + r += reader.ReadByte(); + } + + return r; + } + [Benchmark] public int BufferedReadStreamWrapReadByte() { From 03343c4abee4d33c128f70050505613022e754df Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 23 Oct 2024 14:18:26 +1000 Subject: [PATCH 13/73] Simplify and optimize position checking --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 39 +++++++++++++----------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index f178764603..06074b25bc 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -127,24 +127,26 @@ public override int Read(Span buffer) int offset = 0; int count = buffer.Length; - int bytesRead = 0; - long bytesToRead = this.length - this.position; - if (bytesToRead > count) + + long remaining = this.length - this.position; + if (remaining > count) { - bytesToRead = count; + remaining = count; } - if (bytesToRead <= 0) + if (remaining <= 0) { // Already at the end of the stream, nothing to read return 0; } + int bytesToRead = (int)remaining; + int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToRead, int.MaxValue); + int n = bytesToRead; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { @@ -193,26 +195,28 @@ public override void Write(ReadOnlySpan buffer) int offset = 0; int count = buffer.Length; - int bytesWritten = 0; - long bytesToWrite = this.memoryChunkBuffer.Length - this.position; + + long remaining = this.memoryChunkBuffer.Length - this.position; // Ensure we have enough capacity to write the data. - while (bytesToWrite < count) + while (remaining < count) { this.memoryChunkBuffer.Expand(); - bytesToWrite = this.memoryChunkBuffer.Length - this.position; + remaining = this.memoryChunkBuffer.Length - this.position; } - if (bytesToWrite > count) + if (remaining > count) { - bytesToWrite = count; + remaining = count; } + int bytesToWrite = (int)remaining; + int bytesWritten = 0; while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToWrite, int.MaxValue); + int n = bytesToWrite; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { @@ -254,19 +258,20 @@ public void WriteTo(Stream stream) this.Position = 0; - int bytesRead = 0; - long bytesToRead = this.length - this.position; - if (bytesToRead <= 0) + long remaining = this.length - this.position; + if (remaining <= 0) { // Already at the end of the stream, nothing to read return; } + int bytesToRead = (int)remaining; + int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; - int n = (int)Math.Min(bytesToRead, int.MaxValue); + int n = bytesToRead; int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; if (n >= remainingBytesInCurrentChunk) { From b74d2e425774333e4be9b1e776f81b10fb003eee Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 30 Oct 2024 09:34:07 +1000 Subject: [PATCH 14/73] Add WriteByte and optimize ReadByte --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 06074b25bc..59c42ec387 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.IO; @@ -13,11 +14,10 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -/// Provides an in-memory stream composed of non-contiguous chunks. public class ChunkedMemoryStream : Stream { private readonly MemoryChunkBuffer memoryChunkBuffer; - private readonly byte[] singleReadBuffer = new byte[1]; + private readonly byte[] singleByteBuffer = new byte[1]; private long length; private long position; @@ -101,8 +101,8 @@ public override int ReadByte() return -1; } - _ = this.Read(this.singleReadBuffer, 0, 1); - return this.singleReadBuffer[^1]; + _ = this.Read(this.singleByteBuffer, 0, 1); + return MemoryMarshal.GetReference(this.singleByteBuffer); } /// @@ -129,17 +129,17 @@ public override int Read(Span buffer) int count = buffer.Length; long remaining = this.length - this.position; - if (remaining > count) - { - remaining = count; - } - if (remaining <= 0) { // Already at the end of the stream, nothing to read return 0; } + if (remaining > count) + { + remaining = count; + } + int bytesToRead = (int)remaining; int bytesRead = 0; while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) @@ -175,6 +175,14 @@ public override int Read(Span buffer) return bytesRead; } + /// + public override void WriteByte(byte value) + { + this.EnsureNotDisposed(); + MemoryMarshal.Write(this.singleByteBuffer, ref value); + this.Write(this.singleByteBuffer, 0, 1); + } + /// public override void Write(byte[] buffer, int offset, int count) { @@ -309,7 +317,7 @@ public byte[] ToArray() byte[] copy = new byte[this.length]; this.Position = 0; - this.Read(copy, 0, copy.Length); + _ = this.Read(copy, 0, copy.Length); this.Position = position; return copy; } From 630166211c7c3d1b6b56f250bf198cc967d3e42b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Nov 2024 10:12:11 +1000 Subject: [PATCH 15/73] Feedback updates and massively expand write tests --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 148 ++++++------------ .../IO/ChunkedMemoryStreamTests.cs | 88 ++++++++--- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 59c42ec387..53de2c3cb5 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -14,15 +13,13 @@ namespace SixLabors.ImageSharp.IO; /// Chunks are allocated by the assigned via the constructor /// and is designed to take advantage of buffer pooling when available. /// -public class ChunkedMemoryStream : Stream +internal sealed class ChunkedMemoryStream : Stream { private readonly MemoryChunkBuffer memoryChunkBuffer; - private readonly byte[] singleByteBuffer = new byte[1]; - private long length; private long position; - private int currentChunk; - private int currentChunkIndex; + private int bufferIndex; + private int chunkIndex; private bool isDisposed; /// @@ -95,21 +92,13 @@ public override void SetLength(long value) /// public override int ReadByte() { - this.EnsureNotDisposed(); - if (this.position >= this.length) - { - return -1; - } - - _ = this.Read(this.singleByteBuffer, 0, 1); - return MemoryMarshal.GetReference(this.singleByteBuffer); + Unsafe.SkipInit(out byte b); + return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1; } /// public override int Read(byte[] buffer, int offset, int count) { - this.EnsureNotDisposed(); - Guard.NotNull(buffer, nameof(buffer)); Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); @@ -135,19 +124,14 @@ public override int Read(Span buffer) return 0; } - if (remaining > count) - { - remaining = count; - } - - int bytesToRead = (int)remaining; + int bytesToRead = count; int bytesRead = 0; - while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToRead; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -155,19 +139,19 @@ public override int Read(Span buffer) } // Read n bytes from the current chunk - chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n).CopyTo(buffer.Slice(offset, n)); + chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n)); bytesToRead -= n; offset += n; bytesRead += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -177,11 +161,7 @@ public override int Read(Span buffer) /// public override void WriteByte(byte value) - { - this.EnsureNotDisposed(); - MemoryMarshal.Write(this.singleByteBuffer, ref value); - this.Write(this.singleByteBuffer, 0, 1); - } + => this.Write(MemoryMarshal.CreateSpan(ref value, 1)); /// public override void Write(byte[] buffer, int offset, int count) @@ -213,19 +193,14 @@ public override void Write(ReadOnlySpan buffer) remaining = this.memoryChunkBuffer.Length - this.position; } - if (remaining > count) - { - remaining = count; - } - - int bytesToWrite = (int)remaining; + int bytesToWrite = count; int bytesWritten = 0; - while (bytesToWrite != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToWrite; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -233,19 +208,19 @@ public override void Write(ReadOnlySpan buffer) } // Write n bytes to the current chunk - buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.currentChunkIndex, n)); + buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n)); bytesToWrite -= n; offset += n; bytesWritten += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -275,12 +250,12 @@ public void WriteTo(Stream stream) int bytesToRead = (int)remaining; int bytesRead = 0; - while (bytesToRead != 0 && this.currentChunk != this.memoryChunkBuffer.Length) + while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { bool moveToNextChunk = false; - MemoryChunk chunk = this.memoryChunkBuffer[this.currentChunk]; + MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex]; int n = bytesToRead; - int remainingBytesInCurrentChunk = chunk.Length - this.currentChunkIndex; + int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex; if (n >= remainingBytesInCurrentChunk) { n = remainingBytesInCurrentChunk; @@ -288,18 +263,18 @@ public void WriteTo(Stream stream) } // Read n bytes from the current chunk - stream.Write(chunk.Buffer.Memory.Span.Slice(this.currentChunkIndex, n)); + stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n)); bytesToRead -= n; bytesRead += n; if (moveToNextChunk) { - this.currentChunkIndex = 0; - this.currentChunk++; + this.chunkIndex = 0; + this.bufferIndex++; } else { - this.currentChunkIndex += n; + this.chunkIndex += n; } } @@ -338,8 +313,8 @@ protected override void Dispose(bool disposing) this.memoryChunkBuffer.Dispose(); } - this.currentChunk = 0; - this.currentChunkIndex = 0; + this.bufferIndex = 0; + this.chunkIndex = 0; this.position = 0; this.length = 0; } @@ -366,8 +341,8 @@ private void SetPosition(long value) // If the new position is greater than the length of the stream, set the position to the end of the stream if (offset > 0 && offset >= this.memoryChunkBuffer.Length) { - this.currentChunk = this.memoryChunkBuffer.ChunkCount - 1; - this.currentChunkIndex = this.memoryChunkBuffer[this.currentChunk].Length - 1; + this.bufferIndex = this.memoryChunkBuffer.ChunkCount - 1; + this.chunkIndex = this.memoryChunkBuffer[this.bufferIndex].Length - 1; return; } @@ -386,10 +361,10 @@ private void SetPosition(long value) currentChunkIndex++; } - this.currentChunk = currentChunkIndex; + this.bufferIndex = currentChunkIndex; // Safe to cast here as we know the offset is less than the chunk length. - this.currentChunkIndex = (int)offset; + this.chunkIndex = (int)offset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -404,7 +379,7 @@ private void EnsureNotDisposed() [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed."); - private sealed class MemoryChunkBuffer : IEnumerable, IDisposable + private sealed class MemoryChunkBuffer : IDisposable { private readonly List memoryChunks = new(); private readonly MemoryAllocator allocator; @@ -439,15 +414,19 @@ public void Expand() public void Dispose() { - this.Dispose(true); - GC.SuppressFinalize(this); - } + if (!this.isDisposed) + { + foreach (MemoryChunk chunk in this.memoryChunks) + { + chunk.Dispose(); + } - public IEnumerator GetEnumerator() - => ((IEnumerable)this.memoryChunks).GetEnumerator(); + this.memoryChunks.Clear(); - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)this.memoryChunks).GetEnumerator(); + this.Length = 0; + this.isDisposed = true; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetChunkSize(int i) @@ -459,25 +438,6 @@ private static int GetChunkSize(int i) const int b4M = 1 << 22; return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M; } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - foreach (MemoryChunk chunk in this.memoryChunks) - { - chunk.Dispose(); - } - - this.memoryChunks.Clear(); - } - - this.Length = 0; - this.isDisposed = true; - } - } } private sealed class MemoryChunk : IDisposable @@ -490,23 +450,13 @@ private sealed class MemoryChunk : IDisposable public int Length { get; init; } - private void Dispose(bool disposing) + public void Dispose() { if (!this.isDisposed) { - if (disposing) - { - this.Buffer.Dispose(); - } - + this.Buffer.Dispose(); this.isDisposed = true; } } - - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 8d7ea9a33e..b1bb7a9f5a 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Tests.IO; /// public class ChunkedMemoryStreamTests { + private readonly Random bufferFiller = new(); + /// /// The default length in bytes of each buffer chunk when allocating large buffers. /// @@ -63,7 +65,7 @@ public void MemoryStream_ReadTest_Negative() [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -84,7 +86,7 @@ public void MemoryStream_ReadByteTest(int length) [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -105,9 +107,10 @@ public void MemoryStream_ReadByteBufferTest(int length) [InlineData(DefaultSmallChunkSize * 4)] [InlineData((int)(DefaultSmallChunkSize * 5.5))] [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] public void MemoryStream_ReadByteBufferSpanTest(int length) { - using MemoryStream ms = CreateTestStream(length); + using MemoryStream ms = this.CreateTestStream(length); using ChunkedMemoryStream cms = new(this.allocator); ms.CopyTo(cms); @@ -122,13 +125,19 @@ public void MemoryStream_ReadByteBufferSpanTest(int length) } } - [Fact] - public void MemoryStream_WriteToTests() + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] + public void MemoryStream_WriteToTests(int length) { using (ChunkedMemoryStream ms2 = new(this.allocator)) { byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + byte[] bytArr = this.CreateTestBuffer(length); // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); @@ -150,7 +159,7 @@ public void MemoryStream_WriteToTests() using (ChunkedMemoryStream ms3 = new(this.allocator)) { byte[] bytArrRet; - byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + byte[] bytArr = this.CreateTestBuffer(length); ms2.Write(bytArr, 0, bytArr.Length); ms2.WriteTo(ms3); @@ -164,13 +173,19 @@ public void MemoryStream_WriteToTests() } } - [Fact] - public void MemoryStream_WriteToSpanTests() + [Theory] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] + [InlineData(DefaultSmallChunkSize * 32)] + public void MemoryStream_WriteToSpanTests(int length) { using (ChunkedMemoryStream ms2 = new(this.allocator)) { Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + Span bytArr = this.CreateTestBuffer(length); // [] Write to memoryStream, check the memoryStream ms2.Write(bytArr, 0, bytArr.Length); @@ -194,7 +209,7 @@ public void MemoryStream_WriteToSpanTests() using (ChunkedMemoryStream ms3 = new(this.allocator)) { Span bytArrRet; - Span bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; + Span bytArr = this.CreateTestBuffer(length); ms2.Write(bytArr, 0, bytArr.Length); @@ -307,7 +322,7 @@ public static IEnumerable GetAllTestImages() return result; } - public static IEnumerable AllTestImages = GetAllTestImages(); + public static IEnumerable AllTestImages { get; } = GetAllTestImages(); [Theory] [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] @@ -337,9 +352,45 @@ public void DecoderIntegrationTest(TestImageProvider provider) using FileStream fs = File.OpenRead(fullPath); using NonSeekableStream nonSeekableStream = new(fs); - Image actual = Image.Load(nonSeekableStream); + using Image actual = Image.Load(nonSeekableStream); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + expected.Dispose(); + } + + [Theory] + [WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] + public void EncoderIntegrationTest(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + Image expected; + try + { + expected = provider.GetImage(); + } + catch + { + // The image is invalid + return; + } + + string fullPath = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + ((TestImageProvider.FileProvider)provider).FilePath); + + using MemoryStream ms = new(); + using NonSeekableStream nonSeekableStream = new(ms); + expected.SaveAsWebp(nonSeekableStream); + + using Image actual = Image.Load(nonSeekableStream); ImageComparer.Exact.VerifySimilarity(expected, actual); + expected.Dispose(); } public static IEnumerable CopyToData() @@ -363,12 +414,13 @@ public static IEnumerable CopyToData() yield return new object[] { stream3, Array.Empty() }; } - private static MemoryStream CreateTestStream(int length) + private byte[] CreateTestBuffer(int length) { byte[] buffer = new byte[length]; - Random random = new(); - random.NextBytes(buffer); - - return new MemoryStream(buffer); + this.bufferFiller.NextBytes(buffer); + return buffer; } + + private MemoryStream CreateTestStream(int length) + => new(this.CreateTestBuffer(length)); } From c45702df5b2c0464098bb3f7def7bf396bd537fd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Nov 2024 13:14:16 +1000 Subject: [PATCH 16/73] Fix read bug. --- src/ImageSharp/IO/ChunkedMemoryStream.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index 53de2c3cb5..760d1d3345 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -124,7 +124,13 @@ public override int Read(Span buffer) return 0; } - int bytesToRead = count; + if (remaining > count) + { + remaining = count; + } + + // 'remaining' can be less than the provided buffer length. + int bytesToRead = (int)remaining; int bytesRead = 0; while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length) { @@ -422,7 +428,6 @@ public void Dispose() } this.memoryChunks.Clear(); - this.Length = 0; this.isDisposed = true; } From c4fd666018266f1db59efc584dd4d0e195ed1736 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 13 Nov 2024 09:54:19 +1000 Subject: [PATCH 17/73] Update tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs Co-authored-by: Anton Firszov --- tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index b1bb7a9f5a..390170cfef 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.IO; /// public class ChunkedMemoryStreamTests { - private readonly Random bufferFiller = new(); + private readonly Random bufferFiller = new(123); /// /// The default length in bytes of each buffer chunk when allocating large buffers. From f4cc295e4cd4495e6491a5eef4e9e5c7efc995ae Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 12:15:08 +1000 Subject: [PATCH 18/73] Decode tile rows directly. Fix #2873 --- .../Formats/Tiff/TiffDecoderCore.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 96e6d2a89d..f16ffd0da1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -608,7 +608,7 @@ private void DecodeTilesPlanar( } /// - /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration. + /// Decodes the image data for TIFFs which arrange the pixel data in tiles and the chunky configuration. /// /// The pixel format. /// The image frame to decode into. @@ -634,14 +634,10 @@ private void DecodeTilesChunky( int width = pixels.Width; int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; - - int bytesPerRow = RoundUpToMultipleOfEight(width * bitsPerPixel); int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); - int uncompressedTilesSize = bytesPerTileRow * tileLength; - using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); - using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); + + using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(bytesPerTileRow * tileLength, AllocationOptions.Clean); Span tileBufferSpan = tileBuffer.GetSpan(); - Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); @@ -649,13 +645,15 @@ private void DecodeTilesChunky( int tileIndex = 0; for (int tileY = 0; tileY < tilesDown; tileY++) { - int remainingPixelsInRow = width; + int rowStartY = tileY * tileLength; + int rowEndY = Math.Min(rowStartY + tileLength, height); + for (int tileX = 0; tileX < tilesAcross; tileX++) { cancellationToken.ThrowIfCancellationRequested(); - int uncompressedPixelBufferOffset = tileY * tileLength * bytesPerRow; bool isLastHorizontalTile = tileX == tilesAcross - 1; + int remainingPixelsInRow = width - (tileX * tileWidth); decompressor.Decompress( this.inputStream, @@ -666,22 +664,23 @@ private void DecodeTilesChunky( cancellationToken); int tileBufferOffset = 0; - uncompressedPixelBufferOffset += bytesPerTileRow * tileX; - int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; - for (int y = 0; y < tileLength; y++) + for (int y = rowStartY; y < rowEndY; y++) { - Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); - tileBufferSpan.Slice(tileBufferOffset, bytesToCopy).CopyTo(uncompressedPixelRow); + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; + ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); + + // Decode the tile row directly into the pixel buffer. + int left = tileX * tileWidth; + int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); + + colorDecoder.Decode(tileRowSpan, pixels, left, y, rowWidth, 1); + tileBufferOffset += bytesPerTileRow; - uncompressedPixelBufferOffset += bytesPerRow; } - remainingPixelsInRow -= tileWidth; tileIndex++; } } - - colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); } private TiffBaseColorDecoder CreateChunkyColorDecoder() From 80b336bd0f62ae758d786b7de867ac7a0005b4bb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 12:19:11 +1000 Subject: [PATCH 19/73] Optimize --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index f16ffd0da1..18535d1e37 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -664,17 +664,15 @@ private void DecodeTilesChunky( cancellationToken); int tileBufferOffset = 0; + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; + int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); + int left = tileX * tileWidth; + for (int y = rowStartY; y < rowEndY; y++) { - int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; - ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); - // Decode the tile row directly into the pixel buffer. - int left = tileX * tileWidth; - int rowWidth = Math.Min(tileWidth, remainingPixelsInRow); - + ReadOnlySpan tileRowSpan = tileBufferSpan.Slice(tileBufferOffset, bytesToCopy); colorDecoder.Decode(tileRowSpan, pixels, left, y, rowWidth, 1); - tileBufferOffset += bytesPerTileRow; } From fcaef778a9924b07e4c0173fe678f2c3cd6b9f89 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 13:25:24 +1000 Subject: [PATCH 20/73] Fix CI build errors. --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/code-coverage.yml | 3 +++ src/ImageSharp/Formats/Png/PngThrowHelper.cs | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e17b8d724e..d0e2f91265 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -67,7 +67,7 @@ jobs: steps: - name: Install libgdi+, which is required for tests running on ubuntu - if: ${{ matrix.options.os == 'buildjet-4vcpu-ubuntu-2204-arm' }} + if: ${{ contains(matrix.options.os, 'ubuntu') }} run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index e551afbd6d..b4038e520d 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -17,6 +17,9 @@ jobs: runs-on: ${{matrix.options.os}} steps: + - name: Install libgdi+, which is required for tests running on ubuntu + run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + - name: Git Config shell: bash run: | diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 0552e9a79e..bb21f348f3 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -40,11 +40,11 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression("value")] string name = "") => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); [DoesNotReturn] - public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value1))] string name2 = "") + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression("value1")] string name1 = "", [CallerArgumentExpression("value2")] string name2 = "") => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); [DoesNotReturn] From b88f789ca04e59f3d8c5a5ac3ec19c952b158c6d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 13:30:56 +1000 Subject: [PATCH 21/73] Don't use TF during build, just test it. --- ci-build.ps1 | 5 ++++- src/ImageSharp/Formats/Png/PngThrowHelper.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ci-build.ps1 b/ci-build.ps1 index d45af6ff4d..d97cbbcb80 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -8,4 +8,7 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" # Building for a specific framework. -dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl +# dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl +# +# CI is now throwing build errors when none were present previously. +dotnet build -c Release -f /p:RepositoryUrl=$repositoryUrl diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index bb21f348f3..8dc70e1d9a 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -40,11 +40,11 @@ public static void ThrowInvalidImageContentException(string errorMessage, Except public static void ThrowInvalidChunkCrc(string chunkTypeName) => throw new InvalidImageContentException($"CRC Error. PNG {chunkTypeName} chunk is corrupt!"); [DoesNotReturn] - public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression("value")] string name = "") + public static void ThrowInvalidParameter(object value, string message, [CallerArgumentExpression(nameof(value))] string name = "") => throw new NotSupportedException($"Invalid {name}. {message}. Was '{value}'."); [DoesNotReturn] - public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression("value1")] string name1 = "", [CallerArgumentExpression("value2")] string name2 = "") + public static void ThrowInvalidParameter(object value1, object value2, string message, [CallerArgumentExpression(nameof(value1))] string name1 = "", [CallerArgumentExpression(nameof(value2))] string name2 = "") => throw new NotSupportedException($"Invalid {name1} or {name2}. {message}. Was '{value1}' and '{value2}'."); [DoesNotReturn] From 09ca511cccddabc1210d77e88a2eab5cc03bc56b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:14:01 +1000 Subject: [PATCH 22/73] Try using setup-dotnet v4 --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/code-coverage.yml | 2 +- ci-build.ps1 | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d0e2f91265..5cf6d98068 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -109,14 +109,14 @@ jobs: - name: DotNet Setup if: ${{ matrix.options.sdk-preview != true }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x - name: DotNet Setup Preview if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 7.0.x diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index b4038e520d..26e2013320 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -58,7 +58,7 @@ jobs: restore-keys: ${{ runner.os }}-nuget- - name: DotNet Setup - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x diff --git a/ci-build.ps1 b/ci-build.ps1 index d97cbbcb80..d45af6ff4d 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -8,7 +8,4 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" # Building for a specific framework. -# dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl -# -# CI is now throwing build errors when none were present previously. -dotnet build -c Release -f /p:RepositoryUrl=$repositoryUrl +dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From b350dbe74870ee9d1e77fd68be1ca76d72dba659 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:31:25 +1000 Subject: [PATCH 23/73] Try adding sdk install path to the global path --- .github/workflows/build-and-test.yml | 6 ++++++ .github/workflows/code-coverage.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5cf6d98068..c874fffc62 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -121,6 +121,12 @@ jobs: dotnet-version: | 7.0.x + - name: Ensure Dotnet Path + run: echo "/usr/share/dotnet" >> $GITHUB_PATH + + - name: Confirm .NET Version + run: dotnet --version # This will now find dotnet in /usr/share/dotnet + - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} shell: pwsh diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 26e2013320..7fdb4bc826 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -63,6 +63,12 @@ jobs: dotnet-version: | 6.0.x + - name: Ensure Dotnet Path + run: echo "/usr/share/dotnet" >> $GITHUB_PATH + + - name: Confirm .NET Version + run: dotnet --version # This will now find dotnet in /usr/share/dotnet + - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" From 80a1432d997398ec2ceae776303e4cf252033d90 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 14:39:16 +1000 Subject: [PATCH 24/73] Check the version within the powershell script --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/code-coverage.yml | 4 ++-- ci-build.ps1 | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c874fffc62..1edb5ae81e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -121,10 +121,10 @@ jobs: dotnet-version: | 7.0.x - - name: Ensure Dotnet Path + - name: Ensure DotNet Path run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm .NET Version + - name: Confirm DotNet Version run: dotnet --version # This will now find dotnet in /usr/share/dotnet - name: DotNet Build diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 7fdb4bc826..da7d792cd8 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -63,10 +63,10 @@ jobs: dotnet-version: | 6.0.x - - name: Ensure Dotnet Path + - name: Ensure DotNet Path run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm .NET Version + - name: Confirm DotNet Version run: dotnet --version # This will now find dotnet in /usr/share/dotnet - name: DotNet Build diff --git a/ci-build.ps1 b/ci-build.ps1 index d45af6ff4d..74d4328e32 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,9 +3,16 @@ param( [string]$targetFramework ) +#$env:DOTNET_ROOT = "/usr/share/dotnet" +#$env:PATH = "$env:DOTNET_ROOT" + [System.IO.Path]::PathSeparator + $env:PATH + +# Confirm dotnet version. +dotnet --version + dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" + # Building for a specific framework. dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From 069ca5a11755ea5f3cc56f86fb8c98b01afd1d92 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:01:44 +1000 Subject: [PATCH 25/73] Add more debugging info --- .github/workflows/build-and-test.yml | 3 ++- ci-build.ps1 | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1edb5ae81e..83bab0d837 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -122,10 +122,11 @@ jobs: 7.0.x - name: Ensure DotNet Path + if: ${{ matrix.options.os != 'windows-latest' }} run: echo "/usr/share/dotnet" >> $GITHUB_PATH - name: Confirm DotNet Version - run: dotnet --version # This will now find dotnet in /usr/share/dotnet + run: dotnet --version - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/ci-build.ps1 b/ci-build.ps1 index 74d4328e32..a98a5e27f8 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,8 +3,9 @@ param( [string]$targetFramework ) -#$env:DOTNET_ROOT = "/usr/share/dotnet" -#$env:PATH = "$env:DOTNET_ROOT" + [System.IO.Path]::PathSeparator + $env:PATH +Write-Output $env:PATH + +dotnet --list-sdks # Confirm dotnet version. dotnet --version @@ -13,6 +14,5 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" - # Building for a specific framework. dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl From 12b8e0dc428e8a86ffaa4c7eb1df4b4795dc089e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:06:55 +1000 Subject: [PATCH 26/73] Test windows only --- .github/workflows/build-and-test.yml | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 83bab0d837..ccab6d574b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,40 +19,40 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - - os: ubuntu-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: windows-latest - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: buildjet-4vcpu-ubuntu-2204-arm - framework: net7.0 - sdk: 7.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false + # - os: ubuntu-latest + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: windows-latest + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: buildjet-4vcpu-ubuntu-2204-arm + # framework: net7.0 + # sdk: 7.0.x + # sdk-preview: true + # runtime: -x64 + # codecov: false + # - os: ubuntu-latest + # framework: net6.0 + # sdk: 6.0.x + # runtime: -x64 + # codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # framework: net6.0 + # sdk: 6.0.x + # runtime: -x64 + # codecov: false - os: windows-latest framework: net6.0 sdk: 6.0.x From e046be656dd2010b254ba9fc4d5bdb6dd097bdfa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:24:19 +1000 Subject: [PATCH 27/73] Now test ubuntu net6 again --- .github/workflows/build-and-test.yml | 14 +++++++------- ci-build.ps1 | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ccab6d574b..a0f9f1b8bc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -43,21 +43,21 @@ jobs: # sdk-preview: true # runtime: -x64 # codecov: false - # - os: ubuntu-latest + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable # framework: net6.0 # sdk: 6.0.x # runtime: -x64 # codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + # - os: windows-latest # framework: net6.0 # sdk: 6.0.x # runtime: -x64 # codecov: false - - os: windows-latest - framework: net6.0 - sdk: 6.0.x - runtime: -x64 - codecov: false exclude: - isARM: false options: diff --git a/ci-build.ps1 b/ci-build.ps1 index a98a5e27f8..d465d8da79 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,11 +3,14 @@ param( [string]$targetFramework ) +Write-Output "PATH" Write-Output $env:PATH +Write-Output "DOTNET_LIST" dotnet --list-sdks # Confirm dotnet version. +Write-Output "DOTNET_VERSION" dotnet --version dotnet clean -c Release From 3a286c53bb7e8bbde0cd90c943b3d7b84949b120 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:27:50 +1000 Subject: [PATCH 28/73] Try installing multiple sdks --- .github/workflows/build-and-test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a0f9f1b8bc..b46429379d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -112,6 +112,8 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | + 8.0.x + 7.0.x 6.0.x - name: DotNet Setup Preview @@ -121,12 +123,12 @@ jobs: dotnet-version: | 7.0.x - - name: Ensure DotNet Path - if: ${{ matrix.options.os != 'windows-latest' }} - run: echo "/usr/share/dotnet" >> $GITHUB_PATH + # - name: Ensure DotNet Path + # if: ${{ matrix.options.os != 'windows-latest' }} + # run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - name: Confirm DotNet Version - run: dotnet --version + # - name: Confirm DotNet Version + # run: dotnet --version - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} From 49a11182efd96a1b911c9a94414c4c42277058b0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Jan 2025 16:36:28 +1000 Subject: [PATCH 29/73] Re-enable all builds --- .github/workflows/build-and-test.yml | 83 ++++++++++++---------------- .github/workflows/code-coverage.yml | 8 +-- ci-build.ps1 | 10 ---- 3 files changed, 36 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b46429379d..a35175ff49 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,45 +19,45 @@ jobs: isARM: - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - # - os: ubuntu-latest - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: windows-latest - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - # - os: buildjet-4vcpu-ubuntu-2204-arm - # framework: net7.0 - # sdk: 7.0.x - # sdk-preview: true - # runtime: -x64 - # codecov: false - os: ubuntu-latest + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: windows-latest + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: buildjet-4vcpu-ubuntu-2204-arm + framework: net7.0 + sdk: 7.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + framework: net6.0 + sdk: 6.0.x + runtime: -x64 + codecov: false + - os: windows-latest framework: net6.0 sdk: 6.0.x runtime: -x64 codecov: false - # - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - # framework: net6.0 - # sdk: 6.0.x - # runtime: -x64 - # codecov: false - # - os: windows-latest - # framework: net6.0 - # sdk: 6.0.x - # runtime: -x64 - # codecov: false exclude: - isARM: false options: @@ -108,7 +108,6 @@ jobs: restore-keys: ${{ runner.os }}-nuget- - name: DotNet Setup - if: ${{ matrix.options.sdk-preview != true }} uses: actions/setup-dotnet@v4 with: dotnet-version: | @@ -116,20 +115,6 @@ jobs: 7.0.x 6.0.x - - name: DotNet Setup Preview - if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 7.0.x - - # - name: Ensure DotNet Path - # if: ${{ matrix.options.os != 'windows-latest' }} - # run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - # - name: Confirm DotNet Version - # run: dotnet --version - - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} shell: pwsh diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index da7d792cd8..db9aca0b08 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -61,14 +61,10 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | + 8.0.x + 7.0.x 6.0.x - - name: Ensure DotNet Path - run: echo "/usr/share/dotnet" >> $GITHUB_PATH - - - name: Confirm DotNet Version - run: dotnet --version # This will now find dotnet in /usr/share/dotnet - - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" diff --git a/ci-build.ps1 b/ci-build.ps1 index d465d8da79..d45af6ff4d 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -3,16 +3,6 @@ param( [string]$targetFramework ) -Write-Output "PATH" -Write-Output $env:PATH - -Write-Output "DOTNET_LIST" -dotnet --list-sdks - -# Confirm dotnet version. -Write-Output "DOTNET_VERSION" -dotnet --version - dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" From 6f3875383050668148f8280e83ff523cdcc6cfc9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 17:36:30 +1000 Subject: [PATCH 30/73] 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 b99bf32bb19a1e88083cae48a8c96a7531859604 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:41:10 +1000 Subject: [PATCH 31/73] Gracefully handle LZW overflows --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 6 ++++-- .../ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs | 12 ++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Gif/issues/issue_2859_A.gif | 3 +++ tests/Images/Input/Gif/issues/issue_2859_B.gif | 3 +++ 5 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Gif/issues/issue_2859_A.gif create mode 100644 tests/Images/Input/Gif/issues/issue_2859_B.gif diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index ec33f2b3ed..7a082e3674 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -137,6 +137,7 @@ public void DecodePixelRow(Span indices) ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); Span buffer = this.scratchBuffer.GetSpan(); + int maxTop = this.pixelStack.Length() - 1; int x = 0; int xyz = 0; @@ -204,7 +205,7 @@ public void DecodePixelRow(Span indices) this.code = this.oldCode; } - while (this.code > this.clearCode) + while (this.code > this.clearCode && this.top < maxTop) { Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code); @@ -250,6 +251,7 @@ public void SkipIndices(int length) ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); Span buffer = this.scratchBuffer.GetSpan(); + int maxTop = this.pixelStack.Length() - 1; int xyz = 0; while (xyz < length) @@ -316,7 +318,7 @@ public void SkipIndices(int length) this.code = this.oldCode; } - while (this.code > this.clearCode) + while (this.code > this.clearCode && this.top < maxTop) { Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index f4e6487a57..bc6eeedcbe 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -334,4 +334,16 @@ public void IssueTooLargeLzwBits(TestImageProvider provider) image.DebugSaveMultiFrame(provider); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + + // https://github.com/SixLabors/ImageSharp/issues/2859 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2859_A, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Issues.Issue2859_B, PixelTypes.Rgba32)] + public void Issue2859_LZWPixelStackOverflow(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a0e951e70d..fa3f38799a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -535,6 +535,8 @@ 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 Issue2859_A = "Gif/issues/issue_2859_A.gif"; + public const string Issue2859_B = "Gif/issues/issue_2859_B.gif"; } public static readonly string[] Animated = diff --git a/tests/Images/Input/Gif/issues/issue_2859_A.gif b/tests/Images/Input/Gif/issues/issue_2859_A.gif new file mode 100644 index 0000000000..f19a047525 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_A.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50a1a4afc62a3a36ff83596f1eb55d91cdd184c64e0d1339bbea17205c23eee1 +size 3406142 diff --git a/tests/Images/Input/Gif/issues/issue_2859_B.gif b/tests/Images/Input/Gif/issues/issue_2859_B.gif new file mode 100644 index 0000000000..109b0f8797 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2859_B.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db9b2992be772a4f0ac495e994a17c7c50fb6de9794cfb9afc4a3dc26ffdfae0 +size 4543 From 170979743ece9b10414868865897378be8eb7ba8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:44:25 +1000 Subject: [PATCH 32/73] Add reference output --- .../00.png | 3 +++ .../00.png | 3 +++ .../01.png | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png create mode 100644 tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png new file mode 100644 index 0000000000..d8c8df1263 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_A.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daa78347749c6ff49891e2e379a373599cd35c98b453af9bf8eac52f615f935c +size 12237 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png new file mode 100644 index 0000000000..36c3683187 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731299281f942f277ce6803e0adda3b5dd0395eb79cae26cabc9d56905fae0fd +size 1833 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png new file mode 100644 index 0000000000..c03e5817f0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2859_LZWPixelStackOverflow_Rgba32_issue_2859_B.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ccac7739142578d99a76b6d39ba377099d4a7ac30cbb0a5aee44ef1e7c9c8c +size 1271 From 9cb022bc96882c0484d6f617babb5141e4d224f0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 4 Feb 2025 18:52:00 +1000 Subject: [PATCH 33/73] 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 ca077545c45263f17e5f7764a155cf20e44f5580 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 5 Feb 2025 15:15:01 +1000 Subject: [PATCH 34/73] Use safe code. --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 308 ++++++++++++++--------- 1 file changed, 183 insertions(+), 125 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index 7a082e3674..ece0f22888 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -37,22 +36,22 @@ internal sealed class LzwDecoder : IDisposable /// /// The prefix buffer. /// - private readonly IMemoryOwner prefix; + private readonly IMemoryOwner prefixOwner; /// /// The suffix buffer. /// - private readonly IMemoryOwner suffix; + private readonly IMemoryOwner suffixOwner; /// /// The scratch buffer for reading data blocks. /// - private readonly IMemoryOwner scratchBuffer; + private readonly IMemoryOwner bufferOwner; /// /// The pixel stack buffer. /// - private readonly IMemoryOwner pixelStack; + private readonly IMemoryOwner pixelStackOwner; private readonly int minCodeSize; private readonly int clearCode; private readonly int endCode; @@ -80,10 +79,10 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); - this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); - this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); - this.scratchBuffer = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); + this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); + this.pixelStackOwner = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + this.bufferOwner = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); this.minCodeSize = minCodeSize; // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize @@ -93,10 +92,11 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in this.endCode = this.clearCode + 1; this.availableCode = this.clearCode + 2; - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - for (this.code = 0; this.code < this.clearCode; this.code++) + Span suffix = this.suffixOwner.GetSpan(); + int max = Math.Min(this.clearCode, suffix.Length); + for (this.code = 0; this.code < max; this.code++) { - Unsafe.Add(ref suffixRef, (uint)this.code) = (byte)this.code; + suffix[this.code] = (byte)this.code; } } @@ -132,113 +132,140 @@ public void DecodePixelRow(Span indices) { indices.Clear(); - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices); - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); - Span buffer = this.scratchBuffer.GetSpan(); - int maxTop = this.pixelStack.Length() - 1; + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + int maxTop = this.pixelStackOwner.Length() - 1; + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; - int x = 0; int xyz = 0; while (xyz < indices.Length) { - if (this.top == 0) + if (top == 0) { - if (this.bits < this.codeSize) + if (bits < codeSize) { // Load bytes until there are enough bits for a code. - if (this.count == 0) + if (count == 0) { // Read a new data block. - this.count = this.ReadBlock(buffer); - if (this.count == 0) + count = ReadBlock(stream, buffer); + if (count == 0) { break; } - this.bufferIndex = 0; + bufferIndex = 0; } - this.data += buffer[this.bufferIndex] << this.bits; - - this.bits += 8; - this.bufferIndex++; - this.count--; + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; continue; } // Get the next code - this.code = this.data & this.codeMask; - this.data >>= this.codeSize; - this.bits -= this.codeSize; + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; // Interpret the code - if (this.code > this.availableCode || this.code == this.endCode) + if (code > availableCode || code == endCode) { break; } - if (this.code == this.clearCode) + if (code == clearCode) { // Reset the decoder - this.codeSize = this.minCodeSize + 1; - this.codeMask = (1 << this.codeSize) - 1; - this.availableCode = this.clearCode + 2; - this.oldCode = NullCode; + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; continue; } - if (this.oldCode == NullCode) + if (oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.oldCode = this.code; - this.first = this.code; + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; continue; } - int inCode = this.code; - if (this.code == this.availableCode) + int inCode = code; + if (code == availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - - this.code = this.oldCode; + pixelStack[top++] = first; + code = oldCode; } - while (this.code > this.clearCode && this.top < maxTop) + while (code > clearCode && top < maxTop) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.code = Unsafe.Add(ref prefixRef, (uint)this.code); + pixelStack[top++] = suffix[code]; + code = prefix[code]; } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); - this.first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; - // Fix for Gifs that have "deferred clear code" as per here : + // Fix for GIFs that have "deferred clear code" as per: // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (this.availableCode < MaxStackSize) + if (availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; - Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; - this.availableCode++; - if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) { - this.codeSize++; - this.codeMask = (1 << this.codeSize) - 1; + codeSize++; + codeMask = (1 << codeSize) - 1; } } - this.oldCode = inCode; + oldCode = inCode; } // Pop a pixel off the pixel stack. - this.top--; + top--; - // Clear missing pixels - xyz++; - Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)this.top); + // Clear missing pixels. + indices[xyz++] = (byte)pixelStack[top]; } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; } /// @@ -247,131 +274,162 @@ public void DecodePixelRow(Span indices) /// The resulting index table length. public void SkipIndices(int length) { - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); - ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); - ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); - Span buffer = this.scratchBuffer.GetSpan(); - int maxTop = this.pixelStack.Length() - 1; + // Get span values from the owners. + Span prefix = this.prefixOwner.GetSpan(); + Span suffix = this.suffixOwner.GetSpan(); + Span pixelStack = this.pixelStackOwner.GetSpan(); + Span buffer = this.bufferOwner.GetSpan(); + int maxTop = this.pixelStackOwner.Length() - 1; + + // Cache frequently accessed instance fields into locals. + // This helps avoid repeated field loads inside the tight loop. + BufferedReadStream stream = this.stream; + int top = this.top; + int bits = this.bits; + int codeSize = this.codeSize; + int codeMask = this.codeMask; + int minCodeSize = this.minCodeSize; + int availableCode = this.availableCode; + int oldCode = this.oldCode; + int first = this.first; + int data = this.data; + int count = this.count; + int bufferIndex = this.bufferIndex; + int code = this.code; + int clearCode = this.clearCode; + int endCode = this.endCode; int xyz = 0; while (xyz < length) { - if (this.top == 0) + if (top == 0) { - if (this.bits < this.codeSize) + if (bits < codeSize) { // Load bytes until there are enough bits for a code. - if (this.count == 0) + if (count == 0) { // Read a new data block. - this.count = this.ReadBlock(buffer); - if (this.count == 0) + count = ReadBlock(stream, buffer); + if (count == 0) { break; } - this.bufferIndex = 0; + bufferIndex = 0; } - this.data += buffer[this.bufferIndex] << this.bits; - - this.bits += 8; - this.bufferIndex++; - this.count--; + data += buffer[bufferIndex] << bits; + bits += 8; + bufferIndex++; + count--; continue; } // Get the next code - this.code = this.data & this.codeMask; - this.data >>= this.codeSize; - this.bits -= this.codeSize; + code = data & codeMask; + data >>= codeSize; + bits -= codeSize; // Interpret the code - if (this.code > this.availableCode || this.code == this.endCode) + if (code > availableCode || code == endCode) { break; } - if (this.code == this.clearCode) + if (code == clearCode) { // Reset the decoder - this.codeSize = this.minCodeSize + 1; - this.codeMask = (1 << this.codeSize) - 1; - this.availableCode = this.clearCode + 2; - this.oldCode = NullCode; + codeSize = minCodeSize + 1; + codeMask = (1 << codeSize) - 1; + availableCode = clearCode + 2; + oldCode = NullCode; continue; } - if (this.oldCode == NullCode) + if (oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.oldCode = this.code; - this.first = this.code; + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; continue; } - int inCode = this.code; - if (this.code == this.availableCode) + int inCode = code; + if (code == availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - - this.code = this.oldCode; + pixelStack[top++] = first; + code = oldCode; } - while (this.code > this.clearCode && this.top < maxTop) + while (code > clearCode && top < maxTop) { - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); - this.code = Unsafe.Add(ref prefixRef, (uint)this.code); + pixelStack[top++] = suffix[code]; + code = prefix[code]; } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); - this.first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; + int suffixCode = suffix[code]; + first = suffixCode; + pixelStack[top++] = suffixCode; - // Fix for Gifs that have "deferred clear code" as per here : + // Fix for GIFs that have "deferred clear code" as per: // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (this.availableCode < MaxStackSize) + if (availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; - Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; - this.availableCode++; - if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) + prefix[availableCode] = oldCode; + suffix[availableCode] = first; + availableCode++; + if (availableCode == codeMask + 1 && availableCode < MaxStackSize) { - this.codeSize++; - this.codeMask = (1 << this.codeSize) - 1; + codeSize++; + codeMask = (1 << codeSize) - 1; } } - this.oldCode = inCode; + oldCode = inCode; } // Pop a pixel off the pixel stack. - this.top--; + top--; - // Clear missing pixels + // Skip missing pixels. xyz++; } + + // Write back the local values to the instance fields. + this.top = top; + this.bits = bits; + this.codeSize = codeSize; + this.codeMask = codeMask; + this.availableCode = availableCode; + this.oldCode = oldCode; + this.first = first; + this.data = data; + this.count = count; + this.bufferIndex = bufferIndex; + this.code = code; } /// /// Reads the next data block from the stream. A data block begins with a byte, /// which defines the size of the block, followed by the block itself. /// + /// The stream to read from. /// The buffer to store the block in. /// /// The . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int ReadBlock(Span buffer) + private static int ReadBlock(BufferedReadStream stream, Span buffer) { - int bufferSize = this.stream.ReadByte(); + int bufferSize = stream.ReadByte(); if (bufferSize < 1) { return 0; } - int count = this.stream.Read(buffer, 0, bufferSize); + int count = stream.Read(buffer, 0, bufferSize); return count != bufferSize ? 0 : bufferSize; } @@ -379,9 +437,9 @@ private int ReadBlock(Span buffer) /// public void Dispose() { - this.prefix.Dispose(); - this.suffix.Dispose(); - this.pixelStack.Dispose(); - this.scratchBuffer.Dispose(); + this.prefixOwner.Dispose(); + this.suffixOwner.Dispose(); + this.pixelStackOwner.Dispose(); + this.bufferOwner.Dispose(); } } From a888544c711cedd5b4a7a8541c034b2ffa1303a9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Feb 2025 09:39:41 +1000 Subject: [PATCH 35/73] 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 36/73] 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 37/73] 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 38/73] 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 39/73] 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 40/73] 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 41/73] 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 20e5596f2a450c7911053cf3ba3fb7d5f03f7f62 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Feb 2025 11:45:47 +1000 Subject: [PATCH 42/73] Clean up and optimize --- src/ImageSharp/Formats/Gif/LzwDecoder.cs | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index ece0f22888..b33d1f3d48 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -78,6 +78,7 @@ internal sealed class LzwDecoder : IDisposable public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size."); this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); @@ -92,12 +93,15 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, in this.endCode = this.clearCode + 1; this.availableCode = this.clearCode + 2; - Span suffix = this.suffixOwner.GetSpan(); - int max = Math.Min(this.clearCode, suffix.Length); - for (this.code = 0; this.code < max; this.code++) + // Fill the suffix buffer with the initial values represented by the number of colors. + Span suffix = this.suffixOwner.GetSpan()[..this.clearCode]; + int i; + for (i = 0; i < suffix.Length; i++) { - suffix[this.code] = (byte)this.code; + suffix[i] = i; } + + this.code = i; } /// @@ -112,8 +116,7 @@ public static bool IsValidMinCodeSize(int minCodeSize) // It is possible to specify a larger LZW minimum code size than the palette length in bits // which may leave a gap in the codes where no colors are assigned. // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression - int clearCode = 1 << minCodeSize; - if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) + if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize) { // Don't attempt to decode the frame indices. // Theoretically we could determine a min code size from the length of the provided @@ -137,7 +140,6 @@ public void DecodePixelRow(Span indices) Span suffix = this.suffixOwner.GetSpan(); Span pixelStack = this.pixelStackOwner.GetSpan(); Span buffer = this.bufferOwner.GetSpan(); - int maxTop = this.pixelStackOwner.Length() - 1; // Cache frequently accessed instance fields into locals. // This helps avoid repeated field loads inside the tight loop. @@ -157,8 +159,8 @@ public void DecodePixelRow(Span indices) int clearCode = this.clearCode; int endCode = this.endCode; - int xyz = 0; - while (xyz < indices.Length) + int i = 0; + while (i < indices.Length) { if (top == 0) { @@ -220,7 +222,7 @@ public void DecodePixelRow(Span indices) code = oldCode; } - while (code > clearCode && top < maxTop) + while (code > clearCode && top < MaxStackSize) { pixelStack[top++] = suffix[code]; code = prefix[code]; @@ -251,7 +253,7 @@ public void DecodePixelRow(Span indices) top--; // Clear missing pixels. - indices[xyz++] = (byte)pixelStack[top]; + indices[i++] = (byte)pixelStack[top]; } // Write back the local values to the instance fields. @@ -279,7 +281,6 @@ public void SkipIndices(int length) Span suffix = this.suffixOwner.GetSpan(); Span pixelStack = this.pixelStackOwner.GetSpan(); Span buffer = this.bufferOwner.GetSpan(); - int maxTop = this.pixelStackOwner.Length() - 1; // Cache frequently accessed instance fields into locals. // This helps avoid repeated field loads inside the tight loop. @@ -299,8 +300,8 @@ public void SkipIndices(int length) int clearCode = this.clearCode; int endCode = this.endCode; - int xyz = 0; - while (xyz < length) + int i = 0; + while (i < length) { if (top == 0) { @@ -362,7 +363,7 @@ public void SkipIndices(int length) code = oldCode; } - while (code > clearCode && top < maxTop) + while (code > clearCode && top < MaxStackSize) { pixelStack[top++] = suffix[code]; code = prefix[code]; @@ -393,7 +394,7 @@ public void SkipIndices(int length) top--; // Skip missing pixels. - xyz++; + i++; } // Write back the local values to the instance fields. From 5d77de9b9ceef9d5091aba4791ec588e09fd9fe7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Feb 2025 22:52:29 +1000 Subject: [PATCH 43/73] 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 44/73] 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 45/73] 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 46/73] 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 47/73] 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 48/73] 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 From d7e58103ec61d7e973f920e2dcf018a7225580e4 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Tue, 4 Mar 2025 02:55:35 +0100 Subject: [PATCH 54/73] set LangVersion in all projects --- tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 3 ++- .../ImageSharp.Tests.ProfilingSandbox.csproj | 1 + tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 0ba2f4b949..c7ce89d13c 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -11,7 +11,8 @@ false Debug;Release - + + 12.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 492ce36b81..a39b5467e4 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -14,6 +14,7 @@ false Debug;Release false + 12.0 diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index dc081e0bea..aa9cb2614e 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -7,6 +7,7 @@ AnyCPU;x64;x86;ARM64 SixLabors.ImageSharp.Tests Debug;Release + 12.0 From 80f73fb3a242efc99195609f7cf92c2da0160a6f Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Tue, 4 Mar 2025 03:08:15 +0100 Subject: [PATCH 55/73] set v12 unconditionally and globally --- Directory.Build.props | 4 ++-- src/ImageSharp/ImageSharp.csproj | 1 - tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj | 1 - .../ImageSharp.Tests.ProfilingSandbox.csproj | 1 - tests/ImageSharp.Tests/ImageSharp.Tests.csproj | 1 - 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 26b3cc5afc..c94d9cbc9c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,9 +21,9 @@ - + - preview + 12.0 diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index c7ce89d13c..8315dfa4b5 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -12,7 +12,6 @@ Debug;Release - 12.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index a39b5467e4..492ce36b81 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -14,7 +14,6 @@ false Debug;Release false - 12.0 diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index aa9cb2614e..dc081e0bea 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -7,7 +7,6 @@ AnyCPU;x64;x86;ARM64 SixLabors.ImageSharp.Tests Debug;Release - 12.0 From 6430b8efee062b9fbfd58feb885e30de5bbc3095 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Mar 2025 15:33:58 +1000 Subject: [PATCH 56/73] Fix GIF RestoreToPrevious --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 106 +++++++++++++----- ...2012BadMinCode_Rgba32_issue2012_drona1.png | 4 +- .../07.png | 4 +- 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index fccb994358..ad073ccbb1 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -89,8 +89,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore /// private GifMetadata? gifMetadata; - // The background color used to fill the frame. - private Color backgroundColor; + /// + /// The background color index. + /// + private byte backgroundColorIndex; /// /// Initializes a new instance of the class. @@ -113,11 +115,11 @@ protected override Image Decode(BufferedReadStream stream, Cance ImageFrame? previousFrame = null; GifDisposalMethod? previousDisposalMethod = null; bool globalColorTableUsed = false; + Color backgroundColor = Color.Transparent; try { this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); - TPixel backgroundPixel = this.backgroundColor.ToPixel(); // Loop though the respective gif parts and read the data. int nextFlag = stream.ReadByte(); @@ -130,7 +132,7 @@ protected override Image Decode(BufferedReadStream stream, Cance break; } - globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, backgroundPixel); + globalColorTableUsed |= this.ReadFrame(stream, ref image, ref previousFrame, ref previousDisposalMethod, ref backgroundColor); // Reset per-frame state. this.imageDescriptor = default; @@ -432,13 +434,13 @@ private void ReadComments(BufferedReadStream stream) /// The image to decode the information to. /// The previous frame. /// The previous disposal method. - /// The background color pixel. + /// The background color. private bool ReadFrame( BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, ref GifDisposalMethod? previousDisposalMethod, - TPixel backgroundPixel) + ref Color backgroundColor) where TPixel : unmanaged, IPixel { this.ReadImageDescriptor(stream); @@ -465,7 +467,47 @@ private bool ReadFrame( } ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); - this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundPixel); + + // First frame + if (image is null) + { + if (this.backgroundColorIndex < colorTable.Length) + { + backgroundColor = colorTable[this.backgroundColorIndex]; + } + else + { + backgroundColor = Color.Transparent; + } + + if (this.graphicsControlExtension.TransparencyFlag) + { + backgroundColor = backgroundColor.WithAlpha(0); + } + } + + this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMethod, colorTable, this.imageDescriptor, backgroundColor.ToPixel()); + + // Update from newly decoded frame. + if (this.graphicsControlExtension.DisposalMethod != GifDisposalMethod.RestoreToPrevious) + { + if (this.backgroundColorIndex < colorTable.Length) + { + backgroundColor = colorTable[this.backgroundColorIndex]; + } + else + { + backgroundColor = Color.Transparent; + } + + // TODO: I don't understand why this is always set to alpha of zero. + // This should be dependent on the transparency flag of the graphics + // control extension. ImageMagick does the same. + // if (this.graphicsControlExtension.TransparencyFlag) + { + backgroundColor = backgroundColor.WithAlpha(0); + } + } // Skip any remaining blocks SkipBlock(stream); @@ -497,11 +539,11 @@ private void ReadFrameColors( int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; bool transFlag = this.graphicsControlExtension.TransparencyFlag; - GifDisposalMethod disposalMethod = this.graphicsControlExtension.DisposalMethod; - ImageFrame currentFrame; - if (previousFrame is null) + ImageFrame? restoreFrame = null; + + if (previousFrame is null && previousDisposalMethod is null) { image = transFlag ? new Image(this.configuration, imageWidth, imageHeight, this.metadata) @@ -512,26 +554,42 @@ private void ReadFrameColors( } else { - // 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 - currentFrame = image!.Frames.AddFrame(previousFrame); + if (previousFrame != null) + { + currentFrame = image!.Frames.AddFrame(previousFrame); + } + else + { + currentFrame = image!.Frames.CreateFrame(backgroundPixel); + } this.SetFrameMetadata(currentFrame.Metadata); + if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) + { + restoreFrame = previousFrame; + } + 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; + if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) + { + previousFrame = restoreFrame; + } + else + { + previousFrame = currentFrame; + } + previousDisposalMethod = disposalMethod; if (disposalMethod == GifDisposalMethod.RestoreToBackground) { - this.restoreArea = interest; + this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height)); } if (colorTable.Length == 0) @@ -812,19 +870,9 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s } } - // 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]; - this.gifMetadata.BackgroundColorIndex = index; - } - else - { - this.backgroundColor = Color.Transparent; - } + this.backgroundColorIndex = index; + this.gifMetadata.BackgroundColorIndex = index; } private unsafe struct ScratchBuffer 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 6f2fc842f8..5d443b52aa 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:ff67035f78690321c29a4e15c8de7c55bcb3260d667dbd9bced15de6b626fca1 -size 148499 +oid sha256:a0e1677baade797de1eaec390f2e475865d24de8bd344edddbb3fce200d6bcb0 +size 135418 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png index efba40c99d..97610dbc00 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309 -size 126 +oid sha256:c816ca1e58d14361b84ba47454e4cbf4d3e4d29dfb7827756eb52ef2604f297c +size 161 From bf66f24a81fd0c222b5a31d637ced74ee5ed8866 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Mar 2025 15:58:34 +1000 Subject: [PATCH 57/73] Respond to feedback --- .../Formats/Webp/Lossy/Vp8Decoder.cs | 12 +-- .../Quantization/EuclideanPixelMap{TPixel}.cs | 22 ++--- .../Formats/Gif/GifEncoderTests.cs | 11 ++- .../Formats/Png/PngEncoderTests.cs | 2 +- .../TestUtilities/TestImageExtensions.cs | 87 +++++++++++++++++++ .../00.gif | 3 + .../08.gif | 3 + .../104.gif | 3 + .../112.gif | 3 + .../16.gif | 3 + .../24.gif | 3 + .../32.gif | 3 + .../40.gif | 3 + .../48.gif | 3 + .../56.gif | 3 + .../64.gif | 3 + .../72.gif | 3 + .../80.gif | 3 + .../88.gif | 3 + .../96.gif | 3 + 20 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif create mode 100644 tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs index eb4a517511..8e0c986a14 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs @@ -67,13 +67,13 @@ 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, AllocationOptions.Clean); + this.CacheY = memoryAllocator.Allocate((16 * this.CacheYStride) + extraY); int cacheUvSize = (16 * this.CacheUvStride) + extraUv; - 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.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), AllocationOptions.Clean); #if DEBUG diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index c76f9158c5..5e6ef9d39c 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -126,7 +127,7 @@ private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel m { // We have explicit instructions. No need to search. index = this.transparentIndex; - this.cache.Add(rgba, (byte)index); + this.cache.Add(rgba, (short)index); match = this.transparentMatch; return index; } @@ -153,7 +154,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); + this.cache.Add(rgba, (short)index); match = Unsafe.Add(ref paletteRef, (uint)index); return index; @@ -168,11 +169,9 @@ private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel m [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); + 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(); @@ -221,7 +220,7 @@ public HybridColorDistanceCache(MemoryAllocator allocator) this.exactCache = new ExactCache(allocator); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] public readonly void Add(Rgba32 color, short index) { if (this.exactCache.TryAdd(color.PackedValue, index)) @@ -232,7 +231,7 @@ public readonly void Add(Rgba32 color, short index) this.fallbackPointer[GetCoarseIndex(color)] = index; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] public readonly bool TryGetValue(Rgba32 color, out short match) { if (this.exactCache.TryGetValue(color.PackedValue, out match)) @@ -244,7 +243,7 @@ public readonly bool TryGetValue(Rgba32 color, out short match) return match > -1; // Coarse match found } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] private static int GetCoarseIndex(Rgba32 color) { int rIndex = color.R >> (8 - IndexRBits); @@ -325,8 +324,6 @@ public ExactCache(MemoryAllocator allocator) /// /// 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) @@ -380,7 +377,6 @@ public bool TryAdd(uint key, short value) /// 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); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 1e152703ab..dcbd4b38eb 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -8,7 +8,6 @@ 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; @@ -386,12 +385,18 @@ public void Encode_Animated_VisualTest(TestImageProvider provide [Theory] [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32)] - public void GifEncoder_CanDecode_Issue2866(TestImageProvider provider) + public void GifEncoder_CanDecode_AndEncode_Issue2866(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - // image.DebugSaveMultiFrame(provider); + // Save the image for visual inspection. provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); + + // Now compare the debug output with the reference output. + // We do this because the gif encoding is lossy and encoding will lead to differences in the 10s of percent. + // From the unencoded image, we can see that the image is visually the same. + static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them. + image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index db4290c175..4f4c6bd1b7 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -489,7 +489,7 @@ public void Encode_APng(TestImageProvider provider) public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider, float percentage) where TPixel : unmanaged, IPixel { - if (TestEnvironment.RunsOnCI) + if (TestEnvironment.RunsOnCI && !TestEnvironment.IsWindows) { return; } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 3c74b48938..01d3b64891 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -277,6 +277,45 @@ public static Image CompareFirstFrameToReferenceOutput( return image; } + public static Image CompareDebugOutputToReferenceOutputMultiFrame( + this Image image, + ITestImageProvider provider, + ImageComparer comparer, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = true, + Func predicate = null) + where TPixel : unmanaged, IPixel + { + image.DebugSaveMultiFrame( + provider, + testOutputDetails, + extension, + appendPixelTypeToFileName, + predicate: predicate); + + using (Image debugImage = GetDebugOutputImageMultiFrame( + provider, + image.Frames.Count, + testOutputDetails, + extension, + appendPixelTypeToFileName, + predicate: predicate)) + + using (Image referenceImage = GetReferenceOutputImageMultiFrame( + provider, + image.Frames.Count, + testOutputDetails, + extension, + appendPixelTypeToFileName, + predicate: predicate)) + { + comparer.VerifySimilarity(referenceImage, debugImage); + } + + return image; + } + public static Image CompareToReferenceOutputMultiFrame( this Image image, ITestImageProvider provider, @@ -375,6 +414,54 @@ public static Image GetReferenceOutputImageMultiFrame( return result; } + public static Image GetDebugOutputImageMultiFrame( + this ITestImageProvider provider, + int frameCount, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = true, + Func predicate = null) + where TPixel : unmanaged, IPixel + { + (int Index, string FileName)[] frameFiles = provider.Utility.GetTestOutputFileNamesMultiFrame( + frameCount, + extension, + testOutputDetails, + appendPixelTypeToFileName, + predicate: predicate).ToArray(); + + List> temporaryFrameImages = new(); + + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0].FileName); + + for (int i = 0; i < frameFiles.Length; i++) + { + string path = frameFiles[i].FileName; + if (!File.Exists(path)) + { + throw new FileNotFoundException("Reference output file missing: " + path); + } + + using FileStream stream = File.OpenRead(path); + Image tempImage = decoder.Decode(DecoderOptions.Default, stream); + temporaryFrameImages.Add(tempImage); + } + + Image firstTemp = temporaryFrameImages[0]; + + Image result = new(firstTemp.Width, firstTemp.Height); + + foreach (Image fi in temporaryFrameImages) + { + result.Frames.AddFrame(fi.Frames.RootFrame); + fi.Dispose(); + } + + // Remove the initial empty frame: + result.Frames.RemoveFrame(0); + return result; + } + public static IEnumerable GetReferenceOutputSimilarityReports( this Image image, ITestImageProvider provider, diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif new file mode 100644 index 0000000000..8668f7102a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e738470c04121376b42937f0a72eed3f068bc32bdc788cc0d5877bc7f705f419 +size 67611 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif new file mode 100644 index 0000000000..8fd66d1206 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33b9244a09ed3b7bb17dfcbb694cbacc4f7de8ce6ffd7d7f7c35d424a6e8b2c5 +size 58696 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif new file mode 100644 index 0000000000..90b8a081ce --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:380f86015751a8445eb706d99001efe96633c62dd183c22303e732d036175dff +size 86095 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif new file mode 100644 index 0000000000..180c1e730b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae0f80bbc03fc35be4622df85f13f8fe7aec2d981424d9ca6dce88bd23ff2dad +size 59015 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif new file mode 100644 index 0000000000..6eceee3979 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:696ef2d76fe8ae8a32f0299b668b0cf537c1ffe5ac3ca4b418499f03ca134d89 +size 70345 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif new file mode 100644 index 0000000000..51ef668112 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dbd382f6f7b71f4cdffe61c01f0ab04523423576bad6195f8950ff7758792a2 +size 67153 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif new file mode 100644 index 0000000000..26818c6a8f --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2942af218c05b29b4f5a8eb9117c8dea4dcf61b33e1ade3cfa6ba47a0d71f6c +size 68007 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif new file mode 100644 index 0000000000..92f5c1fb95 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45872e2361fc445d956abd5de454ad42bcdb8ade69742b032c865978e41cb477 +size 73531 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif new file mode 100644 index 0000000000..5524ce8a41 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:432af0fd8df68724f2faa3fc90dc751ff15a7e45604e20f3737d1902c64a913e +size 72024 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif new file mode 100644 index 0000000000..bfd22ffef5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aab6a8c7892d8449ad8915f414689019684f99d3f011ddb82f722037b35a70c0 +size 77189 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif new file mode 100644 index 0000000000..e3f17772eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b93da4e1ab6ed01c561d5b565c078ff8752f7b16b78c733b658237977b1e4689 +size 78172 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif new file mode 100644 index 0000000000..cfe4a2eefe --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b426203cd6b6b5f4b97a5213a8a04da66056e3b238be3407751a43b365e11b +size 71198 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif new file mode 100644 index 0000000000..6c713186f4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d47400e426792c550afb2797b68e41e3ce04409dde627e0f74c39f216a951f22 +size 83675 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif new file mode 100644 index 0000000000..89a8e3880a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d11ae15a30005e335d2f84f2749573fc59d88d2f1c415cfea2126b47e3dac60b +size 91647 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif new file mode 100644 index 0000000000..f7ef065348 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6410a7792823ad1c5d5f0fcaed388b362498f1bbd57175b70165e0ce3542662c +size 99183 From b65abe7541146b8ead559be1d6f3a248734ff296 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 12 Mar 2025 23:03:36 +1000 Subject: [PATCH 58/73] Add options for color lookup. --- src/ImageSharp/ImageSharp.csproj | 7 + .../PaletteDitherProcessor{TPixel}.cs | 6 +- .../Quantization/ColorMatchingMode.cs | 28 ++ .../EuclideanPixelMap{TPixel,TCache}.cs | 219 +++++++++ .../Quantization/EuclideanPixelMap{TPixel}.cs | 438 ------------------ .../Quantization/IColorIndexCache.cs | 377 +++++++++++++++ .../Quantization/OctreeQuantizer{TPixel}.cs | 4 +- .../Quantization/PaletteQuantizer{TPixel}.cs | 4 +- .../Quantization/QuantizerOptions.cs | 5 + .../Quantization/WuQuantizer{TPixel}.cs | 4 +- src/ImageSharp/Properties/AssemblyInfo.cs | 9 - .../Codecs/Gif/EncodeGif.cs | 2 +- 12 files changed, 646 insertions(+), 457 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Quantization/ColorMatchingMode.cs create mode 100644 src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel,TCache}.cs delete mode 100644 src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs create mode 100644 src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs delete mode 100644 src/ImageSharp/Properties/AssemblyInfo.cs diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index b08c27c41b..24b880a770 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -13,6 +13,13 @@ Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET Debug;Release + + + 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 59/73] 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 From 71357d2d2ec6ab47d6cea9e0d7fd36db22076eb9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Mar 2025 14:27:28 +1000 Subject: [PATCH 60/73] Fix PNG encoder blending. --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 12 +++++++++++- .../ImageSharp.Tests/Formats/Png/PngEncoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/issues/Issue_2882.png | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Png/issues/Issue_2882.png diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index db6a1ffc80..6f0b91c9c7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -272,7 +272,17 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; frameMetadata = GetPngFrameMetadata(currentFrame); - bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over; + + // Determine whether to blend the current frame over the existing canvas. + // Blending is applied only when the blend method is 'Over' (source-over blending) + // and when the frame's disposal method is not 'RestoreToPrevious', which indicates that + // the frame should not permanently alter the canvas. + bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over + && frameMetadata.DisposalMethod != PngDisposalMethod.RestoreToPrevious; + + // Establish the background color for the current frame. + // If the disposal method is 'RestoreToBackground', use the predefined background color; + // otherwise, use transparent, as no explicit background restoration is needed. Color background = frameMetadata.DisposalMethod == PngDisposalMethod.RestoreToBackground ? this.backgroundColor.Value : Color.Transparent; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 57c8ff667c..528cb42391 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -449,6 +449,7 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color [WithFile(TestImages.Png.APng, PixelTypes.Rgba32)] [WithFile(TestImages.Png.DefaultNotAnimated, PixelTypes.Rgba32)] [WithFile(TestImages.Png.FrameOffset, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Issue2882, PixelTypes.Rgba32)] public void Encode_APng(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 575da53219..0974bab6dd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -76,6 +76,7 @@ public static class Png public const string FrameOffset = "Png/animated/frame-offset.png"; public const string DefaultNotAnimated = "Png/animated/default-not-animated.png"; public const string Issue2666 = "Png/issues/Issue_2666.png"; + public const string Issue2882 = "Png/issues/Issue_2882.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/issues/Issue_2882.png b/tests/Images/Input/Png/issues/Issue_2882.png new file mode 100644 index 0000000000..2d7a51dacb --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_2882.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cebc98e62bcfe31df73ae7b6980382f4b56bdf7e7e6e9037946f5a84cb51c7d2 +size 1117 From 4ace8140b396508f973d8bd379902b9d5c10d1c1 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Wed, 26 Mar 2025 00:26:39 +0100 Subject: [PATCH 61/73] extend gif benchmarks --- .../Codecs/Gif/DecodeEncodeGif.cs | 59 +++++++++++++++++++ .../Codecs/Gif/EncodeGif.cs | 26 +++++--- 2 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Gif/DecodeEncodeGif.cs diff --git a/tests/ImageSharp.Benchmarks/Codecs/Gif/DecodeEncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/Gif/DecodeEncodeGif.cs new file mode 100644 index 0000000000..06b07c3187 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Gif/DecodeEncodeGif.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing.Imaging; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using SixLabors.ImageSharp.Tests; +using SDImage = System.Drawing.Image; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs; + +public abstract class DecodeEncodeGif +{ + private Stream outputStream; + + protected abstract GifEncoder Encoder { get; } + + [Params(TestImages.Gif.Leo, TestImages.Gif.Cheers)] + public string TestImage { get; set; } + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [GlobalSetup] + public void Setup() => this.outputStream = new MemoryStream(); + + [GlobalCleanup] + public void Cleanup() => this.outputStream.Close(); + + [Benchmark(Baseline = true)] + public void SystemDrawing() + { + this.outputStream.Position = 0; + using SDImage image = SDImage.FromFile(this.TestImageFullPath); + image.Save(this.outputStream, ImageFormat.Gif); + } + + [Benchmark] + public void ImageSharp() + { + this.outputStream.Position = 0; + using Image image = Image.Load(this.TestImageFullPath); + image.SaveAsGif(this.outputStream, this.Encoder); + } +} + +public class DecodeEncodeGif_DefaultEncoder : DecodeEncodeGif +{ + protected override GifEncoder Encoder => new(); +} + +public class DecodeEncodeGif_CoarsePaletteEncoder : DecodeEncodeGif +{ + protected override GifEncoder Encoder => new() + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4, ColorMatchingMode = ColorMatchingMode.Coarse }) + }; +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs index 23a39a6720..d11cb3218b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Gif/EncodeGif.cs @@ -12,21 +12,16 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs; -[Config(typeof(Config.ShortMultiFramework))] -public class EncodeGif +public abstract class EncodeGif { // System.Drawing needs this. private Stream bmpStream; private SDImage bmpDrawing; private Image bmpCore; - // 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, ColorMatchingMode = ColorMatchingMode.Coarse }) - }; + protected abstract GifEncoder Encoder { get; } - [Params(TestImages.Bmp.Car, TestImages.Png.Rgb48Bpp)] + [Params(TestImages.Gif.Leo, TestImages.Gif.Cheers)] public string TestImage { get; set; } [GlobalSetup] @@ -61,6 +56,19 @@ public void GifSystemDrawing() public void GifImageSharp() { using var memoryStream = new MemoryStream(); - this.bmpCore.SaveAsGif(memoryStream, this.encoder); + this.bmpCore.SaveAsGif(memoryStream, this.Encoder); } } + +public class EncodeGif_DefaultEncoder : EncodeGif +{ + protected override GifEncoder Encoder => new(); +} + +public class EncodeGif_CoarsePaletteEncoder : EncodeGif +{ + protected override GifEncoder Encoder => new() + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4, ColorMatchingMode = ColorMatchingMode.Coarse }) + }; +} From 0ea2404eeb4d70cd8c371b8cb2b7e461eb5f7ccb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 26 Mar 2025 11:26:22 +1000 Subject: [PATCH 62/73] Revert to coarse cache default, additional feedback. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 15 +++++++++++++++ .../PaletteDitherProcessor{TPixel}.cs | 2 +- .../Quantization/IColorIndexCache.cs | 18 +++++++++--------- .../Quantization/QuantizerOptions.cs | 3 ++- .../Formats/Png/PngEncoderTests.cs | 8 ++++++-- ...ode_8BitColor_WithOctreeQuantizer_rgb32.bmp | 2 +- .../Encode_8BitColor_WithWuQuantizer_rgb32.bmp | 2 +- ...ionFilterInBox_Rgba32_CalliphoraPartial.png | 4 ++-- ...orksWithAllErrorDiffusers_Bike_Atkinson.png | 4 ++-- ...r_WorksWithAllErrorDiffusers_Bike_Burks.png | 4 ++-- ...thAllErrorDiffusers_Bike_FloydSteinberg.png | 4 ++-- ...llErrorDiffusers_Bike_JarvisJudiceNinke.png | 4 ++-- ...WorksWithAllErrorDiffusers_Bike_Sierra2.png | 4 ++-- ...WorksWithAllErrorDiffusers_Bike_Sierra3.png | 4 ++-- ...ksWithAllErrorDiffusers_Bike_SierraLite.png | 4 ++-- ...ithAllErrorDiffusers_Bike_StevensonArce.png | 4 ++-- ..._WorksWithAllErrorDiffusers_Bike_Stucki.png | 4 ++-- ...rorDiffusers_CalliphoraPartial_Atkinson.png | 4 ++-- ...lErrorDiffusers_CalliphoraPartial_Burks.png | 4 ++-- ...fusers_CalliphoraPartial_FloydSteinberg.png | 4 ++-- ...ers_CalliphoraPartial_JarvisJudiceNinke.png | 4 ++-- ...rrorDiffusers_CalliphoraPartial_Sierra2.png | 4 ++-- ...rrorDiffusers_CalliphoraPartial_Sierra3.png | 4 ++-- ...rDiffusers_CalliphoraPartial_SierraLite.png | 4 ++-- ...ffusers_CalliphoraPartial_StevensonArce.png | 4 ++-- ...ErrorDiffusers_CalliphoraPartial_Stucki.png | 4 ++-- ...tDependOnSinglePixelType_Bgra32_filter0.png | 4 ++-- ...otDependOnSinglePixelType_Rgb24_filter0.png | 4 ++-- ...tDependOnSinglePixelType_Rgba32_filter0.png | 4 ++-- ...endOnSinglePixelType_RgbaVector_filter0.png | 4 ++-- ...r_WorksWithAllDitherers_Bike_Bayer16x16.png | 4 ++-- ...ter_WorksWithAllDitherers_Bike_Bayer2x2.png | 4 ++-- ...ter_WorksWithAllDitherers_Bike_Bayer4x4.png | 4 ++-- ...ter_WorksWithAllDitherers_Bike_Bayer8x8.png | 4 ++-- ...r_WorksWithAllDitherers_Bike_Ordered3x3.png | 4 ++-- ...lDitherers_CalliphoraPartial_Bayer16x16.png | 4 ++-- ...AllDitherers_CalliphoraPartial_Bayer2x2.png | 4 ++-- ...AllDitherers_CalliphoraPartial_Bayer4x4.png | 4 ++-- ...AllDitherers_CalliphoraPartial_Bayer8x8.png | 4 ++-- ...lDitherers_CalliphoraPartial_Ordered3x3.png | 4 ++-- .../00.gif | 4 ++-- .../08.gif | 4 ++-- .../104.gif | 4 ++-- .../112.gif | 4 ++-- .../16.gif | 4 ++-- .../24.gif | 4 ++-- .../32.gif | 4 ++-- .../40.gif | 4 ++-- .../48.gif | 4 ++-- .../56.gif | 4 ++-- .../64.gif | 4 ++-- .../72.gif | 4 ++-- .../80.gif | 4 ++-- .../88.gif | 4 ++-- .../96.gif | 4 ++-- ...ized_Encode_Artifacts_Rgba32_issue_2469.png | 4 ++-- ...uantized_Encode_Alpha_Rgba32_Issue_2668.png | 4 ++-- ...nInBox_Bike_OctreeQuantizer_ErrorDither.png | 4 ++-- ...nBox_Bike_OctreeQuantizer_OrderedDither.png | 4 ++-- ...ike_WebSafePaletteQuantizer_ErrorDither.png | 4 ++-- ...x_Bike_WebSafePaletteQuantizer_NoDither.png | 4 ++-- ...e_WebSafePaletteQuantizer_OrderedDither.png | 4 ++-- ...Bike_WernerPaletteQuantizer_ErrorDither.png | 4 ++-- ...ox_Bike_WernerPaletteQuantizer_NoDither.png | 4 ++-- ...ke_WernerPaletteQuantizer_OrderedDither.png | 4 ++-- ...ationInBox_Bike_WuQuantizer_ErrorDither.png | 4 ++-- ...ionInBox_Bike_WuQuantizer_OrderedDither.png | 4 ++-- ...horaPartial_OctreeQuantizer_ErrorDither.png | 4 ++-- ...liphoraPartial_OctreeQuantizer_NoDither.png | 4 ++-- ...raPartial_OctreeQuantizer_OrderedDither.png | 4 ++-- ...ial_WebSafePaletteQuantizer_ErrorDither.png | 4 ++-- ...artial_WebSafePaletteQuantizer_NoDither.png | 4 ++-- ...l_WebSafePaletteQuantizer_OrderedDither.png | 4 ++-- ...tial_WernerPaletteQuantizer_ErrorDither.png | 4 ++-- ...Partial_WernerPaletteQuantizer_NoDither.png | 4 ++-- ...al_WernerPaletteQuantizer_OrderedDither.png | 4 ++-- ...lliphoraPartial_WuQuantizer_ErrorDither.png | 4 ++-- ..._CalliphoraPartial_WuQuantizer_NoDither.png | 4 ++-- ...iphoraPartial_WuQuantizer_OrderedDither.png | 4 ++-- ..._david_OctreeQuantizer_ErrorDither_0.25.png | 4 ++-- ...e_david_OctreeQuantizer_ErrorDither_0.5.png | 4 ++-- ..._david_OctreeQuantizer_ErrorDither_0.75.png | 4 ++-- ...ale_david_OctreeQuantizer_ErrorDither_0.png | 4 ++-- ...ale_david_OctreeQuantizer_ErrorDither_1.png | 4 ++-- ...avid_OctreeQuantizer_OrderedDither_0.25.png | 4 ++-- ...david_OctreeQuantizer_OrderedDither_0.5.png | 4 ++-- ...avid_OctreeQuantizer_OrderedDither_0.75.png | 4 ++-- ...e_david_OctreeQuantizer_OrderedDither_0.png | 4 ++-- ...e_david_OctreeQuantizer_OrderedDither_1.png | 4 ++-- ...ebSafePaletteQuantizer_ErrorDither_0.25.png | 4 ++-- ...WebSafePaletteQuantizer_ErrorDither_0.5.png | 4 ++-- ...ebSafePaletteQuantizer_ErrorDither_0.75.png | 4 ++-- ...d_WebSafePaletteQuantizer_ErrorDither_0.png | 4 ++-- ...d_WebSafePaletteQuantizer_ErrorDither_1.png | 4 ++-- ...SafePaletteQuantizer_OrderedDither_0.25.png | 4 ++-- ...bSafePaletteQuantizer_OrderedDither_0.5.png | 4 ++-- ...SafePaletteQuantizer_OrderedDither_0.75.png | 4 ++-- ...WebSafePaletteQuantizer_OrderedDither_0.png | 4 ++-- ...WebSafePaletteQuantizer_OrderedDither_1.png | 4 ++-- ...WernerPaletteQuantizer_ErrorDither_0.25.png | 4 ++-- ..._WernerPaletteQuantizer_ErrorDither_0.5.png | 4 ++-- ...WernerPaletteQuantizer_ErrorDither_0.75.png | 4 ++-- ...id_WernerPaletteQuantizer_ErrorDither_0.png | 4 ++-- ...id_WernerPaletteQuantizer_ErrorDither_1.png | 4 ++-- ...rnerPaletteQuantizer_OrderedDither_0.25.png | 4 ++-- ...ernerPaletteQuantizer_OrderedDither_0.5.png | 4 ++-- ...rnerPaletteQuantizer_OrderedDither_0.75.png | 4 ++-- ..._WernerPaletteQuantizer_OrderedDither_0.png | 4 ++-- ..._WernerPaletteQuantizer_OrderedDither_1.png | 4 ++-- ...cale_david_WuQuantizer_ErrorDither_0.25.png | 4 ++-- ...Scale_david_WuQuantizer_ErrorDither_0.5.png | 4 ++-- ...cale_david_WuQuantizer_ErrorDither_0.75.png | 4 ++-- ...ngScale_david_WuQuantizer_ErrorDither_0.png | 4 ++-- ...ngScale_david_WuQuantizer_ErrorDither_1.png | 4 ++-- ...le_david_WuQuantizer_OrderedDither_0.25.png | 4 ++-- ...ale_david_WuQuantizer_OrderedDither_0.5.png | 4 ++-- ...le_david_WuQuantizer_OrderedDither_0.75.png | 4 ++-- ...Scale_david_WuQuantizer_OrderedDither_0.png | 4 ++-- ...Scale_david_WuQuantizer_OrderedDither_1.png | 4 ++-- ...zation_Bike_OctreeQuantizer_ErrorDither.png | 4 ++-- ...ntization_Bike_OctreeQuantizer_NoDither.png | 4 ++-- ...tion_Bike_OctreeQuantizer_OrderedDither.png | 4 ++-- ...ike_WebSafePaletteQuantizer_ErrorDither.png | 4 ++-- ...n_Bike_WebSafePaletteQuantizer_NoDither.png | 4 ++-- ...e_WebSafePaletteQuantizer_OrderedDither.png | 4 ++-- ...Bike_WernerPaletteQuantizer_ErrorDither.png | 4 ++-- ...on_Bike_WernerPaletteQuantizer_NoDither.png | 4 ++-- ...ke_WernerPaletteQuantizer_OrderedDither.png | 4 ++-- ...antization_Bike_WuQuantizer_ErrorDither.png | 4 ++-- ...yQuantization_Bike_WuQuantizer_NoDither.png | 4 ++-- ...tization_Bike_WuQuantizer_OrderedDither.png | 4 ++-- ...horaPartial_OctreeQuantizer_ErrorDither.png | 4 ++-- ...liphoraPartial_OctreeQuantizer_NoDither.png | 4 ++-- ...raPartial_OctreeQuantizer_OrderedDither.png | 4 ++-- ...ial_WebSafePaletteQuantizer_ErrorDither.png | 4 ++-- ...artial_WebSafePaletteQuantizer_NoDither.png | 4 ++-- ...l_WebSafePaletteQuantizer_OrderedDither.png | 4 ++-- ...tial_WernerPaletteQuantizer_ErrorDither.png | 4 ++-- ...Partial_WernerPaletteQuantizer_NoDither.png | 4 ++-- ...al_WernerPaletteQuantizer_OrderedDither.png | 4 ++-- ...lliphoraPartial_WuQuantizer_ErrorDither.png | 4 ++-- ..._CalliphoraPartial_WuQuantizer_NoDither.png | 4 ++-- ...iphoraPartial_WuQuantizer_OrderedDither.png | 4 ++-- 143 files changed, 307 insertions(+), 287 deletions(-) diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 0d5faabe18..c17aa76285 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -138,6 +138,7 @@ private static void Seed() AotCompileResamplers(); AotCompileQuantizers(); AotCompilePixelSamplingStrategys(); + AotCompilePixelMaps(); AotCompileDithers(); AotCompileMemoryManagers(); @@ -514,6 +515,20 @@ private static void AotCompilePixelSamplingStrategys() default(ExtensivePixelSamplingStrategy).EnumeratePixelRegions(default(ImageFrame)); } + /// + /// This method pre-seeds the all in the AoT compiler. + /// + /// The pixel format. + [Preserve] + private static void AotCompilePixelMaps() + where TPixel : unmanaged, IPixel + { + default(EuclideanPixelMap).GetClosestColor(default, out _); + default(EuclideanPixelMap).GetClosestColor(default, out _); + default(EuclideanPixelMap).GetClosestColor(default, out _); + default(EuclideanPixelMap).GetClosestColor(default, out _); + } + /// /// This method pre-seeds the all in the AoT compiler. /// diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 067ae69fdf..94a23ef5fe 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -89,7 +89,7 @@ public DitherProcessor( float ditherScale) { this.Configuration = configuration; - this.pixelMap = PixelMapFactory.Create(configuration, palette, ColorMatchingMode.Hybrid); + this.pixelMap = PixelMapFactory.Create(configuration, palette, ColorMatchingMode.Coarse); this.Palette = palette; this.DitherScale = ditherScale; } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs index 6234e2e63e..eabe9af850 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IColorIndexCache.cs @@ -74,13 +74,13 @@ internal interface IColorIndexCache : IDisposable /// internal unsafe struct HybridCache : IColorIndexCache { - private ExactCache exactCache; + private AccurateCache accurateCache; private CoarseCache coarseCache; [RequiresPreviewFeatures] private HybridCache(MemoryAllocator allocator) { - this.exactCache = ExactCache.Create(allocator); + this.accurateCache = AccurateCache.Create(allocator); this.coarseCache = CoarseCache.Create(allocator); } @@ -90,7 +90,7 @@ private HybridCache(MemoryAllocator allocator) [MethodImpl(InliningOptions.ShortMethod)] public bool TryAdd(Rgba32 color, short index) { - if (this.exactCache.TryAdd(color, index)) + if (this.accurateCache.TryAdd(color, index)) { return true; } @@ -101,7 +101,7 @@ public bool TryAdd(Rgba32 color, short index) [MethodImpl(InliningOptions.ShortMethod)] public bool TryGetValue(Rgba32 color, out short value) { - if (this.exactCache.TryGetValue(color, out value)) + if (this.accurateCache.TryGetValue(color, out value)) { return true; } @@ -111,13 +111,13 @@ public bool TryGetValue(Rgba32 color, out short value) public void Clear() { - this.exactCache.Clear(); + this.accurateCache.Clear(); this.coarseCache.Clear(); } public void Dispose() { - this.exactCache.Dispose(); + this.accurateCache.Dispose(); this.coarseCache.Dispose(); } } @@ -209,7 +209,7 @@ public void Dispose() /// 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 +internal unsafe struct AccurateCache : IColorIndexCache { // Buckets array: each bucket holds the index (0-based) into the entries array // of the first entry in the chain, or -1 if empty. @@ -226,7 +226,7 @@ internal unsafe struct ExactCache : IColorIndexCache public const int Capacity = 512; - private ExactCache(MemoryAllocator allocator) + private AccurateCache(MemoryAllocator allocator) { this.count = 0; @@ -244,7 +244,7 @@ private ExactCache(MemoryAllocator allocator) } [RequiresPreviewFeatures] - public static ExactCache Create(MemoryAllocator allocator) => new(allocator); + public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); public bool TryAdd(Rgba32 color, short value) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs index 2d6cc49db7..da64b26d66 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -41,6 +41,7 @@ public int MaxColors /// /// Gets or sets the color matching mode used for matching pixel values to palette colors. + /// Defaults to . /// - public ColorMatchingMode ColorMatchingMode { get; set; } = ColorMatchingMode.Hybrid; + public ColorMatchingMode ColorMatchingMode { get; set; } = ColorMatchingMode.Coarse; } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 528cb42391..ad58f12b27 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -485,7 +485,7 @@ public void Encode_APng(TestImageProvider provider) } [Theory] - [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.7629F)] + [WithFile(TestImages.Gif.Leo, PixelTypes.Rgba32, 0.7921F)] [WithFile(TestImages.Gif.Issues.Issue2866, PixelTypes.Rgba32, 1.06F)] public void Encode_AnimatedFormatTransform_FromGif(TestImageProvider provider, float percentage) where TPixel : unmanaged, IPixel @@ -718,11 +718,15 @@ public void Issue_2862() } using MemoryStream ms = new(); + PaletteQuantizer quantizer = new( + palette.Select(Color.FromPixel).ToArray(), + new QuantizerOptions() { ColorMatchingMode = ColorMatchingMode.Hybrid}); + image.Save(ms, new PngEncoder { ColorType = PngColorType.Palette, BitDepth = PngBitDepth.Bit8, - Quantizer = new PaletteQuantizer(palette.Select(Color.FromPixel).ToArray()) + Quantizer = quantizer }); ms.Position = 0; 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 6ce22169e2..2b8e05b070 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:6785d108cab9e3b508e98ce10cef383037471beb2a6d14a69df6069b5fbf5f3d +oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d 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 c69ed3130c..f7eb06c558 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:5d9f2745de2b6e7fc3b1403fe651f3bbba835c67a6fb410fc8a9d91a15b44328 +oid sha256:e063e97cd8a000de6830adcc3961a7dc41785d40cd4d83af10ca38d96e071362 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 401ceaa36c..de42d1bfc2 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:596472e74050d968479b672c1d2436b179e41a7b99fcefb53286ad47e5a4fe13 -size 273115 +oid sha256:681b0e36298cb702683fb9ffb2a82f7dfd9080b268db19a03f413809f69d0e07 +size 273269 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 0b03e4e185..1656b2e9cb 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:2465dde9a5d6202194f7af3924ca24ab3151948d551549a711977d3302dbc0a3 -size 51158 +oid sha256:38597c6144d61960d25c74d7a465b1cdf69b7c0804a6dec68128a6c953258313 +size 52688 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 1d27fab771..c6016ae358 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:82dcdd4f28a9ffafd36a21d06aee8adb49017df2d4abeee4205d65b1ae3df35e -size 59875 +oid sha256:5f9191c71eea1f73aa4c55397ca26f240615c9c4a7fff9a05e6f2e046b5e4d8b +size 62323 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 51c93894de..40243937d3 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:a836c8efd7aa9818cf807cf56412e78399a6568798be23d0f3f6b89552856ff1 -size 62172 +oid sha256:b63810145832db459bb7a6b37a028a7b778f6b6b4e6eae00e50e6e21c5a06086 +size 62199 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 de9ef47037..83f9e067db 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:78900d779181140a02a2b9fb9fa922ca854d9905c1dc7e006592a3fdc00f8dee -size 58107 +oid sha256:a67c14ef99a943706f050ff1ea0ef101429292d52bc14ed4610f8338736ff87e +size 56800 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 bb62475c97..22e4f4b6d6 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:76f10d4280258d2941d85e795cf788977ca1e85bdc1b75b5a482b5bbdaa49d32 -size 57900 +oid sha256:623dd82d372ba517b0d3357d06cffaf105d407a9090cbcbc6a76ae944ab33d67 +size 59468 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 2d99410117..838863c158 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:aba9172bb4d117ba1b0c5f32b46251d473cc06b3f697e5729da0c5768a70b5d2 -size 59104 +oid sha256:8edceef8e12c4f3d194523437045c5cf4e80c7bb95ff75f38c1f38a21872e3d0 +size 59376 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 bd3295e424..60513e1992 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:e7d6ea824ba19632afa940b3062632d305bf3521b1795d46f3fea90abc1f0ed8 -size 64431 +oid sha256:b1d7019e8cb170ae67496f8250446c4f6b6217378658408c3d51a95c49a4c3bc +size 63287 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 6bea03c0be..0d1b34d8ce 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:3efcf6f924d3d07cad9dbf9dddb6104c3748ac4354298acf5afde66c2321e819 -size 55358 +oid sha256:d7c03ede7ab3bd4e57e6a63e53e2e8c771e938fdc7d5dfe5c9339a2c9907c9cf +size 55550 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 b535b14155..f8c998ecbd 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:2b9f295f6b539fbeeae3c473907fa450f9b8c94017abad4bf915a8a4a2e7b612 -size 56982 +oid sha256:79b690b91223d1fe7ddf1b8826b4474b89644822bc8aa9adee3cf819bc095b4c +size 60979 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 e0334be2d5..70acb3f32e 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:c4c45632b6cd387c929a9e0982f3943a7c3f64f27862c0b539bbf71228561f39 -size 57886 +oid sha256:e44c49a8f2ab1280c38e6ba71da29a93803b2aa4cf117e1e919909521b0373e6 +size 57636 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 db22e17585..af35177491 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:bf9e8bd50b62ba62ab04a5ab2af207414183a015567080fa7cdd827016694369 -size 60458 +oid sha256:359a44bb957481c85d5acd65559b43ffc0acf806d4f4e57d6a791ca65b28295b +size 59839 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 2d6226d096..a14c2cb1f6 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:89864a77216b51cc5b9415453ade7f7ec64c1c112546aa47ee6b4b89f9b258a3 -size 60543 +oid sha256:7fb3743098a8147fd24294d933d93a61ec0155d754f52544650f6589719905be +size 60688 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 4d9b22139e..683f59ea1e 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:0c03c3dc0b3da69ef4f55b5ad6d162da94ad46f4e426e318695bedc7e5bb3dfd -size 58725 +oid sha256:41fa7d92a10db450f3b3729ab9e36074224baaefeda21cffd0466e37a111e138 +size 59113 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 3b7c78182a..813289a26d 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:af86b108639f833972958fd2cc7d00221982069c40cab67b5bc6b8ce1a7e826d -size 59137 +oid sha256:bebf3b3762b339874891e3d434511e5f2557be90d66d6d7fe827b50334ede6c2 +size 58976 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 b56e3ccfd6..d4da100376 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:959b49f5498e4018bfb8a5fac8a688c51b06161dc0c6559547293c613ddca760 -size 59248 +oid sha256:fd4358826739db2c22064e8aa90597f8b6403b9d7e2866ec280e743c51d2f41f +size 59203 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 78078ac90d..8d3cf1a564 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:fdc28c281666e381c7ba2483d033f73c88111f13eec10cc406e07730eb5fa709 -size 60804 +oid sha256:6c88740c0553829eaa42ca751b34cc456623a84ccdff4020949a06ef4b4802d1 +size 61137 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 523a87b7a5..a146f8f668 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:c80f215d4a839fb1ca722d03923b587bac6326d54d2d7a3656667e46464b4307 -size 58011 +oid sha256:0a4a404b0767faac952435f768867cf7bf053848e1e3ef121624f136658a107c +size 58386 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 741ac096a3..fa8eea57a9 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:38fbfc201e8ef31b879e863f7f49ac1e731c4d7dfca58a80e1e45890565af979 -size 58742 +oid sha256:174ee39c08eb9a174b48b19dc618d043bf6b71eee68ab7127407eb713e164e61 +size 58934 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 91ff81d5df..2c67b3bf23 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:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc -size 875 +oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 +size 870 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 91ff81d5df..1305c5ede9 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:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc -size 875 +oid sha256:e51abcab66201997deda99637de604330ef977fd2d1dbebaa0416c621d03b8f9 +size 869 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 91ff81d5df..2c67b3bf23 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:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc -size 875 +oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 +size 870 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 91ff81d5df..2c67b3bf23 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:a9b0209e8bae05da6de72a4249d2fe43ef08388c7296556921c17b11bdb8bdcc -size 875 +oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 +size 870 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 537b367eaa..e899ffb42a 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:f22d61e15a46b5ff3a01b1eed3cf7b6ee4970a86b56a66e5a44914962946792c -size 44381 +oid sha256:ca70bb0200776efd00c4ef7596d4e1f2f5fbc68e447b395b25ef2b3c732e5156 +size 44189 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 f623d52ac7..543640c2e8 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:ea34b188ce71a8fbd76fddf052fc1322fff62ba0acc218582b996d9b00c81671 -size 42667 +oid sha256:8474b847b7d4a8f3e5c9793ca257ce46efcf49c473c731a9ca9c759851410b94 +size 43066 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 120b569fdb..fec3c9b2b3 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:dc335434b535cc26bf16f7233bdb4606ebff3b95ae3e900e0e4b2dffcae652ca -size 44159 +oid sha256:20e80e7d9e68fd85bfbc63c61953327354b0634000ec142e01a42618995fd14c +size 44391 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 0386f0e33f..68a95a0540 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:be3277c05d05314653976167612ef2bb5f19f8cf3159eca5122878da0f0c0fcd -size 44391 +oid sha256:8af98bfcc5edef3f3ff33ee8f76f33ce2906a6677167e2b29e1dbe63b00a78d8 +size 44202 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 38b9f87df6..d67f02dca5 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:9dbd000634c3aad9b31166ef070f337fc3aa2ff13a9afea88e2a4d880280b960 -size 45061 +oid sha256:b149ebbd550808ae46ff05b5ddcdb1fc0eb6ae0eacbe048e9a1ff24368d8f64d +size 45003 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 62ebd483b8..da1f62b728 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:b58e70ae7f38d9f25fb701b9ab5d01a20ef61504dc11f3cf4e1d232a2e09e96e -size 51204 +oid sha256:eb86f2037a0aff48a84c0161f22eb2e2495daadbfa9c33185ddfd7b8429a4ea9 +size 51266 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 84176ad9b3..11d916bdc3 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:968bba323acfabd9b1b02001e5b37047f6ab7fb7dae8c781eed2f84771beb9c9 -size 52812 +oid sha256:08c39a43993deadebab21f1d3504027b5910a52adc437c167d77d62e5f5db46e +size 52762 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 5d05f354ee..a4f91b3301 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:9e3102436373a8238c2156caeb1b6479fc4651c0018c28c148bb3965b36fe13a -size 51779 +oid sha256:0c9c47fa755d603f8c148011511ee91f32444e0d94367f9db57593e3bf30f2e0 +size 51808 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 82ba28be09..03848e81ce 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:1366d7c4e9770bceed30aecd894b6e947f822f8dd74f5d231f013167c43ad3c4 -size 51553 +oid sha256:ef033a419e2e1b06b57a66175bad9068f71ae4c862a66c5734f65cdaae8a27f0 +size 51461 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 b03713b402..9a7c7b4611 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:3bd7d03172aa7f70afe5122ef5e582dcc370523db15661455cf512119be67b7f -size 52226 +oid sha256:366e84ab8587735455798651096d2af5f965fc325f4852dc68356e94600598b1 +size 52176 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif index 8668f7102a..b219975ade 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/00.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e738470c04121376b42937f0a72eed3f068bc32bdc788cc0d5877bc7f705f419 -size 67611 +oid sha256:cb4bbef09dc6618380e34c5dcf8612fa5a51ba81a09edc5500be9191f0554d9c +size 49665 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif index 8fd66d1206..2d50761636 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/08.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33b9244a09ed3b7bb17dfcbb694cbacc4f7de8ce6ffd7d7f7c35d424a6e8b2c5 -size 58696 +oid sha256:81a0d629326bb39cfced1a261542e5f94b423527f95bc45422670091b91583b4 +size 50730 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif index 90b8a081ce..b1b7781a21 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/104.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:380f86015751a8445eb706d99001efe96633c62dd183c22303e732d036175dff -size 86095 +oid sha256:e64d9f2f7a8346f62c9b41a14b3e6b71f76a48e07fa42ac9e0d4a5b146a8a9da +size 58856 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif index 180c1e730b..f058764b4f 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/112.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae0f80bbc03fc35be4622df85f13f8fe7aec2d981424d9ca6dce88bd23ff2dad -size 59015 +oid sha256:b01517c53b19f6b151a76cc75142ba3a8a45da8c6e94416447703cbd54ce1a8a +size 48282 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif index 6eceee3979..b9f1e2d099 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/16.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:696ef2d76fe8ae8a32f0299b668b0cf537c1ffe5ac3ca4b418499f03ca134d89 -size 70345 +oid sha256:1db212159613778c962883de9067852da3bea5f3483dd9f967c0aabbcdc1b2f6 +size 64655 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif index 51ef668112..c7a1368ce1 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/24.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dbd382f6f7b71f4cdffe61c01f0ab04523423576bad6195f8950ff7758792a2 -size 67153 +oid sha256:9d09ddfff1f26ed7842df5bf4b8938373700658322c85b154a878dd5e3a90dc1 +size 64432 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif index 26818c6a8f..ffd61e5123 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/32.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2942af218c05b29b4f5a8eb9117c8dea4dcf61b33e1ade3cfa6ba47a0d71f6c -size 68007 +oid sha256:18ca31ff631ecc33fe33a893e94e23af8b086a78c3684461e449c02800fffb2b +size 66510 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif index 92f5c1fb95..eb93ea4d4a 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/40.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45872e2361fc445d956abd5de454ad42bcdb8ade69742b032c865978e41cb477 -size 73531 +oid sha256:823358342cbc25a9f7ae34abc2669096acd7c0e0c93a8a0b371e548822ed0897 +size 66912 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif index 5524ce8a41..99f0e64dc6 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/48.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:432af0fd8df68724f2faa3fc90dc751ff15a7e45604e20f3737d1902c64a913e -size 72024 +oid sha256:90558311a7b7127d9f970a17ae0630d81507be246f511f1cc3b10c6ee953a25c +size 61986 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif index bfd22ffef5..8e6410f9bf 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/56.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aab6a8c7892d8449ad8915f414689019684f99d3f011ddb82f722037b35a70c0 -size 77189 +oid sha256:9514b736d946d4e93ba3f59b586d2c29e0c031155f7824756ecf468ef87ea8e6 +size 61367 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif index e3f17772eb..2257625c41 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/64.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b93da4e1ab6ed01c561d5b565c078ff8752f7b16b78c733b658237977b1e4689 -size 78172 +oid sha256:0334c551b9efcaa9f5c16c4599884b4aabe5129e3f023222be3214cf8623242e +size 60825 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif index cfe4a2eefe..efc9569f4d 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/72.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8b426203cd6b6b5f4b97a5213a8a04da66056e3b238be3407751a43b365e11b -size 71198 +oid sha256:32a9fecdad6508c1c6beae839717d1854cca1f7b247bff36a00a93cc953f608c +size 57370 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif index 6c713186f4..9f7ae53fb0 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/80.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d47400e426792c550afb2797b68e41e3ce04409dde627e0f74c39f216a951f22 -size 83675 +oid sha256:5d50d4ccba947ef95b9e8a2c4acd08f57c414f4e38a0d03d65b5fee093e4481a +size 67784 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif index 89a8e3880a..22dc30784c 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/88.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d11ae15a30005e335d2f84f2749573fc59d88d2f1c415cfea2126b47e3dac60b -size 91647 +oid sha256:840780f2916cb9d010a95802d9c123c3051bcee5dde7b173a50854e3b5f3636a +size 72552 diff --git a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif index f7ef065348..53e1a35cbf 100644 --- a/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif +++ b/tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue2866_Rgba32_issue_2866.gif/96.gif @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6410a7792823ad1c5d5f0fcaed388b362498f1bbd57175b70165e0ce3542662c -size 99183 +oid sha256:41db6ded3de84d43dec1175c1481f75a045c5ad126369e4e82ae29ec4bad0bc4 +size 76868 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 16f8c9e4a1..4c78303750 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:c4c4de6d55bea2684ff268ab98aa5c8ed3ea9b6fe8bc976b9116d947c661429e -size 930843 +oid sha256:1af50619f835b4470afac4553445176c121c3c9fa838dff937dcc56ae37941c3 +size 945821 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 8683948b8f..7af5391f70 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:9c32480edf4e42228593cd34ce4906823aeea2f85542c7f80a314e68200a49b0 -size 7706 +oid sha256:f934af128b85b9e8f557d71ac8b1f1473a0922d0754fc0c4ece0d0e3d8d94c39 +size 7702 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 d40e549366..4948c7adee 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:9a1c1376d76bbd1ee2f798ffbe368dc6e8dd8428a9f87289660e46eadc471935 -size 247122 +oid sha256:a51d04953c1c82d99884af62912d2271108c6bc62f18d4b32d0b5290c01fa7f7 +size 247462 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 bdaa1b2f3d..223d3bc012 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:a47fea2454050baa9105fcdf9992ed97dfaa1ca470c2a5b950566a94ff926be4 -size 265635 +oid sha256:34eaa0696da00838e591b2c48e7797641521f7f3feb01abbd774591c4dd6f200 +size 265546 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 0f3d653f43..922c2bf9b2 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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e -size 216246 +oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f +size 216030 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 0f3d653f43..922c2bf9b2 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:684cdf0f3f9d074e986b8b85b2c6c65da1f6f486c0eab727cc8a1c92b651fc9e -size 216246 +oid sha256:4f1462733e02d499b0d8c61ab835a27c7fee560fdc7fc521d20ec09bb4ccc80f +size 216030 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 58a94f3522..29c93d14e2 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:265fa873f693e2ffbfb049161927ca6a286509c5c8c8d700182d488876975535 -size 226823 +oid sha256:7e6d91a3ec4f974af675dc360fd5fd623ec8773cdbc88c0a3a6506880838718a +size 226727 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 ec95754094..dbfab2b508 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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853 -size 220689 +oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 +size 220192 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 ec95754094..dbfab2b508 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:7aa18d1a444a12c30003c533b411b018c83684dbe48fce07293f83401c44b853 -size 220689 +oid sha256:c68eba122814b5470e5f2e03e34190ff79e84e4b431ad8227355ce7ffcd4a6a7 +size 220192 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 05cb5f7bc8..86655af42b 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:fa84eb63b480314a5ec2439892547d25d4fda1414aac027e397aed4df5b8ea2d -size 231247 +oid sha256:6dbd3189b559941f91dd6e0aa15b34a3e5081477400678c2396c6a66d398876f +size 230883 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 52a297cdf6..82d5e5d592 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:14b8be6579cea0742be6ab1d8a44b7fc7f7acc26698692dbe445435f1fa2e48a -size 262707 +oid sha256:f4df5b1bc2c291ec1cf599580d198b447278412576ab998e099cc21110e82b3d +size 263152 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 c85f05eb07..34569dde9c 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:c344c9c5c0585fcde88c71d960dac7ab2867d3d321bfb769fc80e032bf0b940f -size 274578 +oid sha256:866e07dd8ce77bd0bbc7d3f8b1796522c0bde6bc8b124d09cbd7538ec0a639c4 +size 322987 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 ef32d86a0c..a923ec2cd9 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:2dc1e0677992a8c513d4a7353e66a1365f6b844b5e8957575c2686d0dd8439f9 -size 313198 +oid sha256:dadc8d9278ba52366013c702629007d6e4659c36c322cdbf0c12ff493fd35390 +size 343004 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png index 7e3080562c..b8ba5f90be 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_OctreeQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0203ecb9e4665e7c3992b7da4777c6d35b539790506fc9ca2acbcbc2bdb5db18 -size 303979 +oid sha256:e8a88406e3178ac652747bd9330e71f4499f0c21f13929a45f269ae82639c1dd +size 339554 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 ac940c161f..718315553f 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:e3d3a6474191e2a5ad8de6df6e985f99244c24e77cfbbe18448b9d26ebd9baa9 -size 320811 +oid sha256:12cac37dbfa66025ace953a70671919276845b9c4f214d0bcb0ede3fe8221f7a +size 345622 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 f33e66693b..f48fe72cd5 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:09d4a269df15a78b84c94a22788b0cdbe187852979ad59afff886a98a66282c4 -size 269323 +oid sha256:6277bf92b80ed8300ed3b05558c6166b8f042302a21faf9329ddd787263b5021 +size 319743 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 f33e66693b..f48fe72cd5 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:09d4a269df15a78b84c94a22788b0cdbe187852979ad59afff886a98a66282c4 -size 269323 +oid sha256:6277bf92b80ed8300ed3b05558c6166b8f042302a21faf9329ddd787263b5021 +size 319743 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 ec4d4c7346..0c03ab4b2e 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:2f60545a9a4c34b69ad78d15361b55f9e44ab2c20ab51d9bf8db6959d3d1aa10 -size 270815 +oid sha256:215a622fbc4abfb3a851b6f19bfeaad038c53d0977a229778cc8dfe9c2e96f59 +size 321716 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 9df2f13dbb..a96e40c460 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:ae0a724189324a60c9f68014f4c178add91aecf7dbe49a1ca2c01c10816927a4 -size 284288 +oid sha256:4901d9429171051abf02d768cae5b9e119a906dc167594324276729bebd14c45 +size 328223 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 9df2f13dbb..a96e40c460 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:ae0a724189324a60c9f68014f4c178add91aecf7dbe49a1ca2c01c10816927a4 -size 284288 +oid sha256:4901d9429171051abf02d768cae5b9e119a906dc167594324276729bebd14c45 +size 328223 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 dc55c1d35e..2366c801aa 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:0581c9cd38c34edb9835cf5a84f5245583c0f14cd25d3ff952986dd9411cff99 -size 285131 +oid sha256:c5acc1924f0f5ec95558b2467a8601860db66acd35b21d3cef6ac5e0be1ed863 +size 330305 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 64560574a6..6fd93f7c08 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:a446d9dba2cdcfd336847e6a475872a1e1c99f204a9aba7aed0e4da282e0e9dd -size 317073 +oid sha256:c4202f0494f6e57027d28989011245fd18001e67fcaec7bea001c6bbb3e12ff5 +size 347829 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png index d94d57759f..3f832fe0fe 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationInBox_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff094e6bafe81e818bcbac69018dcfe29366389dfca0d63d8e05ef42896ffe1d -size 317309 +oid sha256:72fae764eeba00aef04f7f7fecae499b10a922014cf02d0f616d2e697dd6da96 +size 347961 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 e3298ba162..0815e87ccd 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:4d30407a28495414d113e82abb60ef1762cbee8fddb667df14b08a1280640274 -size 323568 +oid sha256:5b48d6f935f0a8b32e19a6b59fa4975e626046f8e153cd4d7edf9976bdd243ec +size 350418 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 8b9efe3364..0127033b84 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:8c21b5a471a54cce860c20fc76660763dcc1392e9794241f70b428c9851063f9 +size 23591 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 8b9efe3364..98f801de72 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:e386c85d99fead8aa59f69ad48203d41a749b656b348d8c032e019654cec902f +size 24144 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 8b9efe3364..d9c8b88228 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:969805a2aba50ee121abc93042628c97b92c98ee4a1b9fd1922c46ec3519b45a +size 24567 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 8b9efe3364..0127033b84 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:8c21b5a471a54cce860c20fc76660763dcc1392e9794241f70b428c9851063f9 +size 23591 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 8b9efe3364..2ae2796af6 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:805f7326bdf0a27fd0112ae386edfc6995bee559de31d5186d029b763a4b3820 +size 24394 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 dc61aff866..5b5ca365fc 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:985e054407907131d536b9852c966a839dc4ca27d9d1851bae247852cda23605 -size 19028 +oid sha256:7bcff42e2de96bf0d9ab297d0e14aa7cb9ce5ba2f25755192445332ddc83302f +size 27169 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 8009275ae0..428977ca72 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:5cd6a1a5e9181e033bdfcc90d1029aaa15178087d95b6cbc97ec62dc96a7903e -size 20357 +oid sha256:d537d50e0d068d24773bd38926fef1fe2d79e26f1dfc772dd9d5e3e97c5c725c +size 29540 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 6387a35b31..5cb00bcd19 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:c8bfc279de7255a591e2647745a3edc5564cf6ac6eeedd376551ff70eaa8f669 -size 21289 +oid sha256:5701252c008c07b518276e83d9610b2b1ce62b48fa174eebe5d7f521e7d5735c +size 30346 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 8b9efe3364..0127033b84 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:88be683fabadd0d6812c208b878def4a64612712332c748bbbf05e793d9a65e1 -size 17790 +oid sha256:8c21b5a471a54cce860c20fc76660763dcc1392e9794241f70b428c9851063f9 +size 23591 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 69ca61d41c..817afbc51c 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:90fae259caa8e2af7c8d5a5b07afb3de6292acd7bb5a9a1ad058fc820dca1a4b -size 22343 +oid sha256:d277084637dbd8ee42dd960ac2f3e490b7ad6ced844a4e15413dbabefbb33306 +size 30985 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 93ebc8ac7e..8be74e8358 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:4f95a03b6167e1e5174b94cbd2d4c0df6fbcf74402abcf027fb51c048d6040ca -size 9236 +oid sha256:90f6ecde3e908c28b3beb792cbf94a929d8607e814aff2c4784eac0c068d3f5f +size 15528 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 9ff1175dfa..ba3831a4fe 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:3f30a9cabe172856d45e058b41adba1580e4aac2c5455738a0fe1222e0fdb313 -size 10128 +oid sha256:75297134242ab181dcc1bbe10818ba20ce6dbd269885f5033a71627b8ffca0e2 +size 15469 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 34257a5236..229c7ed515 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:28f444d388662a516e261dd7b5d28fc142b6089061ca9450a9990053884eee6d -size 10596 +oid sha256:455090d81b44e1c70b4099eb7617bd9937342443dafbf2eb54dea6689899f9cc +size 18576 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 170dd9482e..ee0eecb299 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:2e51a7ab6a24ad8466eb1c07bbd221a9b99cff2515304cd7d8b5577d7924c333 -size 8600 +oid sha256:290211f05249e28efc27f545792f4fd0b2812e996a8ab83dfb91a9fb7ee05a53 +size 13780 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 0523babb39..a69310beb4 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:1eb4268feb96c5fe5e53cf9a70dc50ff54a4d229f41a9ec7a005232f707ae395 -size 12835 +oid sha256:e6d677bdb93434b887462826b2c801a6b003280ca57d38022914619f12151fed +size 19657 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 5d6ec0c0fa..d2c00641f4 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:db9c3dee98f961011829fd5d1733c990016d8518684dea43bdc0ed8b46dc065b -size 9461 +oid sha256:c6cf0022a2b0d554dc245b07fd7cde253b2e972c0fce65543e02c601ffe53db1 +size 15570 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 a13f473352..76dacfa161 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:c49ae3b99cac4592c3aba6b1bd2613b7c2887419df84135facd9caab1c67c4f2 -size 9527 +oid sha256:c31a399f198d84b4eece814c6c7a43907e6697a8309e87448d7c5a9f0536012b +size 16504 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 1b4547c806..a9f5ccc93c 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:a0449b5b9a2471a6df694a1e36c8544e07d6a1ad01a78dc2de06ca1f9171a668 -size 9711 +oid sha256:7824b90b7074de18543d82d9f398590a32b331259bb544c7008af2fb1a92a2db +size 17617 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 170dd9482e..ee0eecb299 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:2e51a7ab6a24ad8466eb1c07bbd221a9b99cff2515304cd7d8b5577d7924c333 -size 8600 +oid sha256:290211f05249e28efc27f545792f4fd0b2812e996a8ab83dfb91a9fb7ee05a53 +size 13780 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 5f25693d28..cfe10c654c 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:fff6cd72098dbdbc5553e21e68a65813c4a938213fc92dab205968d0d5cba613 -size 9776 +oid sha256:3c2c275dcce8d2804dbfe38afc9855b3b7ced716958b1c74b404c4cdc638281e +size 17750 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 80c5b82ce0..66f642c0e8 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:88881839126c275a3d71278e4873cd05f4fe26eb7e1d1c2f5b29826d5bc60ef2 -size 11601 +oid sha256:08f7a1de549bf90cb478e040c9ea2d1a50d788ce5c861598e549a1bbeb96b75f +size 18353 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 dde458b3a9..4845de610f 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:bcd2b1732cfd93dc307c783fd71df4736a3e99ff318a32197e472b6202118582 -size 11457 +oid sha256:ae58e14acbbf57c58d029fb0252a8254bb466d8da14a1723a9076a70924988fc +size 19534 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 395b0db882..ceda179e91 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:3d9f342735dd97d58952ac3f4561cdc71c0c0c9059886d5a9cc7fe87c3f5dd3f -size 12955 +oid sha256:b8a50244fc615316296aeb71115b3b9253d08de6896e7628b426b6779e8305f2 +size 20715 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 d50b18799f..06b85709db 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:b3c5370823f6f2a80a843967d541afdbb38779e2e56d629e5e011c3b35c7060d -size 10928 +oid sha256:45ec19d6ee9b709e0d88e30f4269c6efa97832980ad25d7e62d1f4382616e02b +size 17058 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 2cc4e0207d..08f86bb545 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:603b127cbb2c4134a2f7cde964dcdc37ab7f8ba4e7eef01df09ac1d8aba02346 -size 14262 +oid sha256:fb45c26fcf4c3d0ca86ccf4cbf1683b521d83c274174254f7cab4812e7d54522 +size 22329 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 1188593ceb..6ff688fbf4 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:05796c5a4b91edf69d39ec63160651b86d296d67d7a1424059741e93d5da8307 -size 12904 +oid sha256:49bfc5339b9b6f9e67c60d3e729c765f78ff87979cf4c949a4f0ec038472fdc9 +size 20414 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 3988b6dae1..8925702f44 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:927046821aeafb5dae87199776830369207adfdb8271ba1d37b69e15af18f005 -size 13186 +oid sha256:53824544862386b344fbc10c4b65e2d36aa670e59be4706effd0db226236133f +size 21089 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 33886fc82f..70c10443d0 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:0787a1790514d8e4582901c9eedb106ada42f859853ed34b983a452487be34b5 -size 13615 +oid sha256:900ede682cc5944f40b9d85775fc6c3936dfe91670e31db462f5ffeb4c29631d +size 21610 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 d50b18799f..06b85709db 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:b3c5370823f6f2a80a843967d541afdbb38779e2e56d629e5e011c3b35c7060d -size 10928 +oid sha256:45ec19d6ee9b709e0d88e30f4269c6efa97832980ad25d7e62d1f4382616e02b +size 17058 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 b09532ebba..f67a288ce1 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:2e3d86d6019c23b8fd912f43247e6b4135b4862aac872205762f89ceb9b40cca -size 13715 +oid sha256:c94fa19fa14bf198a4bb73ce3a8f6bc5de8ce658732d65b06e3d7fba898a8757 +size 22626 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png index eaf7e8241d..20c8da6df4 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:89fda258a97d9f2e4fbf241597bc47ade06f1efd2121bcf7d448b8ef553b672f +size 23585 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 1c131b0017..5ff3bd5c23 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:bac98b38fa3d0029341d9ad1a4325b1957c353734dec225605a07fb64662802a -size 13219 +oid sha256:38f14b89e5b9bbce116bebb88d1c1faf38ad367a26e6c2de745b208324b6c6b7 +size 23318 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 ae6bfc9fa7..5dd8cb3c91 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:28881d29b70b83facf59e4dd6d4c396412f65b322503af57ee2fbbbee8031e53 -size 13474 +oid sha256:72fbcf4452b1165e3335b20f1b5eb002eecbaa2b5c46f54f73b224ca43e11557 +size 23750 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png index eaf7e8241d..20c8da6df4 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_ErrorDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:89fda258a97d9f2e4fbf241597bc47ade06f1efd2121bcf7d448b8ef553b672f +size 23585 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 0d85aeab5d..3711dd43af 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:d18dda730014d92b7e5718762c457d013a5c0b1086ef076ed398d656697607ba -size 13849 +oid sha256:32b8454efc498aba5c683ff1e2e56c0769bf02caff0bce531d4a2d396a0bc77f +size 24000 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 cd728c99bc..5bc52bf10f 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:83208a455e42ef4d097c3db84535a5e83f1b84d26b0294d9f374b20c4987d1df -size 15810 +oid sha256:da97e3e08101e0644e23ba6c42b6041c9747c1dfba5915b9e24e8c3fded7458f +size 26507 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 d2dc5e9629..2698b41314 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:f89b399b52f4d4881afaf39c468d338afbd112f6e02cb9c7a2f32467facdece1 -size 17146 +oid sha256:49418d8afb66a8aebac421109632c2354c94df282414a79c624432063f8aa688 +size 27199 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 8061d4dbdf..b2caaace4d 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:44757077f63742a2b6ed447f2b7cd840164f55dc76711836b911a350de0d59ac -size 18053 +oid sha256:dbdf35eff77d29034b85163a750a14d4a9885eec5e843b73dfbb0d59dc455e91 +size 28278 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png index eaf7e8241d..20c8da6df4 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantizationWithDitheringScale_david_WuQuantizer_OrderedDither_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4baf0e7bc4ae8b8a911d87f3a7af2bf3ef0235f77f3f509251f2d2f26cfb639d -size 13158 +oid sha256:89fda258a97d9f2e4fbf241597bc47ade06f1efd2121bcf7d448b8ef553b672f +size 23585 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 4dc0bf0eeb..697c237aee 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:85ecdd525ee8c6168ada7c2d0e3821bf8cc6d883b298bcb15eae71fdf30c8673 -size 18130 +oid sha256:91caca9cd102fdba760a2e4df28dc532b177c92a16bd9c48a8c84fe6b4ef864a +size 29272 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 3987423556..c895f21239 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:55253883878ff5721d084b7b280b9728e9b0979d733fcad2b73d4b78749819a5 -size 83175 +oid sha256:109f3a6259b6deae129655c33002f1e9cd459d76e412f93fe3c8f4f9e0546889 +size 134745 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png index 3bd7cbabbb..49362554b8 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_OctreeQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bc93509a983e20986614f4937f66d5d979bbb433a30a7736150934cf14b452a -size 55213 +oid sha256:a5c792d69895f0bad0971fb818cfbbb0ef477c3418cd51a8e10890cf2f2d180a +size 89659 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 3d0d4eccb5..d0f0ac6105 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:6083fbe3278a08b7a311d2aa68448b634426cc95bf6ff2dc9614eecc334776b6 -size 80773 +oid sha256:2b5a1f376bf131c75626e9494a47ff60349f6d48f43ecad7b0de085bfc4c15fb +size 134103 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 51c93894de..1fe3ca9837 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:a836c8efd7aa9818cf807cf56412e78399a6568798be23d0f3f6b89552856ff1 -size 62172 +oid sha256:0dddd264a5430d52b9c3605a7855f3a287300c642a4083270e39dea67d208dbd +size 108741 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 c48268b955..7252f6c878 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:fdf1ebcbf951a82b4899c12c1e24eb28c84694dac52d390b54b6db8a06949ddf -size 33901 +oid sha256:98f37db69b19f1938dc130fe0ffb6d0a428828f836acb62f6705137dbfba452f +size 59272 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 0386f0e33f..2dda761b1f 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:be3277c05d05314653976167612ef2bb5f19f8cf3159eca5122878da0f0c0fcd -size 44391 +oid sha256:bbfc8736fd342c7f855791de2fbbed047fcffd673dc0c96f8d940ac2c2149857 +size 78269 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 7cfe595a16..25b18dd2eb 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:1739c75759f8714526bbb6bff1df02aa1e327f2e22b50e3514903ec1fc672aa7 -size 35332 +oid sha256:130f5e12712e0fd9bd9a701afa227dac8ff5217e1d03ee4989f63a37484f8fb1 +size 59389 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 ff91ab181f..040cc2a722 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:7c1d2789714f291746e254815df3001655c4bae2c9c02a08b9a913e87bec5036 -size 34335 +oid sha256:d56f30b21853532f75b36f3536c0393b5370f9334cdb910628e1f62893c24bba +size 57742 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 b28fef249e..f15ad906dc 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:eec1380b1eec5bf7b3ea10ba0b6c4890286d78e6f7a3d2cea6533cb6a1ccbeca -size 44835 +oid sha256:bcef9c235d2e1c95c803a70d6a6c8b6fa07d03b42373e6283f3ac1aa1e277c77 +size 76829 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 5860c0eacc..d1ff35e0d5 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:b979180d0e4c4c644bc39d87f17d5d5b7a7dac12cb830622f3db2cd6ef2dfdc0 -size 106317 +oid sha256:2f7dd8aa0a842f3e1e501e5bf690eecd4cb1c3905f08b3e476357b404b253294 +size 157691 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png index e72ea4b246..23c9e04f23 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf2021eba9edbb2295924f8394472ac0bb237f0c462c39aa32a2074ef15f9acc -size 81771 +oid sha256:c9d001a5d53930ac954c367f2f5b7ec4c4d7c1ab4d7c5a440c89369b064d90f3 +size 132654 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 ca0c1b2949..0b55b6fb60 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:50338ed207a0b5576c3981bd4856f5de1551c765b4c08cd07404da0b1d572598 -size 101284 +oid sha256:04d2bfe81083fc03ee3d95f0d7e38eb9c2b40afbaf268fcc36ef52735f91843d +size 161739 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 a9d92676b6..2a49b0479b 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:f2f42914537c706478b010b42c5763c845450af75f7b6edee39e35b5398e465f -size 95520 +oid sha256:85539db8200dffa452f6c2ca2440c64caad7e4040f7d74ac3167b4a1a7bb0694 +size 144579 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 4f0f1cffaa..f91f614c0d 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:33effef7168a64aba77288273b03ba2db21c19f51131f08ef5783d4c6c196beb -size 79876 +oid sha256:7545baf8f2479f5663d3f35be2f813b40afe5615971e65220cb73f81ced583cd +size 119042 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 58fbba51b7..e126072c09 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:9645c6b7dad1ca5aad6a7d7e0a54d576914a4da778762a34634c3c26a6810fbc -size 92356 +oid sha256:40d1492b70f0d398395c46d3465baf6e9226df9e5487927fb445293a37ab15d7 +size 141334 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 2d6226d096..38d31b845a 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:89864a77216b51cc5b9415453ade7f7ec64c1c112546aa47ee6b4b89f9b258a3 -size 60543 +oid sha256:c07156c806631b900d57384e8a18c9dfb909286f09107e2f4202b082671722dd +size 99687 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 7b842e1f78..0124fd6155 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:e2982181e3ca61a0e6e5e27a3909d7003dc784d6bf51800159dc274ce058eb49 -size 47214 +oid sha256:30c9f22f8415ed4dabd32836530b24c9a98dbcc340624cfac809d1b12472905f +size 78738 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 82ba28be09..c273b375dc 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:1366d7c4e9770bceed30aecd894b6e947f822f8dd74f5d231f013167c43ad3c4 -size 51553 +oid sha256:3395a2b23035340734cab5d312bdbb3ce6905e9f8b744d236d1a2a9552659f61 +size 85441 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 fd27271129..6eb317c06c 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:96abf20f6b75757da529a661a2567a74527ffe8391615e34ae8f27ceaf381dba -size 67973 +oid sha256:762526f76d4b3c804c9d6e28d8cc6b486a35c5e09375080256ba795925272d6a +size 105632 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 2bad1f1f25..6f3047ec1d 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:be280b1be360e42b2eb2f8270d900a7f268e75a9ba1828fe0985925a7add6192 -size 64155 +oid sha256:fa463cae87eaa15924e8efdb3d5d6c8fde88466c82963f66c7d12e67fe4df966 +size 99238 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 4421744a88..21017d32ae 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:3a1a5f51df0ee5045de824bf8cca7aed4d89fe146280ad2697c47c86258504d4 -size 70074 +oid sha256:ab61266440141f83d77c97568990c0c06cedd6414e4a5ad959e8fe7487551bb1 +size 108837 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 d8a04a055d..f4745d054a 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:156319efa8874050ed646bf3a8b7dfd2aa7eef56c9d31fad3c408b418608ff69 -size 111255 +oid sha256:ff3092ae183d94c3f471c98c2bd47b4e7b98b28d4cfe9e0fa6bd645ec12fba3f +size 169878 diff --git a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png index 691623fc88..623a372a60 100644 --- a/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png +++ b/tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_CalliphoraPartial_WuQuantizer_NoDither.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58a61c1d9a1d05acd484948c3e5c0496dbc74c0060f5de71741de39eae04ffa8 -size 103875 +oid sha256:49706f6b846e94ede8d01a82bc7bccd7677dd3ce691c13c9462b2a594dac2205 +size 161935 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 8200f1154a..dbc63031e2 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:c3ab33be7ab7694401cd8a201784c42524afeca61ce194418b079acad22172a1 -size 110109 +oid sha256:cb7185f0655b06c48975f20cf904d36c6d8504649538746c898897a3477664a1 +size 173550 From efe11dc9a0349f8632acf4be70ad259e094d9dc2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 27 Mar 2025 00:55:46 +1000 Subject: [PATCH 63/73] Fix PNG chunk detection. --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 74 ++++++++++++++++++- .../Formats/Png/PngEncoderTests.cs | 2 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 44e76407c5..cef8396d4b 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1853,6 +1853,9 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) return false; } + // Capture the current position so we can revert back to it if we fail to read a valid chunk. + long position = this.currentStream.Position; + if (!this.TryReadChunkLength(buffer, out int length)) { // IEND @@ -1871,7 +1874,48 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) } } - PngChunkType type = this.ReadChunkType(buffer); + PngChunkType type; + + // Loop until we get a chunk type that is valid. + while (true) + { + type = this.ReadChunkType(buffer); + if (!IsValidChunkType(type)) + { + // The chunk type is invalid. + // Revert back to the next byte past the previous position and try again. + this.currentStream.Position = ++position; + + // If we are now at the end of the stream, we're done. + if (this.currentStream.Position >= this.currentStream.Length) + { + chunk = default; + return false; + } + + // Read the next chunk’s length. + if (!this.TryReadChunkLength(buffer, out length)) + { + chunk = default; + return false; + } + + while (length < 0) + { + if (!this.TryReadChunkLength(buffer, out length)) + { + chunk = default; + return false; + } + } + + // Continue to try reading the next chunk. + continue; + } + + // We have a valid chunk type. + break; + } // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip most other chunk data in the stream for better performance. @@ -1888,7 +1932,7 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) // A chunk might report a length that exceeds the length of the stream. // Take the minimum of the two values to ensure we don't read past the end of the stream. - long position = this.currentStream.Position; + position = this.currentStream.Position; chunk = new PngChunk( length: (int)Math.Min(length, this.currentStream.Length - position), type: type, @@ -1906,6 +1950,32 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) return true; } + /// + /// Determines whether the 4-byte chunk type is valid (all ASCII letters). + /// + /// The chunk type. + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsValidChunkType(PngChunkType type) + { + uint value = (uint)type; + byte b0 = (byte)(value >> 24); + byte b1 = (byte)(value >> 16); + byte b2 = (byte)(value >> 8); + byte b3 = (byte)value; + return IsAsciiLetter(b0) && IsAsciiLetter(b1) && IsAsciiLetter(b2) && IsAsciiLetter(b3); + } + + /// + /// Returns a value indicating whether the given byte is an ASCII letter. + /// + /// The byte to check. + /// + /// if the byte is an ASCII letter; otherwise, . + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsAsciiLetter(byte b) + => (b >= (byte)'A' && b <= (byte)'Z') || (b >= (byte)'a' && b <= (byte)'z'); + /// /// Validates the png chunk. /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index ad58f12b27..c94413cb69 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -720,7 +720,7 @@ public void Issue_2862() using MemoryStream ms = new(); PaletteQuantizer quantizer = new( palette.Select(Color.FromPixel).ToArray(), - new QuantizerOptions() { ColorMatchingMode = ColorMatchingMode.Hybrid}); + new QuantizerOptions() { ColorMatchingMode = ColorMatchingMode.Hybrid }); image.Save(ms, new PngEncoder { From 925a6519f09f0066f355c4feada0132598254782 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Thu, 27 Mar 2025 14:22:59 +0100 Subject: [PATCH 64/73] fix build (add blank lines) --- src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs | 1 + src/ImageSharp/Formats/AnimatedImageMetadata.cs | 1 + src/ImageSharp/Formats/Qoi/QoiDecoder.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs index 5f4015180b..75595e1f7d 100644 --- a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs +++ b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats; + internal class AnimatedImageFrameMetadata { /// diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs index d89ec41f07..ac3ca29f4f 100644 --- a/src/ImageSharp/Formats/AnimatedImageMetadata.cs +++ b/src/ImageSharp/Formats/AnimatedImageMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.Formats; + internal class AnimatedImageMetadata { /// diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoder.cs b/src/ImageSharp/Formats/Qoi/QoiDecoder.cs index a54095dfc6..5c1bf6ad23 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoder.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoder.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Qoi; + internal class QoiDecoder : ImageDecoder { private QoiDecoder() From aae2c479f06d12f5fb7a1cbbeb0959dd49a915c9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 24 Apr 2025 22:31:46 +1000 Subject: [PATCH 65/73] Reimplement #2869 --- .../Metadata/Profiles/Exif/ExifReader.cs | 22 ++++++++--- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 39 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Jpg/issues/issue-2857-subsub-ifds.jpg | 3 ++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/Images/Input/Jpg/issues/issue-2857-subsub-ifds.jpg diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 953ef74afb..fe510f250d 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using SixLabors.ImageSharp.Memory; @@ -187,11 +188,21 @@ protected void ReadValues(List values, uint offset) protected void ReadSubIfd(List values) { - if (this.subIfds is not null) + if (this.subIfds != null) { - foreach (ulong subIfdOffset in this.subIfds) + const int maxSubIfds = 8; + const int maxNestingLevel = 8; + Span buf = stackalloc ulong[maxSubIfds]; + for (int i = 0; i < maxNestingLevel && this.subIfds.Count > 0; i++) { - this.ReadValues(values, (uint)subIfdOffset); + int sz = Math.Min(this.subIfds.Count, maxSubIfds); + CollectionsMarshal.AsSpan(this.subIfds)[..sz].CopyTo(buf); + + this.subIfds.Clear(); + foreach (ulong subIfdOffset in buf[..sz]) + { + this.ReadValues(values, (uint)subIfdOffset); + } } } } @@ -481,8 +492,9 @@ private void Add(IList values, IExifValue exif, object? value) foreach (IExifValue val in values) { - // Sometimes duplicates appear, can compare val.Tag == exif.Tag - if (val == exif) + // to skip duplicates must be used Equals method, + // == operator not defined for ExifValue and IExifValue + if (exif.Equals(val)) { Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}"); return; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index e68dd1f879..bd2acaff1c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -452,6 +452,45 @@ public void Issue2758_DecodeWorks(TestImageProvider provider) image.Save(ms, new JpegEncoder()); } + // https://github.com/SixLabors/ImageSharp/issues/2857 + [Theory] + [WithFile(TestImages.Jpeg.Issues.Issue2857, PixelTypes.Rgb24)] + public void Issue2857_SubSubIfds(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder.Instance); + + Assert.Equal(5616, image.Width); + Assert.Equal(3744, image.Height); + + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + Assert.Equal(92, meta.LuminanceQuality); + Assert.Equal(93, meta.ChrominanceQuality); + + ExifProfile exifProfile = image.Metadata.ExifProfile; + Assert.NotNull(exifProfile); + + using MemoryStream ms = new(); + bool hasThumbnail = exifProfile.TryCreateThumbnail(out _); + Assert.False(hasThumbnail); + + Assert.Equal("BilderBox - Erwin Wodicka / wodicka@aon.at", exifProfile.GetValue(ExifTag.Copyright).Value); + Assert.Equal("Adobe Photoshop CS3 Windows", exifProfile.GetValue(ExifTag.Software).Value); + + Assert.Equal("Carers; seniors; caregiver; senior care; retirement home; hands; old; elderly; elderly caregiver; elder care; elderly care; geriatric care; nursing home; age; old age care; outpatient; needy; health care; home nurse; home care; sick; retirement; medical; mobile; the elderly; nursing department; nursing treatment; nursing; care services; nursing services; nursing care; nursing allowance; nursing homes; home nursing; care category; nursing class; care; nursing shortage; nursing patient care staff\0", exifProfile.GetValue(ExifTag.XPKeywords).Value); + + Assert.Equal( + new EncodedString(EncodedString.CharacterCode.ASCII, "StockSubmitter|Miscellaneous||Miscellaneous$|00|0000330000000110000000000000000|22$@NA_1005010.460@145$$@Miscellaneous.Miscellaneous$$@$@26$$@$@$@$@205$@$@$@$@$@$@$@$@$@43$@$@$@$$@Miscellaneous.Miscellaneous$$@90$$@22$@$@$@$@$@$@$|||"), + exifProfile.GetValue(ExifTag.UserComment).Value); + + // the profile contains 4 duplicated UserComment + Assert.Equal(1, exifProfile.Values.Count(t => t.Tag == ExifTag.UserComment)); + + image.Mutate(x => x.Crop(new(0, 0, 100, 100))); + + image.Save(ms, new JpegEncoder()); + } + private static void VerifyEncodedStrings(ExifProfile exif) { Assert.NotNull(exif); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 0974bab6dd..f501f8e9bc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -325,6 +325,7 @@ public static class Issues public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg"; public const string Issue2638 = "Jpg/issues/Issue2638.jpg"; public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; + public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg"; public static class Fuzz { diff --git a/tests/Images/Input/Jpg/issues/issue-2857-subsub-ifds.jpg b/tests/Images/Input/Jpg/issues/issue-2857-subsub-ifds.jpg new file mode 100644 index 0000000000..5e5288f22e --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-2857-subsub-ifds.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab45137ded01a42658aa94421165a358b184a536b6ab64427d8255e8d78c25b9 +size 2829977 From 1cc358814b5b12aa767b95be1058a2007ba74fdb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 26 Apr 2025 18:53:50 +1000 Subject: [PATCH 66/73] Undo horizontal prediction for each tile row in case of tiled tiff's --- .../Decompressors/DeflateTiffCompression.cs | 24 +- .../Decompressors/LzwTiffCompression.cs | 24 +- .../Tiff/Compression/HorizontalPredictor.cs | 937 +++++++++++------- .../Compression/TiffDecompressorsFactory.cs | 9 +- .../Formats/Tiff/TiffDecoderCore.cs | 9 +- .../DeflateTiffCompressionTests.cs | 2 +- .../Compression/LzwTiffCompressionTests.cs | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 34 + tests/ImageSharp.Tests/TestImages.cs | 30 + .../ReferenceCodecs/MagickReferenceDecoder.cs | 2 +- ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...led_gray_deflate_compressed_predictor.tiff | 3 + .../tiled_gray_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...iled_rgb_deflate_compressed_predictor.tiff | 3 + .../tiled_rgb_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...g_endian_deflate_compressed_predictor.tiff | 3 + ...t_big_endian_lzw_compressed_predictor.tiff | 3 + ...e_endian_deflate_compressed_predictor.tiff | 3 + ...ittle_endian_lzw_compressed_predictor.tiff | 3 + ...led_rgba_deflate_compressed_predictor.tiff | 3 + .../tiled_rgba_lzw_compressed_predictor.tiff | 3 + 44 files changed, 806 insertions(+), 369 deletions(-) create mode 100644 tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_gray_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgb_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_lzw_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_deflate_compressed_predictor.tiff create mode 100644 tests/Images/Input/Tiff/tiled_rgba_lzw_compressed_predictor.tiff diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index 27c311009c..64e702f1be 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -22,6 +22,12 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor private readonly TiffColorType colorType; + private readonly bool isTiled; + + private readonly int tileWidth; + + private readonly int tileHeight; + /// /// Initializes a new instance of the class. /// @@ -31,11 +37,17 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor /// The color type of the pixel data. /// The tiff predictor used. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian) + /// Flag indicates, if the image is a tiled image. + /// Number of pixels in a tile row. + /// Number of rows in a tile. + public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian, bool isTiled, int tileWidth, int tileHeight) : base(memoryAllocator, width, bitsPerPixel, predictor) { this.colorType = colorType; this.isBigEndian = isBigEndian; + this.isTiled = isTiled; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; } /// @@ -70,7 +82,15 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + if (this.isTiled) + { + // When the image is tiled, undoing the horizontal predictor will be done for each tile row. + HorizontalPredictor.UndoTile(buffer, this.tileWidth, this.tileHeight, this.colorType, this.isBigEndian); + } + else + { + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs index 01591e138b..2402927186 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs @@ -17,6 +17,12 @@ internal sealed class LzwTiffCompression : TiffBaseDecompressor private readonly TiffColorType colorType; + private readonly bool isTiled; + + private readonly int tileWidth; + + private readonly int tileHeight; + /// /// Initializes a new instance of the class. /// @@ -26,11 +32,17 @@ internal sealed class LzwTiffCompression : TiffBaseDecompressor /// The color type of the pixel data. /// The tiff predictor used. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian) + /// Flag indicates, if the image is a tiled image. + /// Number of pixels in a tile row. + /// Number of rows in a tile. + public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffColorType colorType, TiffPredictor predictor, bool isBigEndian, bool isTiled, int tileWidth, int tileHeight) : base(memoryAllocator, width, bitsPerPixel, predictor) { this.colorType = colorType; this.isBigEndian = isBigEndian; + this.isTiled = isTiled; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; } /// @@ -41,7 +53,15 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int if (this.Predictor == TiffPredictor.Horizontal) { - HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + if (this.isTiled) + { + // When the image is tiled, undoing the horizontal predictor will be done for each tile row. + HorizontalPredictor.UndoTile(buffer, this.tileWidth, this.tileHeight, this.colorType, this.isBigEndian); + } + else + { + HorizontalPredictor.Undo(buffer, this.Width, this.colorType, this.isBigEndian); + } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index 1c838b0b76..838adc1477 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression; internal static class HorizontalPredictor { /// - /// Inverts the horizontal prediction. + /// Inverts the horizontal predictor. /// /// Buffer with decompressed pixel data. /// The width of the image or strip. @@ -62,6 +62,126 @@ public static void Undo(Span pixelBytes, int width, TiffColorType colorTyp } } + /// + /// Inverts the horizontal predictor for each tile row. + /// + /// Buffer with decompressed pixel data for a tile. + /// Tile width in pixels. + /// Tile height in pixels. + /// The color type of the pixel data. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public static void UndoTile(Span pixelBytes, int tileWidth, int tileHeight, TiffColorType colorType, bool isBigEndian) + { + for (int y = 0; y < tileHeight; y++) + { + UndoRow(pixelBytes, tileWidth, y, colorType, isBigEndian); + } + } + + /// + /// Inverts the horizontal predictor for one row. + /// + /// Buffer with decompressed pixel data. + /// The width in pixels of the row. + /// The row index. + /// The color type of the pixel data. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public static void UndoRow(Span pixelBytes, int width, int y, TiffColorType colorType, bool isBigEndian) + { + switch (colorType) + { + case TiffColorType.BlackIsZero8: + case TiffColorType.WhiteIsZero8: + case TiffColorType.PaletteColor: + UndoGray8BitRow(pixelBytes, width, y); + break; + + case TiffColorType.BlackIsZero16: + case TiffColorType.WhiteIsZero16: + if (isBigEndian) + { + UndoGray16BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoGray16BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.BlackIsZero32: + case TiffColorType.WhiteIsZero32: + if (isBigEndian) + { + UndoGray32BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoGray32BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgb888: + case TiffColorType.CieLab: + UndoRgb24BitRow(pixelBytes, width, y); + break; + + case TiffColorType.Rgba8888: + case TiffColorType.Cmyk: + UndoRgba32BitRow(pixelBytes, width, y); + break; + + case TiffColorType.Rgb161616: + if (isBigEndian) + { + UndoRgb48BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb48BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgba16161616: + if (isBigEndian) + { + UndoRgb64BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb64BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgb323232: + if (isBigEndian) + { + UndoRgb96BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgb96BitLittleEndianRow(pixelBytes, width, y); + } + + break; + + case TiffColorType.Rgba32323232: + if (isBigEndian) + { + UndoRgba128BitBigEndianRow(pixelBytes, width, y); + } + else + { + UndoRgba128BitLittleEndianRow(pixelBytes, width, y); + } + + break; + } + } + public static void ApplyHorizontalPrediction(Span rows, int width, int bitsPerPixel) { if (bitsPerPixel == 8) @@ -154,20 +274,64 @@ private static void ApplyHorizontalPrediction8Bit(Span rows, int width) } } + private static void UndoGray8BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width; + int height = pixelBytes.Length / rowBytesCount; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + byte pixelValue = rowBytes[0]; + for (int x = 1; x < width; x++) + { + pixelValue += rowBytes[x]; + rowBytes[x] = pixelValue; + } + } + private static void UndoGray8Bit(Span pixelBytes, int width) { int rowBytesCount = width; int height = pixelBytes.Length / rowBytesCount; for (int y = 0; y < height; y++) { - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + UndoGray8BitRow(pixelBytes, width, y); + } + } - byte pixelValue = rowBytes[0]; - for (int x = 1; x < width; x++) - { - pixelValue += rowBytes[x]; - rowBytes[x] = pixelValue; - } + private static void UndoGray16BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtils.ConvertToUShortBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, pixelValue); + offset += 2; + } + } + + private static void UndoGray16BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 2; + int height = pixelBytes.Length / rowBytesCount; + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort pixelValue = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort diff = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, pixelValue); + offset += 2; } } @@ -179,42 +343,58 @@ private static void UndoGray16Bit(Span pixelBytes, int width, bool isBigEn { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort pixelValue = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort diff = TiffUtils.ConvertToUShortBigEndian(rowSpan); - pixelValue += diff; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, pixelValue); - offset += 2; - } + UndoGray16BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort pixelValue = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort diff = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - pixelValue += diff; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, pixelValue); - offset += 2; - } + UndoGray16BitLittleEndianRow(pixelBytes, width, y); } } } + private static void UndoGray32BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtils.ConvertToUIntBigEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, pixelValue); + offset += 4; + } + } + + private static void UndoGray32BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint pixelValue = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint diff = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + pixelValue += diff; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, pixelValue); + offset += 4; + } + } + private static void UndoGray32Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 4; @@ -223,64 +403,68 @@ private static void UndoGray32Bit(Span pixelBytes, int width, bool isBigEn { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint pixelValue = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint diff = TiffUtils.ConvertToUIntBigEndian(rowSpan); - pixelValue += diff; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, pixelValue); - offset += 4; - } + UndoGray32BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint pixelValue = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint diff = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - pixelValue += diff; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, pixelValue); - offset += 4; - } + UndoGray32BitLittleEndianRow(pixelBytes, width, y); } } } + private static void UndoRgb24BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 3; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; + ref Rgb24 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgb24 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + pixel = new Rgb24(r, g, b); + } + } + private static void UndoRgb24Bit(Span pixelBytes, int width) { int rowBytesCount = width * 3; int height = pixelBytes.Length / rowBytesCount; for (int y = 0; y < height; y++) { - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; - ref Rgb24 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); - byte r = rowRgbBase.R; - byte g = rowRgbBase.G; - byte b = rowRgbBase.B; + UndoRgb24BitRow(pixelBytes, width, y); + } + } - for (int x = 1; x < rowRgb.Length; x++) - { - ref Rgb24 pixel = ref rowRgb[x]; - r += pixel.R; - g += pixel.G; - b += pixel.B; - var rgb = new Rgb24(r, g, b); - pixel.FromRgb24(rgb); - } + private static void UndoRgba32BitRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 4; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; + ref Rgba32 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + byte a = rowRgbBase.A; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgba32 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + a += pixel.A; + pixel = new Rgba32(r, g, b, a); } } @@ -290,24 +474,79 @@ private static void UndoRgba32Bit(Span pixelBytes, int width) int height = pixelBytes.Length / rowBytesCount; for (int y = 0; y < height; y++) { - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - Span rowRgb = MemoryMarshal.Cast(rowBytes)[..width]; - ref Rgba32 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); - byte r = rowRgbBase.R; - byte g = rowRgbBase.G; - byte b = rowRgbBase.B; - byte a = rowRgbBase.A; + UndoRgba32BitRow(pixelBytes, width, y); + } + } - for (int x = 1; x < rowRgb.Length; x++) - { - ref Rgba32 pixel = ref rowRgb[x]; - r += pixel.R; - g += pixel.G; - b += pixel.B; - a += pixel.A; - var rgb = new Rgba32(r, g, b, a); - pixel.FromRgba32(rgb); - } + private static void UndoRgb48BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + } + } + + private static void UndoRgb48BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 6; + int height = pixelBytes.Length / rowBytesCount; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; } } @@ -319,74 +558,104 @@ private static void UndoRgb48Bit(Span pixelBytes, int width, bool isBigEnd { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); - offset += 2; - } + UndoRgb48BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); - offset += 2; - } + UndoRgb48BitLittleEndianRow(pixelBytes, width, y); } } } + private static void UndoRgb64BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 8; + int offset = 0; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtils.ConvertToUShortBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, a); + offset += 2; + } + } + + private static void UndoRgb64BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 8; + int offset = 0; + + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, a); + offset += 2; + } + } + private static void UndoRgba64Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 8; @@ -395,90 +664,88 @@ private static void UndoRgba64Bit(Span pixelBytes, int width, bool isBigEn { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort a = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaA = TiffUtils.ConvertToUShortBigEndian(rowSpan); - a += deltaA; - BinaryPrimitives.WriteUInt16BigEndian(rowSpan, a); - offset += 2; - } + UndoRgb64BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - ushort a = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); - offset += 2; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 2); - ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); - offset += 2; - - rowSpan = rowBytes.Slice(offset, 2); - ushort deltaA = TiffUtils.ConvertToUShortLittleEndian(rowSpan); - a += deltaA; - BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, a); - offset += 2; - } + UndoRgb64BitLittleEndianRow(pixelBytes, width, y); } } } + private static void UndoRgb96BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 12; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + } + } + + private static void UndoRgb96BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 12; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + } + } + private static void UndoRgb96Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 12; @@ -487,74 +754,104 @@ private static void UndoRgb96Bit(Span pixelBytes, int width, bool isBigEnd { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); - offset += 4; - } + UndoRgb96BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); - offset += 4; - } + UndoRgb96BitLittleEndianRow(pixelBytes, width, y); } } } + private static void UndoRgba128BitBigEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 16; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtils.ConvertToUIntBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, a); + offset += 4; + } + } + + private static void UndoRgba128BitLittleEndianRow(Span pixelBytes, int width, int y) + { + int rowBytesCount = width * 16; + + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, a); + offset += 4; + } + } + private static void UndoRgba128Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 16; @@ -563,86 +860,14 @@ private static void UndoRgba128Bit(Span pixelBytes, int width, bool isBigE { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint a = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaA = TiffUtils.ConvertToUIntBigEndian(rowSpan); - a += deltaA; - BinaryPrimitives.WriteUInt32BigEndian(rowSpan, a); - offset += 4; - } + UndoRgba128BitBigEndianRow(pixelBytes, width, y); } } else { for (int y = 0; y < height; y++) { - int offset = 0; - Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); - uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - uint a = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); - offset += 4; - - for (int x = 1; x < width; x++) - { - Span rowSpan = rowBytes.Slice(offset, 4); - uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - r += deltaR; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - g += deltaG; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - b += deltaB; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); - offset += 4; - - rowSpan = rowBytes.Slice(offset, 4); - uint deltaA = TiffUtils.ConvertToUIntLittleEndian(rowSpan); - a += deltaA; - BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, a); - offset += 4; - } + UndoRgba128BitLittleEndianRow(pixelBytes, width, y); } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index 720e376b9d..2f3176f013 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -24,7 +24,10 @@ public static TiffBaseDecompressor Create( byte[] jpegTables, uint oldJpegStartOfImageMarker, TiffFillOrder fillOrder, - ByteOrder byteOrder) + ByteOrder byteOrder, + bool isTiled = false, + int tileWidth = 0, + int tileHeight = 0) { switch (method) { @@ -40,11 +43,11 @@ public static TiffBaseDecompressor Create( case TiffDecoderCompressionType.Deflate: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new DeflateTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian); + return new DeflateTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian, isTiled, tileWidth, tileHeight); case TiffDecoderCompressionType.Lzw: DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected"); - return new LzwTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian); + return new LzwTiffCompression(allocator, width, bitsPerPixel, colorType, predictor, byteOrder == ByteOrder.BigEndian, isTiled, tileWidth, tileHeight); case TiffDecoderCompressionType.T4: DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 18535d1e37..e66f0f3765 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -639,7 +639,7 @@ private void DecodeTilesChunky( using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(bytesPerTileRow * tileLength, AllocationOptions.Clean); Span tileBufferSpan = tileBuffer.GetSpan(); - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, true, tileWidth, tileLength); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); int tileIndex = 0; @@ -707,7 +707,7 @@ private TiffBasePlanarColorDecoder CreatePlanarColorDecoder() this.YcbcrSubSampling, this.byteOrder); - private TiffBaseDecompressor CreateDecompressor(int frameWidth, int bitsPerPixel) + private TiffBaseDecompressor CreateDecompressor(int frameWidth, int bitsPerPixel, bool isTiled = false, int tileWidth = 0, int tileHeight = 0) where TPixel : unmanaged, IPixel => TiffDecompressorsFactory.Create( this.Options, @@ -722,7 +722,10 @@ private TiffBaseDecompressor CreateDecompressor(int frameWidth, int bits this.JpegTables, this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(), this.FillOrder, - this.byteOrder); + this.byteOrder, + isTiled, + tileWidth, + tileHeight); private IMemoryOwner ConvertNumbers(Array array, out Span span) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs index 1b12adac23..b16119f338 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -23,7 +23,7 @@ public void Compress_Decompress_Roundtrip_Works(byte[] data) using BufferedReadStream stream = CreateCompressedStream(data); byte[] buffer = new byte[data.Length]; - using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); + using var decompressor = new DeflateTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false, false, 0, 0); decompressor.Decompress(stream, 0, (uint)stream.Length, 1, buffer, default); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs index 635a3a33e4..8c21e346af 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/LzwTiffCompressionTests.cs @@ -37,7 +37,7 @@ public void Compress_Decompress_Roundtrip_Works(byte[] data) using BufferedReadStream stream = CreateCompressedStream(data); byte[] buffer = new byte[data.Length]; - using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false); + using var decompressor = new LzwTiffCompression(Configuration.Default.MemoryAllocator, 10, 8, TiffColorType.BlackIsZero8, TiffPredictor.None, false, false, 0, 0); decompressor.Decompress(stream, 0, (uint)stream.Length, 1, buffer, default); Assert.Equal(data, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ab49805a35..839334449d 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -91,6 +91,40 @@ public void TiffDecoder_CanDecode_Planar(TestImageProvider provi public void TiffDecoder_CanDecode_Tiled(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(TiledRgbaDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgbDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGrayDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray16BitLittleEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray16BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray32BitLittleEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray32BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb48BitLittleEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb48BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgba64BitLittleEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgba64BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb96BitLittleEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb96BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Tiled_Deflate_Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(TiledRgbaLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgbLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGrayLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray16BitLittleEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray16BitBigEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray32BitLittleEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledGray32BitBigEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb48BitLittleEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb48BitBigEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgba64BitLittleEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgba64BitBigEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb96BitLittleEndianLzwCompressedWithPredictor, PixelTypes.Rgba32)] + [WithFile(TiledRgb96BitBigEndianDeflateCompressedWithPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Tiled_Lzw_Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Rgba8BitPlanarUnassociatedAlpha, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_Planar_32Bit(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index f501f8e9bc..1b93d61e44 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1014,6 +1014,36 @@ public static class Tiff public const string QuadTile = "Tiff/quad-tile.tiff"; public const string TiledChunky = "Tiff/rgb_uncompressed_tiled_chunky.tiff"; public const string TiledPlanar = "Tiff/rgb_uncompressed_tiled_planar.tiff"; + public const string TiledRgbaDeflateCompressedWithPredictor = "Tiff/tiled_rgba_deflate_compressed_predictor.tiff"; + public const string TiledRgbDeflateCompressedWithPredictor = "Tiff/tiled_rgb_deflate_compressed_predictor.tiff"; + public const string TiledGrayDeflateCompressedWithPredictor = "Tiff/tiled_gray_deflate_compressed_predictor.tiff"; + public const string TiledGray16BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_gray_16bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledGray16BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_gray_16bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledGray32BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_gray_32bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledGray32BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_gray_32bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgb48BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgb_48bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgb48BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgb_48bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgba64BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgba_64bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgba64BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgba_64bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgb96BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgb_96bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgb96BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgb_96bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgba128BitLittleEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgba_128bit_little_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgba128BitBigEndianDeflateCompressedWithPredictor = "Tiff/tiled_rgba_128bit_big_endian_deflate_compressed_predictor.tiff"; + public const string TiledRgbaLzwCompressedWithPredictor = "Tiff/tiled_rgba_lzw_compressed_predictor.tiff"; + public const string TiledRgbLzwCompressedWithPredictor = "Tiff/tiled_rgb_lzw_compressed_predictor.tiff"; + public const string TiledGrayLzwCompressedWithPredictor = "Tiff/tiled_gray_lzw_compressed_predictor.tiff"; + public const string TiledGray16BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_gray_16bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledGray16BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_gray_16bit_big_endian_lzw_compressed_predictor.tiff"; + public const string TiledGray32BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_gray_32bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledGray32BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_gray_32bit_big_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgb48BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_rgb_48bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgb48BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_rgb_48bit_big_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgba64BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_rgba_64bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgba64BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_rgba_64bit_big_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgb96BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_rgb_96bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgb96BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_rgb_96bit_big_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgba128BitLittleEndianLzwCompressedWithPredictor = "Tiff/tiled_rgba_128bit_little_endian_lzw_compressed_predictor.tiff"; + public const string TiledRgba128BitBigEndianLzwCompressedWithPredictor = "Tiff/tiled_rgba_128bit_big_endian_lzw_compressed_predictor.tiff"; // Images with alpha channel. public const string Rgba2BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha2bit.tiff"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index a06e19594d..01aff1c5f0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -66,7 +66,7 @@ protected override Image Decode(DecoderOptions options, Stream s FromRgba32Bytes(configuration, data, buffer); } - else if (magicFrame.Depth is 16 or 14) + else if (magicFrame.Depth is 14 or 16 or 32) { ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); diff --git a/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..f99c06f3fa --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af45780d252025f690e039738f37a6656fea72ecdc8b816eea354259af46ebed +size 87491 diff --git a/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..fbeb462fbc --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_16bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:190a4968abd4bbd39273020f0f8bee0e0e48573931653eaea5e49b49d3961206 +size 87301 diff --git a/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..2fbe32e6fa --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19b4a2ee8761e0016a598f66f12eb62edf4c4d30f33694d30986ce84516ac454 +size 80177 diff --git a/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..d4508e32ad --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_16bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:879bacee73f5fea767439071aa6057d66d2d61bc554b109abb7b79765873730b +size 78795 diff --git a/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..e7c527c9cb --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac571a8aa1274bd11f79d7d428d72e46d25ffcb331630f5eb114b94283055f7e +size 90547 diff --git a/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..76f83f3973 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_32bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4bebd7e62dbe54456923981b27dcc1415362c30a7c3c0ca665acf8dbdfe1cc8 +size 115129 diff --git a/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..8ca8fba8f5 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bec8072a0062101c08e0c1c1f4265150a53276d2568ada482b0ad554b13d658d +size 90585 diff --git a/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..53aac39e78 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_32bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e618556125236e2b0034b8654ab7b3e8956cf6db3c7c35ae365445ce85b2ea3d +size 114137 diff --git a/tests/Images/Input/Tiff/tiled_gray_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..563af83ac2 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce5c12864099fa02c5702a5da3f5af8cadcf7822ff95ee117a3b8d846518d9d8 +size 59361 diff --git a/tests/Images/Input/Tiff/tiled_gray_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_gray_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..ed05c4b526 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_gray_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2942e3a2e83ee1e10cc238299c21dcc9ccad7058b0b389f69dcf3186cbd215a1 +size 67849 diff --git a/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..c9a2e6fd22 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5ffcad0698d5485dfaffc8fa8368e62e45ec54b715f82c93b0854a41875ea11 +size 220425 diff --git a/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..d3dc3f332a --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_48bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f69c65b66041cc34bfe894628bbe39a9e09cc9a8e7eccaefad42b672b2d2e3b +size 234705 diff --git a/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..cf11248642 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3ea3976405c5512d676a0830c5d2d7a4d2c12128e56b0cf1c722b7152f4e255 +size 220415 diff --git a/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..b8d98e5d77 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_48bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6715b725ac4ac2ba57f7c9f6b8fd89182b6d2c5a6b8d9141f204783bc44a8c5 +size 234697 diff --git a/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..827584750d --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4b95d268817cfd2e261d76a6698b51f2ccb7fbda39dc26737c9572d445f3e0e +size 240737 diff --git a/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..2cb3db6007 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_64bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74dbdcc550fd0c9a425a02bb48a458e2c285030fd3fdaa8e6359bae1f4e06096 +size 270201 diff --git a/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..61f2d10271 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8601908cea23f1cb15d46472c53424a7222213a8f1a3aa8df984f64068c6654c +size 240651 diff --git a/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..93d046f1c5 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_64bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7364b83b36201d4f40144a619c7bf551f0ff27509aba3db3c7a1dbc01a881d8 +size 270305 diff --git a/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..9863b2271b --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ad32fab0240fdd87752857d349768802305a4eed73cc42146ad92a25525963e +size 232587 diff --git a/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..107a311535 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_96bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c0b50066a4480b02c36ee97885e30498b0c9a702de6d317c648a7908a2d119d +size 313111 diff --git a/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..28ee6d3f61 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6022edc75314270ca5afb198113d74d5598b50663eda27a9a40563b7384d5976 +size 232667 diff --git a/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..c636b5872c --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_96bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02e12574235e0861f4cad850869f53893b35334142c36d7c27d317382167ed68 +size 311165 diff --git a/tests/Images/Input/Tiff/tiled_rgb_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..71b4b29ce5 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba8740738c2d86a22cc1bf43b1036e296e7caca8dad1ae70fd090edd9579f2cd +size 163803 diff --git a/tests/Images/Input/Tiff/tiled_rgb_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgb_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..a1b1bbfdd5 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgb_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c47d681bfefa0cb12a25d685f13cc9fd01b3e0cb99828b97ccd948b43c138cfa +size 180641 diff --git a/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..c980ae69fe --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b7aad5756525ba52e43e309a982bc318197b68a168d2cf08bae6ee4422f59c +size 264637 diff --git a/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..09edf13030 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_128bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b8e0812543f47ce79701ddc57a6473f8f3c6e016520cdc76cf4c1c11a5f54a0 +size 370941 diff --git a/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..5f589070a8 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0405c620eb35ce375a2a3e198ab4208e450d420a1eea17846d69fd810b7bf0aa +size 264749 diff --git a/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..5eb324e931 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_128bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50620e1aeeb1d99fc108d08215f51eb7ef648cba506a1b657a74ee138b9e3a5f +size 369133 diff --git a/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..0def244601 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f456272c6948159ea17d6fa7d2a89e30521c1d4e29f0ad0b1a79894f122d2153 +size 220119 diff --git a/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..9c442bf340 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_64bit_big_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0ddd2ec8f73c784b060ff790e70f586bba70905b75f3cf8ae18f2b054e1eb06 +size 244677 diff --git a/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..bcd96a88bb --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee63d8b0d63fd091390678a4a2600df5c14a122002a4d9c93e7d01082a2ee347 +size 220045 diff --git a/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..456f57cbd6 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_64bit_little_endian_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8e05d9ca953045d732c4304575588d2db2ead50177b2ed9416922568760ee1f +size 244625 diff --git a/tests/Images/Input/Tiff/tiled_rgba_deflate_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_deflate_compressed_predictor.tiff new file mode 100644 index 0000000000..79bf1f6b76 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_deflate_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0a3ecf077c701f450ce363633583134a79fd9d4d91fff0bd79f4bebe5f18649 +size 185503 diff --git a/tests/Images/Input/Tiff/tiled_rgba_lzw_compressed_predictor.tiff b/tests/Images/Input/Tiff/tiled_rgba_lzw_compressed_predictor.tiff new file mode 100644 index 0000000000..6cc0f28dc7 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled_rgba_lzw_compressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:183e011bc22048d65cd1945d60dc25dc9cb688d9141afefa1c66ae0edfca8309 +size 202825 From 454b02a88826d4278f795312f54579ed40d74b52 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 26 Apr 2025 19:59:37 +1000 Subject: [PATCH 67/73] Use BPP from color writer --- src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs | 18 +++++++++--------- .../Formats/Tiff/TiffEncoderTests.cs | 10 ++++++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Tiff/Issues/Issue2909.tiff | 3 +++ 4 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 tests/Images/Input/Tiff/Issues/Issue2909.tiff diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 59b4eac0bc..7525eccdab 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -210,15 +210,6 @@ private long WriteFrame( long ifdOffset) where TPixel : unmanaged, IPixel { - using TiffBaseCompressor compressor = TiffCompressorFactory.Create( - this.CompressionType ?? TiffCompression.None, - writer.BaseStream, - this.memoryAllocator, - frame.Width, - (int)this.BitsPerPixel, - this.compressionLevel, - this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); - TiffEncoderEntriesCollector entriesCollector = new(); using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, @@ -230,6 +221,15 @@ private long WriteFrame( entriesCollector, (int)this.BitsPerPixel); + using TiffBaseCompressor compressor = TiffCompressorFactory.Create( + this.CompressionType ?? TiffCompression.None, + writer.BaseStream, + this.memoryAllocator, + frame.Width, + colorWriter.BitsPerPixel, + this.compressionLevel, + this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); colorWriter.Write(compressor, rowsPerStrip); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 1fafb4cd04..2c3fbc3aec 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -518,6 +518,16 @@ public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D); + [Theory] + [WithFile(Issue2909, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithLzwCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, null, TiffCompression.Lzw, imageDecoder: TiffDecoder.Instance); + + [Theory] + [WithFile(Issue2909, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithDeflateCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, null, TiffCompression.Deflate, imageDecoder: TiffDecoder.Instance); + [Theory] [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)] [WithFile(GrayscaleUncompressed16Bit, PixelTypes.L16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1b93d61e44..6463f799b5 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1131,6 +1131,8 @@ public static class Tiff public const string InvalidIptcData = "Tiff/7324fcaff3aad96f27899da51c1bb5d9.tiff"; public const string IptcData = "Tiff/iptc.tiff"; + public const string Issue2909 = "Tiff/Issues/Issue2909.tiff"; + public static readonly string[] Multiframes = { MultiframeDeflateWithPreview, MultiframeLzwPredictor /*, MultiFrameDifferentSize, MultiframeDifferentSizeTiled, MultiFrameDifferentVariants,*/ }; public static readonly string[] Metadata = { SampleMetadata }; diff --git a/tests/Images/Input/Tiff/Issues/Issue2909.tiff b/tests/Images/Input/Tiff/Issues/Issue2909.tiff new file mode 100644 index 0000000000..99f1f088ee --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/Issue2909.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c69a7e7c7920766e98fccd273424a34e9094550e2176a7b4757ab2c0756d084 +size 1272 From de566af47b396eecf6e866aa2709e09b1a612ee4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 26 Apr 2025 20:07:02 +1000 Subject: [PATCH 68/73] Update TiffEncoderTests.cs --- tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 2c3fbc3aec..86d518e902 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -520,12 +520,12 @@ public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_BlackIsZero [Theory] [WithFile(Issue2909, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeBiColor_WithLzwCompression_Works(TestImageProvider provider) + public void TiffEncoder_WithLzwCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, null, TiffCompression.Lzw, imageDecoder: TiffDecoder.Instance); [Theory] [WithFile(Issue2909, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeBiColor_WithDeflateCompression_Works(TestImageProvider provider) + public void TiffEncoder_WithDeflateCompression_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, null, TiffCompression.Deflate, imageDecoder: TiffDecoder.Instance); [Theory] From ad25188391774e8c5a1f7b5e4777341a84bb33ce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 May 2025 15:05:45 +1000 Subject: [PATCH 69/73] Fix #2924 --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 17 ++++++++++++++--- .../Formats/Png/PngDecoderTests.cs | 12 +++++++++++- tests/ImageSharp.Tests/TestImages.cs | 3 +++ .../CanDecode_Issue2924_Rgba32_Issue_2924.png | 3 +++ tests/Images/Input/Png/issues/Issue_2924.png | 3 +++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/PngDecoderTests/CanDecode_Issue2924_Rgba32_Issue_2924.png create mode 100644 tests/Images/Input/Png/issues/Issue_2924.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index cef8396d4b..f6dad651b7 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -125,6 +125,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore /// private readonly int maxUncompressedLength; + /// + /// A value indicating whether the image data has been read. + /// + private bool hasImageData; + /// /// Initializes a new instance of the class. /// @@ -746,7 +751,11 @@ private void ReadScanlines( where TPixel : unmanaged, IPixel { using ZlibInflateStream inflateStream = new(this.currentStream, getData); - inflateStream.AllocateNewBytes(chunkLength, true); + if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData)) + { + return; + } + DeflateStream dataStream = inflateStream.CompressedStream!; if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) @@ -800,7 +809,7 @@ private void DecodePixelData( int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); if (bytesRead <= 0) { - return; + goto EXIT; } currentRowBytesRead += bytesRead; @@ -845,6 +854,7 @@ private void DecodePixelData( } EXIT: + this.hasImageData = true; blendMemory?.Dispose(); } @@ -903,7 +913,7 @@ private void DecodeInterlacedPixelData( int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); if (bytesRead <= 0) { - return; + goto EXIT; } currentRowBytesRead += bytesRead; @@ -976,6 +986,7 @@ private void DecodeInterlacedPixelData( } EXIT: + this.hasImageData = true; blendMemory?.Dispose(); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 11af57e39f..5a5dd9aaa7 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -705,10 +705,20 @@ public void Decode_BadPalette(string file) [Theory] [WithFile(TestImages.Png.Issue2752, PixelTypes.Rgba32)] public void CanDecodeJustOneFrame(TestImageProvider provider) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { DecoderOptions options = new() { MaxFrames = 1 }; using Image image = provider.GetImage(PngDecoder.Instance, options); Assert.Equal(1, image.Frames.Count); } + + [Theory] + [WithFile(TestImages.Png.Issue2924, PixelTypes.Rgba32)] + public void CanDecode_Issue2924(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6463f799b5..0c73860e9c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -160,6 +160,9 @@ public static class Png // Issue 2752: https://github.com/SixLabors/ImageSharp/issues/2752 public const string Issue2752 = "Png/issues/Issue_2752.png"; + // Issue 2924: https://github.com/SixLabors/ImageSharp/issues/2924 + public const string Issue2924 = "Png/issues/Issue_2924.png"; + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/CanDecode_Issue2924_Rgba32_Issue_2924.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/CanDecode_Issue2924_Rgba32_Issue_2924.png new file mode 100644 index 0000000000..023f346e03 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/CanDecode_Issue2924_Rgba32_Issue_2924.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4347cd89196c09496288724afdd876b227063149bba33615c338ebb474a0cb19 +size 47260 diff --git a/tests/Images/Input/Png/issues/Issue_2924.png b/tests/Images/Input/Png/issues/Issue_2924.png new file mode 100644 index 0000000000..0454642190 --- /dev/null +++ b/tests/Images/Input/Png/issues/Issue_2924.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd366a2de522041c706729b01a6030df6c82a4e87ea3509cc78a47486f097044 +size 49376 From 9e58dfb16152141178314d68bd3acf88e35d639c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 29 May 2025 22:41:56 +1000 Subject: [PATCH 70/73] Fix #2925 --- src/ImageSharp/Formats/Webp/WebpDecoderCore.cs | 13 ++++++++++--- .../Formats/WebP/WebpDecoderTests.cs | 10 ++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/issues/Issue2925.webp | 3 +++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/Images/Input/Webp/issues/Issue2925.webp diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 781b8246ff..21e1b55cfc 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -217,7 +217,7 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad else { // Ignore unknown chunks. - uint chunkSize = ReadChunkSize(stream, buffer); + uint chunkSize = ReadChunkSize(stream, buffer, false); stream.Skip((int)chunkSize); } } @@ -500,9 +500,10 @@ private static WebpChunkType ReadChunkType(BufferedReadStream stream, Span /// /// The stream to decode from. /// Temporary buffer. + /// If true, the chunk size is required to be read, otherwise it can be skipped. /// The chunk size in bytes. /// Invalid data. - private static uint ReadChunkSize(BufferedReadStream stream, Span buffer) + private static uint ReadChunkSize(BufferedReadStream stream, Span buffer, bool required = true) { if (stream.Read(buffer, 0, 4) == 4) { @@ -510,7 +511,13 @@ private static uint ReadChunkSize(BufferedReadStream stream, Span buffer) return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; } - throw new ImageFormatException("Invalid Webp data."); + if (required) + { + throw new ImageFormatException("Invalid Webp data."); + } + + // Return the size of the remaining data in the stream. + return (uint)(stream.Length - stream.Position); } /// diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 29362105db..d81cd20849 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -549,4 +549,14 @@ public void Identify_VerifyRatio(string imagePath) Assert.Equal(37.8, meta.VerticalResolution); Assert.Equal(PixelResolutionUnit.PixelsPerCentimeter, meta.ResolutionUnits); } + + [Theory] + [WithFile(Lossy.Issue2925, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Issue2925(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 0c73860e9c..5a7c2e66ea 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -859,6 +859,7 @@ public static class Lossy public const string Issue2763 = "Webp/issues/Issue2763.png"; public const string Issue2801 = "Webp/issues/Issue2801.webp"; public const string Issue2866 = "Webp/issues/Issue2866.webp"; + public const string Issue2925 = "Webp/issues/Issue2925.webp"; } } diff --git a/tests/Images/Input/Webp/issues/Issue2925.webp b/tests/Images/Input/Webp/issues/Issue2925.webp new file mode 100644 index 0000000000..414a06caad --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2925.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e12207babf122af7a62246938c2e78faa0d3f730edb3182f4f9d6adae6bfc602 +size 262144 From 3dd6bd66e842ef3f956fe671ac4047ceaf2527f6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 May 2025 09:49:55 +1000 Subject: [PATCH 71/73] Fix CI scripts --- .github/workflows/build-and-test.yml | 4 +++- .github/workflows/code-coverage.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d9e0f1e08f..ec3ebfa1dc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -68,7 +68,9 @@ jobs: steps: - name: Install libgdi+, which is required for tests running on ubuntu if: ${{ contains(matrix.options.os, 'ubuntu') }} - run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + run: | + sudo apt-get update + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index db9aca0b08..de95bf01b2 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -18,7 +18,9 @@ jobs: steps: - name: Install libgdi+, which is required for tests running on ubuntu - run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + run: | + sudo apt-get update + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash From 5310d9213663a6f1dc3aa38065495aa731af166d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 30 May 2025 12:41:22 +1000 Subject: [PATCH 72/73] Do not encode WEBP images exceeding max dimensions --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs | 3 --- src/ImageSharp/Formats/Webp/WebpEncoderCore.cs | 5 +++++ src/ImageSharp/Formats/Webp/WebpThrowHelper.cs | 3 +++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 4477df35cd..455227e889 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -54,7 +54,7 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - if (image.Width >= JpegConstants.MaxLength || image.Height >= JpegConstants.MaxLength) + if (image.Width > JpegConstants.MaxLength || image.Height > JpegConstants.MaxLength) { JpegThrowHelper.ThrowDimensionsTooLarge(image.Width, image.Height); } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 40009f525b..0351798b5f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -374,9 +374,6 @@ public int EncodeAlphaImageData(Buffer2DRegion frame, IMemoryOwn /// The input image height. private void WriteImageSize(int inputImgWidth, int inputImgHeight) { - Guard.MustBeLessThan(inputImgWidth, WebpConstants.MaxDimension, nameof(inputImgWidth)); - Guard.MustBeLessThan(inputImgHeight, WebpConstants.MaxDimension, nameof(inputImgHeight)); - uint width = (uint)inputImgWidth - 1; uint height = (uint)inputImgHeight - 1; diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 5d904380bf..4b273e6162 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -117,6 +117,11 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + if (image.Width > WebpConstants.MaxDimension || image.Height > WebpConstants.MaxDimension) + { + WebpThrowHelper.ThrowDimensionsTooLarge(image.Width, image.Height); + } + bool lossless; if (this.fileFormat is not null) { diff --git a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs index c633c52738..d730953829 100644 --- a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs +++ b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs @@ -18,4 +18,7 @@ internal static class WebpThrowHelper [DoesNotReturn] public static void ThrowInvalidImageDimensions(string errorMessage) => throw new InvalidImageContentException(errorMessage); + + [DoesNotReturn] + public static void ThrowDimensionsTooLarge(int width, int height) => throw new ImageFormatException($"Image is too large to encode at {width}x{height} for WEBP format."); } From 6205e0dcd2c1f14a70cb89cf64bab2733a07f0b5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Jun 2025 22:53:38 +1000 Subject: [PATCH 73/73] Remove ExifTagValue.InteroperabilityIndex --- .../Metadata/Profiles/Exif/Tags/ExifTagValue.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs index 56e8a3ffd1..07dbc51e7d 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -23,14 +23,6 @@ internal enum ExifTagValue /// GPSIFDOffset = 0x8825, - /// - /// Indicates the identification of the Interoperability rule. - /// See https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/interoperability/interoperabilityindex.html - /// - [ExifTagDescription("R98", "Indicates a file conforming to R98 file specification of Recommended Exif Interoperability Rules (ExifR98) or to DCF basic file stipulated by Design Rule for Camera File System.")] - [ExifTagDescription("THM", "Indicates a file conforming to DCF thumbnail file stipulated by Design rule for Camera File System.")] - InteroperabilityIndex = 0x0001, - /// /// A general indication of the kind of data contained in this subfile. /// See Section 8: Baseline Fields.