From 1d994132efa2e461a8961bb19832cd5336e1913e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 15 Nov 2022 20:52:07 +1000 Subject: [PATCH] Re-introduce IImageDecoder and split decoding pipelines. --- src/ImageSharp/Advanced/AotCompilerTools.cs | 8 +- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 8 +- src/ImageSharp/Formats/Gif/GifDecoder.cs | 6 +- src/ImageSharp/Formats/IImageDecoder.cs | 73 +++++++ src/ImageSharp/Formats/IImageInfoDetector.cs | 23 --- .../Formats/ISpecializedImageDecoder{T}.cs | 56 ++++++ src/ImageSharp/Formats/ImageDecoder.cs | 178 +++++++++++++----- .../Formats/ImageDecoderExtensions.cs | 178 ------------------ src/ImageSharp/Formats/ImageFormatManager.cs | 16 +- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 8 +- src/ImageSharp/Formats/Pbm/PbmDecoder.cs | 6 +- src/ImageSharp/Formats/Png/PngDecoder.cs | 6 +- .../Formats/SpecializedImageDecoder{T}.cs | 91 +++++++++ src/ImageSharp/Formats/Tga/TgaDecoder.cs | 6 +- .../Decompressors/WebpTiffCompression.cs | 3 +- src/ImageSharp/Formats/Tiff/TiffDecoder.cs | 6 +- .../Formats/Tiff/TiffDecoderCore.cs | 2 +- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 6 +- src/ImageSharp/IO/ChunkedMemoryStream.cs | 1 + src/ImageSharp/Image.Decode.cs | 76 ++++++-- src/ImageSharp/Image.FromStream.cs | 77 ++++---- src/ImageSharp/Image.cs | 5 + .../Codecs/Jpeg/IdentifyJpeg.cs | 2 +- .../Formats/Bmp/BmpEncoderTests.cs | 10 +- .../Formats/Gif/GifMetadataTests.cs | 4 +- .../Formats/ImageFormatManagerTests.cs | 10 +- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 12 +- .../Formats/Png/PngEncoderTests.cs | 4 +- .../Formats/Tiff/TiffDecoderBaseTester.cs | 2 +- .../Formats/Tiff/TiffEncoderBaseTester.cs | 4 +- .../Image/ImageTests.ImageLoadTestBase.cs | 17 +- tests/ImageSharp.Tests/TestFile.cs | 6 +- tests/ImageSharp.Tests/TestFormat.cs | 8 +- .../ImageProviders/FileProvider.cs | 24 +-- .../ImageProviders/TestImageProvider.cs | 12 +- ...SharpPngEncoderWithDefaultConfiguration.cs | 2 +- .../ReferenceCodecs/MagickReferenceDecoder.cs | 12 +- .../SystemDrawingReferenceDecoder.cs | 6 +- .../TestUtilities/TestEnvironment.Formats.cs | 4 +- .../TestUtilities/TestImageExtensions.cs | 26 +-- .../Tests/MagickReferenceCodecTests.cs | 8 +- .../Tests/ReferenceDecoderBenchmarks.cs | 2 +- .../Tests/SystemDrawingReferenceCodecTests.cs | 2 +- .../Tests/TestEnvironmentTests.cs | 4 +- .../Tests/TestImageProviderTests.cs | 16 +- 45 files changed, 605 insertions(+), 431 deletions(-) create mode 100644 src/ImageSharp/Formats/IImageDecoder.cs delete mode 100644 src/ImageSharp/Formats/IImageInfoDetector.cs create mode 100644 src/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs delete mode 100644 src/ImageSharp/Formats/ImageDecoderExtensions.cs create mode 100644 src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index be0d057b2..00f7b1c3b 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -248,7 +248,7 @@ internal static class AotCompilerTools } /// - /// This method pre-seeds the all in the AoT compiler. + /// This method pre-seeds the all in the AoT compiler. /// /// The pixel format. [Preserve] @@ -280,15 +280,15 @@ internal static class AotCompilerTools } /// - /// This method pre-seeds the in the AoT compiler. + /// This method pre-seeds the in the AoT compiler. /// /// The pixel format. /// The decoder. [Preserve] private static void AotCompileImageDecoder() where TPixel : unmanaged, IPixel - where TDecoder : ImageDecoder - => default(TDecoder).Decode(default, default, default); + where TDecoder : IImageDecoder + => default(TDecoder).Decode(default, default); /// /// This method pre-seeds the all in the AoT compiler. diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index d56aaa1ac..8ef64b762 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp; public sealed class BmpDecoder : SpecializedImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -20,7 +20,7 @@ public sealed class BmpDecoder : SpecializedImageDecoder } /// - protected internal override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -33,10 +33,10 @@ public sealed class BmpDecoder : SpecializedImageDecoder } /// - protected internal override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(BmpDecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); /// - protected internal override BmpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + protected override BmpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 6ae877516..fa35e339b 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; public sealed class GifDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -20,7 +20,7 @@ public sealed class GifDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -34,6 +34,6 @@ public sealed class GifDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs new file mode 100644 index 000000000..86f4f3fa8 --- /dev/null +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for all image decoders. +/// +public interface IImageDecoder +{ + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The containing image data. + /// The object. + /// Thrown if the encoded image contains errors. + public IImageInfo Identify(DecoderOptions options, Stream stream); + + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The object. + /// Thrown if the encoded image contains errors. + public Task IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The general decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The general decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(DecoderOptions options, Stream stream); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The general decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken); +} diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs deleted file mode 100644 index ab5f536ff..000000000 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Encapsulates methods used for detecting the raw image information without fully decoding it. -/// -public interface IImageInfoDetector -{ - /// - /// Reads the raw image information from the specified stream. - /// - /// - /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. - /// - /// The general decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The object. - /// Thrown if the encoded image contains errors. - IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken); -} diff --git a/src/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs b/src/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs new file mode 100644 index 000000000..2bda864bc --- /dev/null +++ b/src/ImageSharp/Formats/ISpecializedImageDecoder{T}.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Defines the contract for an image decoder that supports specialized options. +/// +/// The type of specialized options. +public interface ISpecializedImageDecoder : IImageDecoder + where T : ISpecializedDecoderOptions +{ + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(T options, Stream stream) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The specialized decoder options. + /// The containing image data. + /// The . + /// Thrown if the encoded image contains errors. + public Image Decode(T options, Stream stream); + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// Thrown if the encoded image contains errors. + public Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken); +} diff --git a/src/ImageSharp/Formats/ImageDecoder.cs b/src/ImageSharp/Formats/ImageDecoder.cs index 000cb9923..42d476e92 100644 --- a/src/ImageSharp/Formats/ImageDecoder.cs +++ b/src/ImageSharp/Formats/ImageDecoder.cs @@ -1,15 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats; /// -/// The base class for all image decoders. +/// Acts as a base class for image decoders. +/// Types that inherit this decoder are required to implement cancellable synchronous decoding operations only. /// -public abstract class ImageDecoder +public abstract class ImageDecoder : IImageDecoder { /// /// Decodes the image from the specified stream to an of a specific pixel type. @@ -23,7 +25,7 @@ public abstract class ImageDecoder /// The token to monitor for cancellation requests. /// The . /// Thrown if the encoded image contains errors. - protected internal abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// @@ -37,7 +39,7 @@ public abstract class ImageDecoder /// The token to monitor for cancellation requests. /// The . /// Thrown if the encoded image contains errors. - protected internal abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken); + protected abstract Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken); /// /// Reads the raw image information from the specified stream. @@ -50,7 +52,7 @@ public abstract class ImageDecoder /// The token to monitor for cancellation requests. /// The object. /// Thrown if the encoded image contains errors. - protected internal abstract IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken); + protected abstract IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken); /// /// Performs a resize operation against the decoded image. If the target size is not set, or the image size @@ -90,56 +92,132 @@ public abstract class ImageDecoder Size currentSize = image.Size(); return currentSize.Width != targetSize.Width && currentSize.Height != targetSize.Height; } -} -/// -/// The base class for all specialized image decoders. -/// Specialized decoders allow for additional options to be passed to the decoder. -/// -/// The type of specialized options. -public abstract class SpecializedImageDecoder : ImageDecoder - where T : ISpecializedDecoderOptions -{ - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// - /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. - /// - /// The pixel format. - /// The specialized decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The . - /// Thrown if the encoded image contains errors. - protected internal abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel; + /// + public Image Decode(DecoderOptions options, Stream stream) + where TPixel : unmanaged, IPixel + => WithSeekableStream( + options, + stream, + s => this.Decode(options, s, default)); - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// - /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. - /// - /// The specialized decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The . - /// Thrown if the encoded image contains errors. - protected internal abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken); + /// + public Image Decode(DecoderOptions options, Stream stream) + => WithSeekableStream( + options, + stream, + s => this.Decode(options, s, default)); /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + public Task> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => WithSeekableStreamAsync( + options, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken); /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + public Task DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => WithSeekableStreamAsync( + options, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken); - /// - /// A factory method for creating the default specialized options. - /// - /// The general decoder options. - /// The new . - protected internal abstract T CreateDefaultSpecializedOptions(DecoderOptions options); + /// + public IImageInfo Identify(DecoderOptions options, Stream stream) + => WithSeekableStream( + options, + stream, + s => this.Identify(options, s, default)); + + /// + public Task IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => WithSeekableStreamAsync( + options, + stream, + (s, ct) => this.Identify(options, s, ct), + cancellationToken); + + internal static T WithSeekableStream( + DecoderOptions options, + Stream stream, + Func action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + T Action(Stream s, long position) + { + T result = action(s); + + // Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } + + return result; + } + + if (stream.CanSeek) + { + return Action(stream, stream.Position); + } + + Configuration configuration = options.Configuration; + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); + memoryStream.Position = 0; + + return Action(memoryStream, 0); + } + + internal static async Task WithSeekableStreamAsync( + DecoderOptions options, + Stream stream, + Func action, + CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + T Action(Stream s, long position, CancellationToken ct) + { + T result = action(s, ct); + + // Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } + + return result; + } + + // NOTE: We are explicitly not executing the action against the stream here as we do in WithSeekableStream() because that + // would incur synchronous IO reads which must be avoided in this asynchronous method. Instead, we will *always* run the + // code below to copy the stream to an in-memory buffer before invoking the action. + + // TODO: Avoid the existing double copy caused by calling IdentifyAsync followed by DecodeAsync. + // Perhaps we can make overloads accepting the chunked memorystream? + Configuration configuration = options.Configuration; + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); + memoryStream.Position = 0; + return Action(memoryStream, 0, cancellationToken); + } } diff --git a/src/ImageSharp/Formats/ImageDecoderExtensions.cs b/src/ImageSharp/Formats/ImageDecoderExtensions.cs deleted file mode 100644 index 26ceec3f6..000000000 --- a/src/ImageSharp/Formats/ImageDecoderExtensions.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats; - -/// -/// Extensions methods for and . -/// -public static class ImageDecoderExtensions -{ - /// - /// Reads the raw image information from the specified stream. - /// - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The object. - /// Thrown if the encoded image contains errors. - public static IImageInfo Identify(this ImageDecoder decoder, DecoderOptions options, Stream stream) - => Image.WithSeekableStream( - options, - stream, - s => decoder.Identify(options, s, default)); - - /// - /// Reads the raw image information from the specified stream. - /// - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// The object. - /// Thrown if the encoded image contains errors. - public static Task IdentifyAsync(this ImageDecoder decoder, DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) - => Image.WithSeekableStreamAsync( - options, - stream, - (s, ct) => decoder.Identify(options, s, ct), - cancellationToken); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The pixel format. - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The . - /// Thrown if the encoded image contains errors. - public static Image Decode(this ImageDecoder decoder, DecoderOptions options, Stream stream) - where TPixel : unmanaged, IPixel - => Image.WithSeekableStream( - options, - stream, - s => decoder.Decode(options, s, default)); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The . - /// Thrown if the encoded image contains errors. - public static Image Decode(this ImageDecoder decoder, DecoderOptions options, Stream stream) - => Image.WithSeekableStream( - options, - stream, - s => decoder.Decode(options, s, default)); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The pixel format. - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - /// Thrown if the encoded image contains errors. - public static Task> DecodeAsync(this ImageDecoder decoder, DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) - where TPixel : unmanaged, IPixel - => Image.WithSeekableStreamAsync( - options, - stream, - (s, ct) => decoder.Decode(options, s, ct), - cancellationToken); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The decoder. - /// The general decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - /// Thrown if the encoded image contains errors. - public static Task DecodeAsync(this ImageDecoder decoder, DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) - => Image.WithSeekableStreamAsync( - options, - stream, - (s, ct) => decoder.Decode(options, s, ct), - cancellationToken); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The type of specialized options. - /// The pixel format. - /// The decoder. - /// The specialized decoder options. - /// The containing image data. - /// The . - /// Thrown if the encoded image contains errors. - public static Image Decode(this SpecializedImageDecoder decoder, T options, Stream stream) - where T : ISpecializedDecoderOptions - where TPixel : unmanaged, IPixel - => Image.WithSeekableStream( - options.GeneralOptions, - stream, - s => decoder.Decode(options, s, default)); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The type of specialized options. - /// The decoder. - /// The specialized decoder options. - /// The containing image data. - /// The . - /// Thrown if the encoded image contains errors. - public static Image Decode(this SpecializedImageDecoder decoder, T options, Stream stream) - where T : ISpecializedDecoderOptions - => Image.WithSeekableStream( - options.GeneralOptions, - stream, - s => decoder.Decode(options, s, default)); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The type of specialized options. - /// The pixel format. - /// The decoder. - /// The specialized decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - /// Thrown if the encoded image contains errors. - public static Task> DecodeAsync(this SpecializedImageDecoder decoder, T options, Stream stream, CancellationToken cancellationToken = default) - where T : ISpecializedDecoderOptions - where TPixel : unmanaged, IPixel - => Image.WithSeekableStreamAsync( - options.GeneralOptions, - stream, - (s, ct) => decoder.Decode(options, s, ct), - cancellationToken); - - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The type of specialized options. - /// The decoder. - /// The specialized decoder options. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - /// Thrown if the encoded image contains errors. - public static Task DecodeAsync(this SpecializedImageDecoder decoder, T options, Stream stream, CancellationToken cancellationToken = default) - where T : ISpecializedDecoderOptions - => Image.WithSeekableStreamAsync( - options.GeneralOptions, - stream, - (s, ct) => decoder.Decode(options, s, ct), - cancellationToken); -} diff --git a/src/ImageSharp/Formats/ImageFormatManager.cs b/src/ImageSharp/Formats/ImageFormatManager.cs index efa6c435a..23229703c 100644 --- a/src/ImageSharp/Formats/ImageFormatManager.cs +++ b/src/ImageSharp/Formats/ImageFormatManager.cs @@ -22,9 +22,9 @@ public class ImageFormatManager private readonly ConcurrentDictionary mimeTypeEncoders = new(); /// - /// The list of supported keyed to mime types. + /// The list of supported keyed to mime types. /// - private readonly ConcurrentDictionary mimeTypeDecoders = new(); + private readonly ConcurrentDictionary mimeTypeDecoders = new(); /// /// The list of supported s. @@ -59,9 +59,9 @@ public class ImageFormatManager internal IEnumerable FormatDetectors => this.imageFormatDetectors; /// - /// Gets the currently registered s. + /// Gets the currently registered s. /// - internal IEnumerable> ImageDecoders => this.mimeTypeDecoders; + internal IEnumerable> ImageDecoders => this.mimeTypeDecoders; /// /// Gets the currently registered s. @@ -130,7 +130,7 @@ public class ImageFormatManager /// /// The image format to register the encoder for. /// The decoder to use, - public void SetDecoder(IImageFormat imageFormat, ImageDecoder decoder) + public void SetDecoder(IImageFormat imageFormat, IImageDecoder decoder) { Guard.NotNull(imageFormat, nameof(imageFormat)); Guard.NotNull(decoder, nameof(decoder)); @@ -158,12 +158,12 @@ public class ImageFormatManager /// For the specified mime type find the decoder. /// /// The format to discover - /// The if found otherwise null - public ImageDecoder FindDecoder(IImageFormat format) + /// The if found otherwise null + public IImageDecoder FindDecoder(IImageFormat format) { Guard.NotNull(format, nameof(format)); - return this.mimeTypeDecoders.TryGetValue(format, out ImageDecoder decoder) + return this.mimeTypeDecoders.TryGetValue(format, out IImageDecoder decoder) ? decoder : null; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 25ace3b8e..ac7187a3c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg; public sealed class JpegDecoder : SpecializedImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -21,7 +21,7 @@ public sealed class JpegDecoder : SpecializedImageDecoder } /// - protected internal override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -38,10 +38,10 @@ public sealed class JpegDecoder : SpecializedImageDecoder } /// - protected internal override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(JpegDecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); /// - protected internal override JpegDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + protected override JpegDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoder.cs b/src/ImageSharp/Formats/Pbm/PbmDecoder.cs index acfebdc84..e816dccb4 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoder.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; public sealed class PbmDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -36,7 +36,7 @@ public sealed class PbmDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -50,6 +50,6 @@ public sealed class PbmDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 617ca1c17..749b57720 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Png; public sealed class PngDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -20,7 +20,7 @@ public sealed class PngDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -34,7 +34,7 @@ public sealed class PngDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs b/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs new file mode 100644 index 000000000..90442689b --- /dev/null +++ b/src/ImageSharp/Formats/SpecializedImageDecoder{T}.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats; + +/// +/// Acts as a base class for specialized image decoders. +/// Specialized decoders allow for additional options to be passed to the decoder. +/// Types that inherit this decoder are required to implement cancellable synchronous decoding operations only. +/// +/// The type of specialized options. +public abstract class SpecializedImageDecoder : ImageDecoder, ISpecializedImageDecoder + where T : ISpecializedDecoderOptions +{ + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The pixel format. + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + + /// + /// Decodes the image from the specified stream to an of a specific pixel type. + /// + /// + /// This method is designed to support the ImageSharp internal infrastructure and is not recommended for direct use. + /// + /// The specialized decoder options. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// The . + /// Thrown if the encoded image contains errors. + protected abstract Image Decode(T options, Stream stream, CancellationToken cancellationToken); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); + + /// + /// A factory method for creating the default specialized options. + /// + /// The general decoder options. + /// The new . + protected abstract T CreateDefaultSpecializedOptions(DecoderOptions options); + + /// + public Image Decode(T options, Stream stream) + where TPixel : unmanaged, IPixel + => WithSeekableStream( + options.GeneralOptions, + stream, + s => this.Decode(options, s, default)); + + /// + public Image Decode(T options, Stream stream) + => WithSeekableStream( + options.GeneralOptions, + stream, + s => this.Decode(options, s, default)); + + /// + public Task> DecodeAsync(T options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => WithSeekableStreamAsync( + options.GeneralOptions, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken); + + /// + public Task DecodeAsync(T options, Stream stream, CancellationToken cancellationToken) + => WithSeekableStreamAsync( + options.GeneralOptions, + stream, + (s, ct) => this.Decode(options, s, ct), + cancellationToken); +} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index b6ae9bd57..af58163c0 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tga; public sealed class TgaDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -20,7 +20,7 @@ public sealed class TgaDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -34,6 +34,6 @@ public sealed class TgaDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index 04b57e297..a5ce4f842 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -32,7 +32,8 @@ internal class WebpTiffCompression : TiffBaseDecompressor /// protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { - using Image image = new WebpDecoder().Decode(this.options, stream, cancellationToken); + using WebpDecoderCore decoder = new(this.options); + using Image image = decoder.Decode(stream, cancellationToken); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs index a2d585a4c..f682daa66 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff; public class TiffDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -20,7 +20,7 @@ public class TiffDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -34,6 +34,6 @@ public class TiffDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 76b486416..3a80d7533 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -113,7 +113,7 @@ internal class TiffDecoderCore : IImageDecoderInternals public FaxCompressionOptions FaxCompressionOptions { get; set; } /// - /// Gets or sets the the logical order of bits within a byte. + /// Gets or sets the logical order of bits within a byte. /// public TiffFillOrder FillOrder { get; set; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index aa62ee709..dc1a7f2ee 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpDecoder : ImageDecoder { /// - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -21,7 +21,7 @@ public sealed class WebpDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); @@ -35,6 +35,6 @@ public sealed class WebpDecoder : ImageDecoder } /// - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index ecb299ca3..55271afcb 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -44,6 +44,7 @@ internal sealed class ChunkedMemoryStream : Stream /// /// Initializes a new instance of the class. /// + /// The memory allocator. public ChunkedMemoryStream(MemoryAllocator allocator) { Guard.NotNull(allocator, nameof(allocator)); diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 9e902c7bb..9616a8739 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -99,7 +99,7 @@ public abstract partial class Image /// The image stream to read the header from. /// The IImageFormat. /// The image format or null if none found. - private static ImageDecoder DiscoverDecoder(DecoderOptions options, Stream stream, out IImageFormat format) + private static IImageDecoder DiscoverDecoder(DecoderOptions options, Stream stream, out IImageFormat format) { format = InternalDetectFormat(options.Configuration, stream); @@ -113,36 +113,81 @@ public abstract partial class Image /// /// The general decoder options. /// The stream. - /// The token to monitor for cancellation requests. /// The pixel format. /// /// A new . /// - private static (Image Image, IImageFormat Format) Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + private static (Image Image, IImageFormat Format) Decode(DecoderOptions options, Stream stream) where TPixel : unmanaged, IPixel { - ImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + if (decoder is null) + { + return (null, null); + } + + Image img = decoder.Decode(options, stream); + return (img, format); + } + + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + if (decoder is null) + { + return (null, null); + } + + Image img = await decoder.DecodeAsync(options, stream, cancellationToken).ConfigureAwait(false); + return (img, format); + } + + private static (Image Image, IImageFormat Format) Decode(DecoderOptions options, Stream stream) + { + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); if (decoder is null) { return (null, null); } - Image img = decoder.Decode(options, stream, cancellationToken); + Image img = decoder.Decode(options, stream); return (img, format); } - private static (Image Image, IImageFormat Format) Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) { - ImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); if (decoder is null) { return (null, null); } - Image img = decoder.Decode(options, stream, cancellationToken); + Image img = await decoder.DecodeAsync(options, stream, cancellationToken).ConfigureAwait(false); return (img, format); } + /// + /// Reads the raw image information from the specified stream. + /// + /// The general decoder options. + /// The stream. + /// + /// The or null if a suitable info detector is not found. + /// + private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentify(DecoderOptions options, Stream stream) + { + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + IImageInfo info = decoder?.Identify(options, stream); + return (info, format); + } + /// /// Reads the raw image information from the specified stream. /// @@ -152,10 +197,19 @@ public abstract partial class Image /// /// The or null if a suitable info detector is not found. /// - private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) + private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentifyAsync( + DecoderOptions options, + Stream stream, + CancellationToken cancellationToken) { - ImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); - IImageInfo info = decoder?.Identify(options, stream, cancellationToken); + IImageDecoder decoder = DiscoverDecoder(options, stream, out IImageFormat format); + + if (decoder is null) + { + return (null, null); + } + + IImageInfo info = await decoder.IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false); return (info, format); } } diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 8cf7e0fe2..19d85cafb 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -62,7 +62,7 @@ public abstract partial class Image => WithSeekableStreamAsync( options, stream, - (s, _) => InternalDetectFormat(options.Configuration, s), + (s, _) => Task.FromResult(InternalDetectFormat(options.Configuration, s)), cancellationToken); /// @@ -160,7 +160,7 @@ public abstract partial class Image /// public static IImageInfo Identify(DecoderOptions options, Stream stream, out IImageFormat format) { - (IImageInfo ImageInfo, IImageFormat Format) data = WithSeekableStream(options, stream, s => InternalIdentity(options, s)); + (IImageInfo ImageInfo, IImageFormat Format) data = WithSeekableStream(options, stream, s => InternalIdentify(options, s)); format = data.Format; return data.ImageInfo; @@ -205,7 +205,7 @@ public abstract partial class Image => WithSeekableStreamAsync( options, stream, - (s, ct) => InternalIdentity(options, s, ct), + (s, ct) => InternalIdentifyAsync(options, s, ct), cancellationToken); /// @@ -289,11 +289,7 @@ public abstract partial class Image /// Image contains invalid content. /// A representing the asynchronous operation. public static async Task LoadAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default) - { - (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(options, stream, cancellationToken) - .ConfigureAwait(false); - return fmt.Image; - } + => (await LoadWithFormatAsync(options, stream, cancellationToken).ConfigureAwait(false)).Image; /// /// Create a new instance of the class from the given stream. @@ -416,7 +412,7 @@ public abstract partial class Image CancellationToken cancellationToken = default) { (Image Image, IImageFormat Format) data = - await WithSeekableStreamAsync(options, stream, (s, ct) => Decode(options, s, ct), cancellationToken) + await WithSeekableStreamAsync(options, stream, (s, ct) => DecodeAsync(options, s, ct), cancellationToken) .ConfigureAwait(false); if (data.Image is null) @@ -447,7 +443,7 @@ public abstract partial class Image where TPixel : unmanaged, IPixel { (Image Image, IImageFormat Format) data = - await WithSeekableStreamAsync(options, stream, (s, ct) => Decode(options, s, ct), cancellationToken) + await WithSeekableStreamAsync(options, stream, (s, ct) => DecodeAsync(options, s, ct), cancellationToken) .ConfigureAwait(false); if (data.Image is null) @@ -531,6 +527,20 @@ public abstract partial class Image throw new NotSupportedException("Cannot read from the stream."); } + T Action(Stream s, long position) + { + T result = action(s); + + // Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } + + return result; + } + Configuration configuration = options.Configuration; if (stream.CanSeek) { @@ -539,15 +549,14 @@ public abstract partial class Image stream.Position = 0; } - return action(stream); + return Action(stream, stream.Position); } - // We want to be able to load images from things like HttpContext.Request.Body using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); memoryStream.Position = 0; - return action(memoryStream); + return Action(memoryStream, 0); } /// @@ -563,7 +572,7 @@ public abstract partial class Image internal static async Task WithSeekableStreamAsync( DecoderOptions options, Stream stream, - Func action, + Func> action, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); @@ -574,34 +583,36 @@ public abstract partial class Image throw new NotSupportedException("Cannot read from the stream."); } - Configuration configuration = options.Configuration; - if (stream.CanSeek && configuration.ReadOrigin == ReadOrigin.Begin) + async Task Action(Stream s, long position, CancellationToken ct) { - stream.Position = 0; + T result = await action(s, ct).ConfigureAwait(false); - // NOTE: We are explicitly not executing the action against the stream here as we do in WithSeekableStream() because that - // would incur synchronous IO reads which must be avoided in this asynchronous method. Instead, we will *always* run the - // code below to copy the stream to an in-memory buffer before invoking the action. - } + // Our buffered reads may have left the stream in an incorrect non-zero position. + // Reset the position of the seekable stream if we did not read to the end to allow additional reads. + if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length) + { + stream.Position = position + s.Position; + } - using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); - await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); - memoryStream.Position = 0; + return result; + } - T Action(Stream ms, CancellationToken ct) + Configuration configuration = options.Configuration; + if (stream.CanSeek) { - // Reset the position of the seekable stream if we did not read to the end - // to allow additional reads. - T result = action(ms, ct); - if (stream.CanSeek && ms.Position != ms.Length) + if (configuration.ReadOrigin == ReadOrigin.Begin) { - stream.Position = ms.Position; + stream.Position = 0; } - return result; + return await Action(stream, stream.Position, cancellationToken).ConfigureAwait(false); } - return Action(memoryStream, cancellationToken); + using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); + memoryStream.Position = 0; + + return await Action(memoryStream, 0, cancellationToken).ConfigureAwait(false); } [DoesNotReturn] @@ -610,7 +621,7 @@ public abstract partial class Image StringBuilder sb = new(); sb.AppendLine("Image cannot be loaded. Available decoders:"); - foreach (KeyValuePair val in options.Configuration.ImageFormatsManager.ImageDecoders) + foreach (KeyValuePair val in options.Configuration.ImageFormatsManager.ImageDecoders) { sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index f2a534b5d..7094bd047 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -41,6 +41,11 @@ public abstract partial class Image : IImage, IConfigurationProvider /// /// Initializes a new instance of the class. /// + /// The configuration. + /// The . + /// The . + /// The width in px units. + /// The height in px units. internal Image( Configuration configuration, PixelTypeInfo pixelType, diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs index fc8beb687..79ec5efab 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs @@ -26,6 +26,6 @@ public class IdentifyJpeg { using MemoryStream memoryStream = new(this.jpegBytes); JpegDecoder decoder = new(); - return decoder.Identify(DecoderOptions.Default, memoryStream, default); + return decoder.Identify(DecoderOptions.Default, memoryStream); } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 14eff7ba9..4ff85cb8f 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -278,9 +278,9 @@ public class BmpEncoderTests // Use the default decoder to test our encoded image. This verifies the content. // We do not verify the reference image though as some are invalid. - ImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); - using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream, default); + using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream); referenceImage.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.01f), provider, @@ -309,9 +309,9 @@ public class BmpEncoderTests // Use the default decoder to test our encoded image. This verifies the content. // We do not verify the reference image though as some are invalid. - ImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); - using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream, default); + using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream); referenceImage.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.01f), provider, @@ -378,7 +378,7 @@ public class BmpEncoderTests bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header. IQuantizer quantizer = null, ImageComparer customComparer = null, - ImageDecoder referenceDecoder = null) + IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index 2d66842e8..4bd40dbcd 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -128,7 +128,7 @@ public class GifMetadataTests var testFile = TestFile.Create(imagePath); using var stream = new MemoryStream(testFile.Bytes, false); var decoder = new GifDecoder(); - IImageInfo image = await decoder.IdentifyAsync(DecoderOptions.Default, stream); + IImageInfo image = await decoder.IdentifyAsync(DecoderOptions.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); @@ -156,7 +156,7 @@ public class GifMetadataTests var testFile = TestFile.Create(imagePath); using var stream = new MemoryStream(testFile.Bytes, false); var decoder = new GifDecoder(); - using Image image = await decoder.DecodeAsync(DecoderOptions.Default, stream); + using Image image = await decoder.DecodeAsync(DecoderOptions.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 277863a58..cb2611c40 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -64,7 +64,7 @@ public class ImageFormatManagerTests [Fact] public void RegisterNullSetDecoder() { - Assert.Throws(() => this.DefaultFormatsManager.SetDecoder(null, new Mock().Object)); + Assert.Throws(() => this.DefaultFormatsManager.SetDecoder(null, new Mock().Object)); Assert.Throws(() => this.DefaultFormatsManager.SetDecoder(BmpFormat.Instance, null)); Assert.Throws(() => this.DefaultFormatsManager.SetDecoder(null, null)); } @@ -87,14 +87,14 @@ public class ImageFormatManagerTests [Fact] public void RegisterMimeTypeDecoderReplacesLast() { - ImageDecoder decoder1 = new Mock().Object; + IImageDecoder decoder1 = new Mock().Object; this.FormatsManagerEmpty.SetDecoder(TestFormat.GlobalTestFormat, decoder1); - ImageDecoder found = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat); + IImageDecoder found = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat); Assert.Equal(decoder1, found); - ImageDecoder decoder2 = new Mock().Object; + IImageDecoder decoder2 = new Mock().Object; this.FormatsManagerEmpty.SetDecoder(TestFormat.GlobalTestFormat, decoder2); - ImageDecoder found2 = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat); + IImageDecoder found2 = this.FormatsManagerEmpty.FindDecoder(TestFormat.GlobalTestFormat); Assert.Equal(decoder2, found2); Assert.NotEqual(found, found2); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index f88431be7..9f6b0d751 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -106,7 +106,7 @@ public partial class JpegDecoderTests var testFile = TestFile.Create(imagePath); using var stream = new MemoryStream(testFile.Bytes, false); var decoder = new JpegDecoder(); - IImageInfo image = await decoder.IdentifyAsync(DecoderOptions.Default, stream); + IImageInfo image = await decoder.IdentifyAsync(DecoderOptions.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); @@ -142,7 +142,7 @@ public partial class JpegDecoderTests { var testFile = TestFile.Create(imagePath); using var stream = new MemoryStream(testFile.Bytes, false); - using Image image = await JpegDecoder.DecodeAsync(DecoderOptions.Default, stream); + using Image image = await JpegDecoder.DecodeAsync(DecoderOptions.Default, stream, default); JpegMetadata meta = image.Metadata.GetJpegMetadata(); Assert.Equal(quality, meta.Quality); } @@ -178,25 +178,25 @@ public partial class JpegDecoderTests Assert.Equal(expectedColorType, meta.ColorType); } - private static void TestImageInfo(string imagePath, ImageDecoder decoder, bool useIdentify, Action test) + private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action test) { var testFile = TestFile.Create(imagePath); using var stream = new MemoryStream(testFile.Bytes, false); if (useIdentify) { - IImageInfo imageInfo = decoder.Identify(DecoderOptions.Default, stream, default); + IImageInfo imageInfo = decoder.Identify(DecoderOptions.Default, stream); test(imageInfo); } else { - using Image img = decoder.Decode(DecoderOptions.Default, stream, default); + using Image img = decoder.Decode(DecoderOptions.Default, stream); test(img); } } private static void TestMetadataImpl( bool useIdentify, - ImageDecoder decoder, + IImageDecoder decoder, string imagePath, int expectedPixelSize, bool exifProfilePresent, diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 0f33f2231..89e7e1159 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -588,7 +588,7 @@ public partial class PngEncoderTests string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType); // Compare to the Magick reference decoder. - ImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); // We compare using both our decoder and the reference decoder as pixel transformation // occurs within the encoder itself leaving the input image unaffected. @@ -598,7 +598,7 @@ public partial class PngEncoderTests fileStream.Position = 0; - using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, fileStream, default); + using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, fileStream); ImageComparer.Exact.VerifySimilarity(referenceImage, imageSharpImage); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs index 456604d8b..95f37ba40 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs @@ -16,7 +16,7 @@ public abstract class TiffDecoderBaseTester protected static MagickReferenceDecoder ReferenceDecoder => new(); - protected static void TestTiffDecoder(TestImageProvider provider, ImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) + protected static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(TiffDecoder); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs index 0b4016dd5..6da0d4fd8 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff; [Trait("Format", "Tiff")] public abstract class TiffEncoderBaseTester { - protected static readonly ImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); protected static void TestStripLength( TestImageProvider provider, @@ -85,7 +85,7 @@ public abstract class TiffEncoderBaseTester TiffPredictor predictor = TiffPredictor.None, bool useExactComparer = true, float compareTolerance = 0.001f, - ImageDecoder imageDecoder = null) + IImageDecoder imageDecoder = null) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index 3f51f1967..7a3296120 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -19,7 +19,7 @@ public partial class ImageTests protected Image localStreamReturnImageAgnostic; - protected Mock localDecoder; + protected Mock localDecoder; protected IImageFormatDetector localMimeTypeDetector; @@ -59,13 +59,16 @@ public partial class ImageTests this.localImageInfoMock = new Mock(); this.localImageFormatMock = new Mock(); - this.localDecoder = new Mock(); - this.localDecoder.Setup(x => x.Identify(It.IsAny(), It.IsAny(), It.IsAny())) + this.localDecoder = new Mock(); + this.localDecoder.Setup(x => x.Identify(It.IsAny(), It.IsAny())) .Returns(this.localImageInfoMock.Object); + this.localDecoder.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(this.localImageInfoMock.Object)); + this.localDecoder - .Setup(x => x.Decode(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((c, s, ct) => + .Setup(x => x.Decode(It.IsAny(), It.IsAny())) + .Callback((c, s) => { using var ms = new MemoryStream(); s.CopyTo(ms); @@ -74,8 +77,8 @@ public partial class ImageTests .Returns(this.localStreamReturnImageRgba32); this.localDecoder - .Setup(x => x.Decode(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((c, s, ct) => + .Setup(x => x.Decode(It.IsAny(), It.IsAny())) + .Callback((c, s) => { using var ms = new MemoryStream(); s.CopyTo(ms); diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs index fc95d5fa8..72298837c 100644 --- a/tests/ImageSharp.Tests/TestFile.cs +++ b/tests/ImageSharp.Tests/TestFile.cs @@ -147,7 +147,7 @@ public sealed class TestFile /// /// The . /// - public Image CreateRgba32Image(ImageDecoder decoder) + public Image CreateRgba32Image(IImageDecoder decoder) => this.CreateRgba32Image(decoder, new()); /// @@ -158,10 +158,10 @@ public sealed class TestFile /// /// The . /// - public Image CreateRgba32Image(ImageDecoder decoder, DecoderOptions options) + public Image CreateRgba32Image(IImageDecoder decoder, DecoderOptions options) { options.Configuration = this.Image.GetConfiguration(); using MemoryStream stream = new(this.Bytes); - return decoder.Decode(options, stream, default); + return decoder.Decode(options, stream); } } diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index e195bc660..eaf0a5df9 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -201,13 +201,13 @@ public class TestFormat : IConfigurationModule, IImageFormat public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header); - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - protected internal override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + protected override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; - protected internal override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) { Configuration configuration = options.GeneralOptions.Configuration; var ms = new MemoryStream(); @@ -224,7 +224,7 @@ public class TestFormat : IConfigurationModule, IImageFormat return this.testFormat.Sample(); } - protected internal override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index e9b7b8407..10126aab7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -27,7 +27,7 @@ public abstract partial class TestImageProvider : IXunitSerializable public Key( PixelTypes pixelType, string filePath, - ImageDecoder customDecoder, + IImageDecoder customDecoder, DecoderOptions options, ISpecializedDecoderOptions specialized) { @@ -175,11 +175,11 @@ public abstract partial class TestImageProvider : IXunitSerializable public override Image GetImage() { - ImageDecoder decoder = TestEnvironment.GetReferenceDecoder(this.FilePath); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(this.FilePath); return this.GetImage(decoder); } - public override Image GetImage(ImageDecoder decoder, DecoderOptions options) + public override Image GetImage(IImageDecoder decoder, DecoderOptions options) { Guard.NotNull(decoder, nameof(decoder)); Guard.NotNull(options, nameof(options)); @@ -202,7 +202,7 @@ public abstract partial class TestImageProvider : IXunitSerializable return cachedImage.Clone(this.Configuration); } - public override Task> GetImageAsync(ImageDecoder decoder, DecoderOptions options) + public override async Task> GetImageAsync(IImageDecoder decoder, DecoderOptions options) { Guard.NotNull(decoder, nameof(decoder)); Guard.NotNull(options, nameof(options)); @@ -213,10 +213,10 @@ public abstract partial class TestImageProvider : IXunitSerializable // TODO: Check Path here. Why combined? string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.FilePath); using Stream stream = System.IO.File.OpenRead(path); - return Task.FromResult(decoder.Decode(options, stream, default)); + return await decoder.DecodeAsync(options, stream, default); } - public override Image GetImage(SpecializedImageDecoder decoder, T options) + public override Image GetImage(ISpecializedImageDecoder decoder, T options) { Guard.NotNull(decoder, nameof(decoder)); Guard.NotNull(options, nameof(options)); @@ -239,7 +239,7 @@ public abstract partial class TestImageProvider : IXunitSerializable return cachedImage.Clone(this.Configuration); } - public override Task> GetImageAsync(SpecializedImageDecoder decoder, T options) + public override async Task> GetImageAsync(ISpecializedImageDecoder decoder, T options) { Guard.NotNull(decoder, nameof(decoder)); Guard.NotNull(options, nameof(options)); @@ -250,7 +250,7 @@ public abstract partial class TestImageProvider : IXunitSerializable // TODO: Check Path here. Why combined? string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.FilePath); using Stream stream = System.IO.File.OpenRead(path); - return Task.FromResult(decoder.Decode(options, stream, default)); + return await decoder.DecodeAsync(options, stream, default); } public override void Deserialize(IXunitSerializationInfo info) @@ -266,23 +266,23 @@ public abstract partial class TestImageProvider : IXunitSerializable info.AddValue("path", this.FilePath); } - private Image DecodeImage(ImageDecoder decoder, DecoderOptions options) + private Image DecodeImage(IImageDecoder decoder, DecoderOptions options) { options.Configuration = this.Configuration; var testFile = TestFile.Create(this.FilePath); using Stream stream = new MemoryStream(testFile.Bytes); - return decoder.Decode(options, stream, default); + return decoder.Decode(options, stream); } - private Image DecodeImage(SpecializedImageDecoder decoder, T options) + private Image DecodeImage(ISpecializedImageDecoder decoder, T options) where T : class, ISpecializedDecoderOptions, new() { options.GeneralOptions.Configuration = this.Configuration; var testFile = TestFile.Create(this.FilePath); using Stream stream = new MemoryStream(testFile.Bytes); - return decoder.Decode(options, stream, default); + return decoder.Decode(options, stream); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index 48e63bdca..8f22fb2b2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -87,23 +87,23 @@ public abstract partial class TestImageProvider : ITestImageProvider, IX /// A test image. public abstract Image GetImage(); - public Image GetImage(ImageDecoder decoder) + public Image GetImage(IImageDecoder decoder) => this.GetImage(decoder, new()); - public Task> GetImageAsync(ImageDecoder decoder) + public Task> GetImageAsync(IImageDecoder decoder) => this.GetImageAsync(decoder, new()); - public virtual Image GetImage(ImageDecoder decoder, DecoderOptions options) + public virtual Image GetImage(IImageDecoder decoder, DecoderOptions options) => throw new NotSupportedException($"Decoder specific GetImage() is not supported with {this.GetType().Name}!"); - public virtual Task> GetImageAsync(ImageDecoder decoder, DecoderOptions options) + public virtual Task> GetImageAsync(IImageDecoder decoder, DecoderOptions options) => throw new NotSupportedException($"Decoder specific GetImageAsync() is not supported with {this.GetType().Name}!"); - public virtual Image GetImage(SpecializedImageDecoder decoder, T options) + public virtual Image GetImage(ISpecializedImageDecoder decoder, T options) where T : class, ISpecializedDecoderOptions, new() => throw new NotSupportedException($"Decoder specific GetImage() is not supported with {this.GetType().Name}!"); - public virtual Task> GetImageAsync(SpecializedImageDecoder decoder, T options) + public virtual Task> GetImageAsync(ISpecializedImageDecoder decoder, T options) where T : class, ISpecializedDecoderOptions, new() => throw new NotSupportedException($"Decoder specific GetImageAsync() is not supported with {this.GetType().Name}!"); diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs index 1290fdea3..8d7b5042f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs @@ -45,6 +45,6 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder // IDisposable means you must use async/await, where the compiler generates the // state machine and a continuation. using PngEncoderCore encoder = new(allocator, configuration, this); - await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); + await encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 0096a29d8..7203116c9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -24,7 +24,7 @@ public class MagickReferenceDecoder : ImageDecoder public static MagickReferenceDecoder Instance { get; } = new(); - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Configuration configuration = options.Configuration; BmpReadDefines bmpReadDefines = new() @@ -32,8 +32,10 @@ public class MagickReferenceDecoder : ImageDecoder IgnoreFileSize = !this.validate }; - MagickReadSettings settings = new(); - settings.FrameCount = (int)options.MaxFrames; + MagickReadSettings settings = new() + { + FrameCount = (int)options.MaxFrames + }; settings.SetDefines(bmpReadDefines); using MagickImageCollection magickImageCollection = new(stream, settings); @@ -67,10 +69,10 @@ public class MagickReferenceDecoder : ImageDecoder return new Image(configuration, new ImageMetadata(), framesList); } - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 6d8104059..503fd53ce 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -13,14 +13,14 @@ public class SystemDrawingReferenceDecoder : ImageDecoder { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { using SDBitmap sourceBitmap = new(stream); PixelTypeInfo pixelType = new(SDImage.GetPixelFormatSize(sourceBitmap.PixelFormat)); return new ImageInfo(pixelType, sourceBitmap.Width, sourceBitmap.Height, new ImageMetadata()); } - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { using SDBitmap sourceBitmap = new(stream); if (sourceBitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb) @@ -45,6 +45,6 @@ public class SystemDrawingReferenceDecoder : ImageDecoder return SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(convertedBitmap); } - protected internal override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index fb76d3b68..41b625959 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -20,7 +20,7 @@ public static partial class TestEnvironment internal static Configuration Configuration => ConfigurationLazy.Value; - internal static ImageDecoder GetReferenceDecoder(string filePath) + internal static IImageDecoder GetReferenceDecoder(string filePath) { IImageFormat format = GetImageFormat(filePath); return Configuration.ImageFormatsManager.FindDecoder(format); @@ -42,7 +42,7 @@ public static partial class TestEnvironment private static void ConfigureCodecs( this Configuration cfg, IImageFormat imageFormat, - ImageDecoder decoder, + IImageDecoder decoder, ImageEncoder encoder, IImageFormatDetector detector) { diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index d6a025fc4..884320476 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -214,7 +214,7 @@ public static class TestImageExtensions bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, - ImageDecoder decoder = null) + IImageDecoder decoder = null) where TPixel : unmanaged, IPixel { using (Image referenceImage = GetReferenceOutputImage( @@ -307,7 +307,7 @@ public static class TestImageExtensions string extension = "png", bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, - ImageDecoder decoder = null) + IImageDecoder decoder = null) where TPixel : unmanaged, IPixel { string referenceOutputFile = provider.Utility.GetReferenceOutputFileName( @@ -324,7 +324,7 @@ public static class TestImageExtensions decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile); using FileStream stream = File.OpenRead(referenceOutputFile); - return decoder.Decode(DecoderOptions.Default, stream, default); + return decoder.Decode(DecoderOptions.Default, stream); } public static Image GetReferenceOutputImageMultiFrame( @@ -343,17 +343,17 @@ public static class TestImageExtensions var temporaryFrameImages = new List>(); - ImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); foreach (string path in frameFiles) { if (!File.Exists(path)) { - throw new Exception("Reference output file missing: " + path); + throw new FileNotFoundException("Reference output file missing: " + path); } using FileStream stream = File.OpenRead(path); - Image tempImage = decoder.Decode(DecoderOptions.Default, stream, default); + Image tempImage = decoder.Decode(DecoderOptions.Default, stream); temporaryFrameImages.Add(tempImage); } @@ -511,7 +511,7 @@ public static class TestImageExtensions public static Image CompareToOriginal( this Image image, ITestImageProvider provider, - ImageDecoder referenceDecoder = null) + IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel => CompareToOriginal(image, provider, ImageComparer.Tolerant(), referenceDecoder); @@ -519,7 +519,7 @@ public static class TestImageExtensions this Image image, ITestImageProvider provider, ImageComparer comparer, - ImageDecoder referenceDecoder = null, + IImageDecoder referenceDecoder = null, DecoderOptions referenceDecoderOptions = null) where TPixel : unmanaged, IPixel { @@ -534,7 +534,7 @@ public static class TestImageExtensions referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream, default)) + using (Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream)) { comparer.VerifySimilarity(original, image); } @@ -546,7 +546,7 @@ public static class TestImageExtensions this Image image, ITestImageProvider provider, ImageComparer comparer, - ImageDecoder referenceDecoder = null) + IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { string path = TestImageProvider.GetFilePathOrNull(provider); @@ -560,7 +560,7 @@ public static class TestImageExtensions referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); - using (Image original = referenceDecoder.Decode(DecoderOptions.Default, stream, default)) + using (Image original = referenceDecoder.Decode(DecoderOptions.Default, stream)) { comparer.VerifySimilarity(original, image); } @@ -668,7 +668,7 @@ public static class TestImageExtensions ImageComparer customComparer = null, bool appendPixelTypeToFileName = true, string referenceImageExtension = null, - ImageDecoder referenceDecoder = null) + IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { string actualOutputFile = provider.Utility.SaveTestOutputFile( @@ -681,7 +681,7 @@ public static class TestImageExtensions referenceDecoder ??= TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); - using Image encodedImage = referenceDecoder.Decode(DecoderOptions.Default, stream, default); + using Image encodedImage = referenceDecoder.Decode(DecoderOptions.Default, stream); ImageComparer comparer = customComparer ?? ImageComparer.Exact; comparer.VerifySimilarity(encodedImage, image); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs index 3d4dd4a65..81ea77b6b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs @@ -40,8 +40,8 @@ public class MagickReferenceCodecTests using FileStream mStream = File.OpenRead(path); using FileStream sdStream = File.OpenRead(path); - using Image mImage = magickDecoder.Decode(DecoderOptions.Default, mStream, default); - using Image sdImage = sdDecoder.Decode(DecoderOptions.Default, sdStream, default); + using Image mImage = magickDecoder.Decode(DecoderOptions.Default, mStream); + using Image sdImage = sdDecoder.Decode(DecoderOptions.Default, sdStream); ImageSimilarityReport report = comparer.CompareImagesOrFrames(mImage, sdImage); @@ -71,8 +71,8 @@ public class MagickReferenceCodecTests var comparer = ImageComparer.TolerantPercentage(1, 1020); using FileStream mStream = File.OpenRead(path); using FileStream sdStream = File.OpenRead(path); - using Image mImage = magickDecoder.Decode(DecoderOptions.Default, mStream, default); - using Image sdImage = sdDecoder.Decode(DecoderOptions.Default, sdStream, default); + using Image mImage = magickDecoder.Decode(DecoderOptions.Default, mStream); + using Image sdImage = sdDecoder.Decode(DecoderOptions.Default, sdStream); ImageSimilarityReport report = comparer.CompareImagesOrFrames(mImage, sdImage); mImage.DebugSave(dummyProvider); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs index f150b6e3c..3a3fcefdb 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceDecoderBenchmarks.cs @@ -74,7 +74,7 @@ public class ReferenceDecoderBenchmarks this.BenchmarkDecoderImpl(BmpBenchmarkFiles, new SystemDrawingReferenceDecoder(), "System.Drawing Decode Bmp"); } - private void BenchmarkDecoderImpl(IEnumerable testFiles, ImageDecoder decoder, string info, int times = DefaultExecutionCount) + private void BenchmarkDecoderImpl(IEnumerable testFiles, IImageDecoder decoder, string info, int times = DefaultExecutionCount) { var measure = new MeasureFixture(this.Output); measure.Measure( diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs index 91df7d149..a89feb3c3 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/SystemDrawingReferenceCodecTests.cs @@ -93,7 +93,7 @@ public class SystemDrawingReferenceCodecTests { string path = TestFile.GetInputFileFullPath(TestImages.Png.Splash); using FileStream stream = File.OpenRead(path); - using Image image = SystemDrawingReferenceDecoder.Instance.Decode(DecoderOptions.Default, stream, default); + using Image image = SystemDrawingReferenceDecoder.Instance.Decode(DecoderOptions.Default, stream); image.DebugSave(dummyProvider); } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs index fc900aa90..b879ec649 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs @@ -79,7 +79,7 @@ public class TestEnvironmentTests return; } - ImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); Assert.IsType(expectedDecoderType, decoder); } @@ -113,7 +113,7 @@ public class TestEnvironmentTests return; } - ImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); + IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); Assert.IsType(expectedDecoderType, decoder); } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 96b62d828..13edd2a06 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -363,19 +363,19 @@ public class TestImageProviderTests } } - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - protected internal override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) { InvocationCounts[this.callerName]++; return new Image(42, 42); } - protected internal override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); - protected internal override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + protected override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName]; @@ -403,19 +403,19 @@ public class TestImageProviderTests } } - protected internal override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override IImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken); - protected internal override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) { InvocationCounts[this.callerName]++; return new Image(42, 42); } - protected internal override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(TestDecoderWithParametersOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); - protected internal override TestDecoderWithParametersOptions CreateDefaultSpecializedOptions(DecoderOptions options) + protected override TestDecoderWithParametersOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new() { GeneralOptions = options }; internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName];