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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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.