diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index 5791c6e92..c5abbda61 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -73,9 +74,10 @@ namespace SixLabors.ImageSharp.Advanced /// /// The source image. /// The image visitor. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor) - => source.AcceptAsync(visitor); + public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) + => source.AcceptAsync(visitor, cancellationToken); /// /// Gets the configuration for the image. diff --git a/src/ImageSharp/Advanced/IImageVisitor.cs b/src/ImageSharp/Advanced/IImageVisitor.cs index fbfdafeb1..ccff18026 100644 --- a/src/ImageSharp/Advanced/IImageVisitor.cs +++ b/src/ImageSharp/Advanced/IImageVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -31,9 +32,10 @@ namespace SixLabors.ImageSharp.Advanced /// Provides a pixel-specific implementation for a given operation. /// /// The image. + /// The token to monitor for cancellation requests. /// The pixel type. /// A representing the asynchronous operation. - Task VisitAsync(Image image) + Task VisitAsync(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs index 3b8641532..e4713e237 100644 --- a/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs +++ b/src/ImageSharp/Common/Exceptions/InvalidImageContentException.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp { @@ -32,5 +33,10 @@ namespace SixLabors.ImageSharp : base(errorMessage, innerException) { } + + internal InvalidImageContentException(Size size, InvalidMemoryOperationException memoryException) + : this($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {size.Width}x{size.Height}.", memoryException) + { + } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index cb26ff606..129b3a1aa 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -36,18 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp Guard.NotNull(stream, nameof(stream)); var decoder = new BmpDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); - } + return decoder.Decode(configuration, stream); } /// @@ -55,46 +45,34 @@ namespace SixLabors.ImageSharp.Formats.Bmp => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new BmpDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new BmpDecoderCore(configuration, this).Identify(bufferedStream); + return new BmpDecoderCore(configuration, this).Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new BmpDecoderCore(configuration, this).IdentifyAsync(bufferedStream); + return new BmpDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index ea8fd11a8..6f9223637 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -118,7 +119,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { try @@ -197,7 +198,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadImageHeaders(stream, out _, out _); return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metadata); diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 08c9bde00..2f5c4b7cf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -42,11 +43,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index b3f64eea6..eb29c4405 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; @@ -19,7 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Image encoder for writing an image to a stream as a Windows bitmap. /// - internal sealed class BmpEncoderCore + internal sealed class BmpEncoderCore : IImageEncoderInternals { /// /// The amount to pad each row by. @@ -97,32 +98,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 2b7103072..196d77ad7 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -30,21 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Gif where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -52,30 +39,17 @@ namespace SixLabors.ImageSharp.Formats.Gif => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - GifThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) @@ -85,18 +59,16 @@ namespace SixLabors.ImageSharp.Formats.Gif var decoder = new GifDecoderCore(configuration, this); using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Identify(bufferedStream); + return decoder.Identify(bufferedStream, default); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); var decoder = new GifDecoderCore(configuration, this); - - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.IdentifyAsync(bufferedStream); + return decoder.IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 78ffee8bd..8f5cc3b5c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -6,6 +6,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -97,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator; /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Image image = null; @@ -158,7 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { try { diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 539ab0fb3..116ee3dae 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -41,11 +42,11 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new GifEncoderCore(image.GetConfiguration(), this); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 556ace203..070864e60 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Implements the GIF encoding protocol. /// - internal sealed class GifEncoderCore + internal sealed class GifEncoderCore : IImageEncoderInternals { /// /// Used for allocating memory during processing operations. @@ -75,25 +76,9 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); diff --git a/src/ImageSharp/Formats/Gif/ImageExtensions.cs b/src/ImageSharp/Formats/Gif/ImageExtensions.cs deleted file mode 100644 index d262b056c..000000000 --- a/src/ImageSharp/Formats/Gif/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Gif; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsGif(this Image source, Stream stream) => SaveAsGif(source, stream, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, Stream stream) => SaveAsGifAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - - /// - /// Saves the image to the given stream with the gif format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index 97886e526..b55f1119b 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -38,9 +39,10 @@ namespace SixLabors.ImageSharp.Formats /// The pixel format. /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task> DecodeAsync(Configuration configuration, Stream stream) + Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// @@ -48,8 +50,9 @@ namespace SixLabors.ImageSharp.Formats /// /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task DecodeAsync(Configuration configuration, Stream stream); + Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/IImageDecoderInternals.cs b/src/ImageSharp/Formats/IImageDecoderInternals.cs index 33748bf24..e190f7add 100644 --- a/src/ImageSharp/Formats/IImageDecoderInternals.cs +++ b/src/ImageSharp/Formats/IImageDecoderInternals.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; @@ -17,21 +18,36 @@ namespace SixLabors.ImageSharp.Formats /// Configuration Configuration { get; } + /// + /// Gets the dimensions of the image being decoded. + /// + Size Dimensions { get; } + /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The stream, where the image should be decoded from. Cannot be null. + /// The token to monitor for cancellation requests. /// is null. /// The decoded image. - Image Decode(BufferedReadStream stream) + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// /// Reads the raw image information from the specified stream. /// /// The containing image data. + /// The token to monitor for cancellation requests. /// The . - IImageInfo Identify(BufferedReadStream stream); + /// + /// Cancellable synchronous method. In case of cancellation, + /// an shall be thrown which will be handled on the call site. + /// + IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/IImageEncoder.cs b/src/ImageSharp/Formats/IImageEncoder.cs index 646e0ecc0..e5a1b1c83 100644 --- a/src/ImageSharp/Formats/IImageEncoder.cs +++ b/src/ImageSharp/Formats/IImageEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -27,8 +28,9 @@ namespace SixLabors.ImageSharp.Formats /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - Task EncodeAsync(Image image, Stream stream) + Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; } } diff --git a/src/ImageSharp/Formats/IImageEncoderInternals.cs b/src/ImageSharp/Formats/IImageEncoderInternals.cs new file mode 100644 index 000000000..d44ac45f2 --- /dev/null +++ b/src/ImageSharp/Formats/IImageEncoderInternals.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats +{ + /// + /// Abstraction for shared internals for ***DecoderCore implementations to be used with . + /// + internal interface IImageEncoderInternals + { + /// + /// Encodes the image. + /// + /// The image. + /// The stream. + /// The token to monitor for cancellation requests. + /// The pixel type. + void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel; + } +} diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 862c64999..6f5fc2333 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; namespace SixLabors.ImageSharp.Formats @@ -24,7 +25,8 @@ namespace SixLabors.ImageSharp.Formats /// /// The configuration for the image. /// The containing image data. + /// The token to monitor for cancellation requests. /// The object - Task IdentifyAsync(Configuration configuration, Stream stream); + Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 9d1639a09..5d77fb0c8 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -14,21 +17,159 @@ namespace SixLabors.ImageSharp.Formats /// Reads the raw image information from the specified stream. /// /// The decoder. - /// The containing image data. + /// /// The configuration for the image. + /// The containing image data. + /// The token to monitor for cancellation requests. /// is null. /// A representing the asynchronous operation. - public static Task IdentifyAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) - => Task.FromResult(decoder.Identify(stream)); + public static Task IdentifyAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + => decoder.IdentifyAsync(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); + + /// + /// Reads the raw image information from the specified stream. + /// + /// The decoder. + /// The configuration for the image. + /// The containing image data. + /// Factory method to handle as . + /// The token to monitor for cancellation requests. + /// is null. + /// A representing the asynchronous operation. + public static Task IdentifyAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func tooLargeImageExceptionFactory, + CancellationToken cancellationToken) + { + try + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + IImageInfo imageInfo = decoder.Identify(bufferedReadStream, cancellationToken); + return Task.FromResult(imageInfo); + } + catch (InvalidMemoryOperationException ex) + { + InvalidImageContentException invalidImageContentException = tooLargeImageExceptionFactory(ex, decoder.Dimensions); + return Task.FromException(invalidImageContentException); + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Decodes the image from the specified stream. + /// + /// The pixel format. + /// The decoder. + /// The configuration for the image. + /// The containing image data. + /// The token to monitor for cancellation requests. + /// A representing the asynchronous operation. + public static Task> DecodeAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => + decoder.DecodeAsync( + configuration, + stream, + DefaultLargeImageExceptionFactory, + cancellationToken); /// /// Decodes the image from the specified stream. /// /// The pixel format. /// The decoder. - /// The containing image data. + /// The configuration for the image. + /// The containing image data. + /// Factory method to handle as . + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public static Task> DecodeAsync(this IImageDecoderInternals decoder, BufferedReadStream stream) + public static Task> DecodeAsync( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func largeImageExceptionFactory, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - => Task.FromResult(decoder.Decode(stream)); + { + try + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + Image image = decoder.Decode(bufferedReadStream, cancellationToken); + return Task.FromResult(image); + } + catch (InvalidMemoryOperationException ex) + { + InvalidImageContentException invalidImageContentException = largeImageExceptionFactory(ex, decoder.Dimensions); + return Task.FromException>(invalidImageContentException); + } + catch (OperationCanceledException) + { + return Task.FromCanceled>(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException>(ex); + } + } + + public static IImageInfo Identify( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream) + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + + try + { + return decoder.Identify(bufferedReadStream, default); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(decoder.Dimensions, ex); + } + } + + public static Image Decode(this IImageDecoderInternals decoder, Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory); + + public static Image Decode( + this IImageDecoderInternals decoder, + Configuration configuration, + Stream stream, + Func largeImageExceptionFactory) + where TPixel : unmanaged, IPixel + { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + + try + { + return decoder.Decode(bufferedReadStream, default); + } + catch (InvalidMemoryOperationException ex) + { + throw largeImageExceptionFactory(ex, decoder.Dimensions); + } + } + + private static InvalidImageContentException DefaultLargeImageExceptionFactory( + InvalidMemoryOperationException memoryOperationException, + Size dimensions) => + new InvalidImageContentException(dimensions, memoryOperationException); } } diff --git a/src/ImageSharp/Formats/ImageEncoderUtilities.cs b/src/ImageSharp/Formats/ImageEncoderUtilities.cs new file mode 100644 index 000000000..896fffa6f --- /dev/null +++ b/src/ImageSharp/Formats/ImageEncoderUtilities.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats +{ + internal static class ImageEncoderUtilities + { + public static async Task EncodeAsync( + this IImageEncoderInternals encoder, + Image image, + Stream stream, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Configuration configuration = image.GetConfiguration(); + if (stream.CanSeek) + { + await DoEncodeAsync(stream).ConfigureAwait(false); + } + else + { + using var ms = new MemoryStream(); + await DoEncodeAsync(ms); + ms.Position = 0; + await ms.CopyToAsync(stream, configuration.StreamProcessingBufferSize, cancellationToken) + .ConfigureAwait(false); + } + + Task DoEncodeAsync(Stream innerStream) + { + try + { + encoder.Encode(image, innerStream, cancellationToken); + return Task.CompletedTask; + } + catch (OperationCanceledException) + { + return Task.FromCanceled(cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + } + + public static void Encode( + this IImageEncoderInternals encoder, + Image image, + Stream stream) + where TPixel : unmanaged, IPixel + => encoder.Encode(image, stream, default); + } +} diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs new file mode 100644 index 000000000..075c708b6 --- /dev/null +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -0,0 +1,539 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; + +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsBmpAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsBmp(this Image source, Stream stream) + => SaveAsBmp(source, stream, null); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsBmpAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Bmp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path) => SaveAsGif(source, path, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path) => SaveAsGifAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsGifAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsGif(this Image source, Stream stream) + => SaveAsGif(source, stream, null); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsGifAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); + + /// + /// Saves the image to the given stream with the Gif format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsJpegAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsJpeg(this Image source, Stream stream) + => SaveAsJpeg(source, stream, null); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsJpegAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); + + /// + /// Saves the image to the given stream with the Jpeg format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsPngAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsPng(this Image source, Stream stream) + => SaveAsPng(source, stream, null); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsPngAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); + + /// + /// Saves the image to the given stream with the Png format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsTgaAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsTga(this Image source, Stream stream) + => SaveAsTga(source, stream, null); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsTgaAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); + + /// + /// Saves the image to the given stream with the Tga format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), + cancellationToken); + + } +} diff --git a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs b/src/ImageSharp/Formats/ImageExtensions.Save.tt similarity index 51% rename from src/ImageSharp/Formats/Bmp/ImageExtensions.cs rename to src/ImageSharp/Formats/ImageExtensions.Save.tt index 8d97c8b46..63b404cc4 100644 --- a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -1,10 +1,32 @@ +<#@ template language="C#" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +// using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Bmp; + +<# + var formats = new []{ + "Bmp", + "Gif", + "Jpeg", + "Png", + "Tga", + }; + + foreach (string fmt in formats) + { +#> +using SixLabors.ImageSharp.Formats.<#= fmt #>; +<# + + } +#> namespace SixLabors.ImageSharp { @@ -13,88 +35,115 @@ namespace SixLabors.ImageSharp /// public static partial class ImageExtensions { +<# + foreach (string fmt in formats) + { +#> /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// Thrown if the path is null. - public static void SaveAsBmp(this Image source, string path) => SaveAsBmp(source, path, null); + public static void SaveAs<#= fmt #>(this Image source, string path) => SaveAs<#= fmt #>(source, path, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, string path) => SaveAsBmpAsync(source, path, null); + public static Task SaveAs<#= fmt #>Async(this Image source, string path) => SaveAs<#= fmt #>Async(source, path, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAs<#= fmt #>Async(this Image source, string path, CancellationToken cancellationToken) + => SaveAs<#= fmt #>Async(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// The encoder to save the image with. /// Thrown if the path is null. - public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => + public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder) => + public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance), + cancellationToken); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// Thrown if the stream is null. - public static void SaveAsBmp(this Image source, Stream stream) => SaveAsBmp(source, stream, null); + public static void SaveAs<#= fmt #>(this Image source, Stream stream) + => SaveAs<#= fmt #>(source, stream, null); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. + /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, Stream stream) => SaveAsBmpAsync(source, stream, null); + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAs<#= fmt #>Async(source, stream, null, cancellationToken); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) => - source.Save( + /// A representing the asynchronous operation. + public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) + => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the bmp format. + /// Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder) => + public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance), + cancellationToken); + +<# + } +#> } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index d6c16f826..6424ee23a 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder @@ -50,6 +51,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private HuffmanScanBuffer scanBuffer; + private CancellationToken cancellationToken; + /// /// Initializes a new instance of the class. /// @@ -63,6 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The spectral selection end. /// The successive approximation bit high end. /// The successive approximation bit low end. + /// The token to monitor cancellation. public HuffmanScanDecoder( BufferedReadStream stream, JpegFrame frame, @@ -73,7 +77,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int spectralStart, int spectralEnd, int successiveHigh, - int successiveLow) + int successiveLow, + CancellationToken cancellationToken) { this.dctZigZag = ZigZag.CreateUnzigTable(); this.stream = stream; @@ -89,6 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.spectralEnd = spectralEnd; this.successiveHigh = successiveHigh; this.successiveLow = successiveLow; + this.cancellationToken = cancellationToken; } /// @@ -96,6 +102,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// public void ParseEntropyCodedData() { + this.cancellationToken.ThrowIfCancellationRequested(); + if (!this.frame.Progressive) { this.ParseBaselineData(); @@ -145,6 +153,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < mcusPerColumn; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + for (int i = 0; i < mcusPerLine; i++) { // Scan an interleaved mcu... process components in order @@ -210,6 +220,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); @@ -376,6 +387,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); @@ -402,6 +415,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int j = 0; j < h; j++) { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.GetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs index 716bb9eb0..5b0331c85 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegImagePostProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Numerics; +using System.Threading; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -111,7 +112,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// The pixel type /// The destination image - public void PostProcess(ImageFrame destination) + /// The token to request cancellation. + public void PostProcess(ImageFrame destination, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { this.PixelRowCounter = 0; @@ -123,6 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) { + cancellationToken.ThrowIfCancellationRequested(); this.DoPostProcessorStep(destination); } } diff --git a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs deleted file mode 100644 index d6600b625..000000000 --- a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Jpeg; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsJpeg(this Image source, string path) => SaveAsJpeg(source, path, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, string path) => SaveAsJpegAsync(source, path, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsJpeg(this Image source, Stream stream) => SaveAsJpeg(source, stream, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, Stream stream) => SaveAsJpegAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - - /// - /// Saves the image to the given stream with the jpeg format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 3eaf3a4c4..39b8e492f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -24,20 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - - JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -45,31 +33,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); - - JpegThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) @@ -77,20 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - - return decoder.Identify(bufferedStream); + return decoder.Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var decoder = new JpegDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - - return decoder.IdentifyAsync(bufferedStream); + // The introduction of a local variable that refers to an object the implements + // IDisposable means you must use async/await, where the compiler generates the + // state machine and a continuation. + using (var decoder = new JpegDecoderCore(configuration, this)) + { + return await decoder.IdentifyAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + } } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index c0b09c4c2..c4355cdbe 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -117,6 +118,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public Size ImageSizeInPixels { get; private set; } + /// + Size IImageDecoderInternals.Dimensions => this.ImageSizeInPixels; + /// /// Gets the number of MCU blocks in the image as . /// @@ -205,21 +209,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - this.ParseStream(stream); + this.ParseStream(stream, cancellationToken: cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitDerivedMetadataProperties(); - return this.PostProcessIntoImage(); + return this.PostProcessIntoImage(cancellationToken); } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ParseStream(stream, true); + this.ParseStream(stream, true, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -233,7 +237,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The input stream /// Whether to decode metadata only. - public void ParseStream(BufferedReadStream stream, bool metadataOnly = false) + /// The token to monitor cancellation. + public void ParseStream(BufferedReadStream stream, bool metadataOnly = false, CancellationToken cancellationToken = default) { this.Metadata = new ImageMetadata(); @@ -263,6 +268,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) { + cancellationToken.ThrowIfCancellationRequested(); + if (!fileMarker.Invalid) { // Get the marker length @@ -279,7 +286,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream); + this.ProcessStartOfScanMarker(stream, cancellationToken); break; } else @@ -996,8 +1003,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the SOS (Start of scan marker). /// - /// The input stream. - private void ProcessStartOfScanMarker(BufferedReadStream stream) + private void ProcessStartOfScanMarker(BufferedReadStream stream, CancellationToken cancellationToken) { if (this.Frame is null) { @@ -1048,7 +1054,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg spectralStart, spectralEnd, successiveApproximation >> 4, - successiveApproximation & 15); + successiveApproximation & 15, + cancellationToken); sd.ParseEntropyCodedData(); } @@ -1081,7 +1088,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The . - private Image PostProcessIntoImage() + private Image PostProcessIntoImage(CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (this.ImageWidth == 0 || this.ImageHeight == 0) @@ -1097,7 +1104,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using (var postProcessor = new JpegImagePostProcessor(this.Configuration, this)) { - postProcessor.PostProcess(image.Frames.RootFrame); + postProcessor.PostProcess(image.Frames.RootFrame, cancellationToken); } return image; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 08d793401..b549bd8a3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; @@ -43,26 +44,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - - if (stream.CanSeek) - { - encoder.Encode(image, stream); - } - else - { - // this hack has to be be here because JpegEncoderCore is unsafe - using (var ms = new MemoryStream()) - { - encoder.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index f755028db..593fe9277 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; @@ -23,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Image encoder for writing an image to a stream as a jpeg. /// - internal sealed unsafe class JpegEncoderCore + internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { /// /// The number of quantization tables. @@ -194,11 +195,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The pixel format. /// The image to write from. /// The stream to write to. - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + cancellationToken.ThrowIfCancellationRequested(); const ushort max = JpegConstants.MaxLength; if (image.Width >= max || image.Height >= max) @@ -247,7 +250,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteDefineHuffmanTables(componentCount); // Write the image data. - this.WriteStartOfScan(image); + this.WriteStartOfScan(image, cancellationToken); // Write the End Of Image marker. this.buffer[0] = JpegConstants.Markers.XFF; @@ -396,7 +399,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void Encode444(Image pixels) + /// The token to monitor for cancellation. + private void Encode444(Image pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -418,6 +422,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int y = 0; y < pixels.Height; y += 8) { + cancellationToken.ThrowIfCancellationRequested(); var currentRows = new RowOctet(pixelBuffer, y); for (int x = 0; x < pixels.Width; x += 8) @@ -943,7 +948,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void WriteStartOfScan(Image image) + /// The token to monitor for cancellation. + private void WriteStartOfScan(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -953,10 +959,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg switch (this.subsample) { case JpegSubsample.Ratio444: - this.Encode444(image); + this.Encode444(image, cancellationToken); break; case JpegSubsample.Ratio420: - this.Encode420(image); + this.Encode420(image, cancellationToken); break; } @@ -970,7 +976,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - private void Encode420(Image pixels) + /// The token to monitor for cancellation. + private void Encode420(Image pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) @@ -998,6 +1005,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int y = 0; y < pixels.Height; y += 16) { + cancellationToken.ThrowIfCancellationRequested(); for (int x = 0; x < pixels.Width; x += 16) { for (int i = 0; i < 4; i++) diff --git a/src/ImageSharp/Formats/Png/ImageExtensions.cs b/src/ImageSharp/Formats/Png/ImageExtensions.cs deleted file mode 100644 index e6a5265b2..000000000 --- a/src/ImageSharp/Formats/Png/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Png; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsPng(this Image source, string path) => SaveAsPng(source, path, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, string path) => SaveAsPngAsync(source, path, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsPng(this Image source, Stream stream) => SaveAsPng(source, stream, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, Stream stream) => SaveAsPngAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - - /// - /// Saves the image to the given stream with the png format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 87e0195c3..479c24b7e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -2,9 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -22,65 +21,37 @@ namespace SixLabors.ImageSharp.Formats.Png where TPixel : unmanaged, IPixel { var decoder = new PngDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var decoder = new PngDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - PngThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Identify(bufferedStream); + return decoder.Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { var decoder = new PngDecoderCore(configuration, this); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.IdentifyAsync(bufferedStream); + return decoder.IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 89caac3f6..3fa0e3f58 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -9,6 +9,8 @@ using System.IO.Compression; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; @@ -131,7 +133,7 @@ namespace SixLabors.ImageSharp.Formats.Png public Size Dimensions => new Size(this.header.Width, this.header.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var metadata = new ImageMetadata(); @@ -223,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { var metadata = new ImageMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata(); diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 61ea7c468..e72e8d3d5 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -71,13 +72,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The to encode from. /// The to encode the image data to. + /// The token to monitor for cancellation requests. /// A representing the asynchronous operation. - public async Task EncodeAsync(Image image, Stream stream) + public async Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + // The introduction of a local variable that refers to an object the implements + // IDisposable means you must use async/await, where the compiler generates the + // state machine and a continuation. using (var encoder = new PngEncoderCore(image.GetMemoryAllocator(), image.GetConfiguration(), new PngEncoderOptions(this))) { - await encoder.EncodeAsync(image, stream).ConfigureAwait(false); + await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a39fdd91f..5cf11099c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -7,6 +7,7 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -21,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Performs the png encoding operation. /// - internal sealed class PngEncoderCore : IDisposable + internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable { /// /// The maximum block size, defaults at 64k for uncompressed blocks. @@ -127,31 +128,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); diff --git a/src/ImageSharp/Formats/Tga/ImageExtensions.cs b/src/ImageSharp/Formats/Tga/ImageExtensions.cs deleted file mode 100644 index f39738eae..000000000 --- a/src/ImageSharp/Formats/Tga/ImageExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.IO; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Tga; - -namespace SixLabors.ImageSharp -{ - /// - /// Extension methods for the type. - /// - public static partial class ImageExtensions - { - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => - source.Save( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The file path to save the image to. - /// The encoder to save the image with. - /// Thrown if the path is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder) => - source.SaveAsync( - path, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - public static void SaveAsTga(this Image source, Stream stream) => SaveAsTga(source, stream, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, Stream stream) => SaveAsTgaAsync(source, stream, null); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => - source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - - /// - /// Saves the image to the given stream with the tga format. - /// - /// The image this method extends. - /// The stream to save the image to. - /// The encoder to save the image with. - /// Thrown if the stream is null. - /// A representing the asynchronous operation. - public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance)); - } -} diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index 25aa233db..e06a0ee88 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -21,21 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Tga Guard.NotNull(stream, nameof(stream)); var decoder = new TgaDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Decode(bufferedStream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.Decode(configuration, stream); } /// @@ -43,49 +30,34 @@ namespace SixLabors.ImageSharp.Formats.Tga => this.Decode(configuration, stream); /// - public async Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new TgaDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return await decoder.DecodeAsync(bufferedStream).ConfigureAwait(false); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - TgaThrowHelper.ThrowInvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - - // Not reachable, as the previous statement will throw a exception. - return null; - } + return decoder.DecodeAsync(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream) - => await this.DecodeAsync(configuration, stream).ConfigureAwait(false); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); /// public IImageInfo Identify(Configuration configuration, Stream stream) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new TgaDecoderCore(configuration, this).Identify(bufferedStream); + return new TgaDecoderCore(configuration, this).Identify(configuration, stream); } /// - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new TgaDecoderCore(configuration, this).IdentifyAsync(bufferedStream); + return new TgaDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 7cd83fedb..eef6e7362 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -77,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Tga public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height); /// - public Image Decode(BufferedReadStream stream) + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { try @@ -640,7 +641,7 @@ namespace SixLabors.ImageSharp.Formats.Tga } /// - public IImageInfo Identify(BufferedReadStream stream) + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadFileHeader(stream); return new ImageInfo( diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index c8f8fb1a5..529d951cd 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -32,11 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tga } /// - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var encoder = new TgaEncoderCore(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream); + return encoder.EncodeAsync(image, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index d9e7c0e6b..d3a628531 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -16,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Tga /// /// Image encoder for writing an image to a stream as a truevision targa image. /// - internal sealed class TgaEncoderCore + internal sealed class TgaEncoderCore : IImageEncoderInternals { /// /// Used for allocating memory during processing operations. @@ -61,31 +62,8 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The pixel format. /// The to encode from. /// The to encode the image data to. - public async Task EncodeAsync(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - if (stream.CanSeek) - { - this.Encode(image, stream); - } - else - { - using (var ms = new MemoryStream()) - { - this.Encode(image, ms); - ms.Position = 0; - await ms.CopyToAsync(stream).ConfigureAwait(false); - } - } - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 683590fd1..da23fb47d 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -156,18 +157,24 @@ namespace SixLabors.ImageSharp /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// The pixel format. /// A representing the asynchronous operation. - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync( + Stream stream, + Configuration config, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); + (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config) + .ConfigureAwait(false); if (decoder is null) { return (null, null); } - Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + Image img = await decoder.DecodeAsync(config, stream, cancellationToken) + .ConfigureAwait(false); return (img, format); } @@ -183,7 +190,7 @@ namespace SixLabors.ImageSharp return (img, format); } - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config) + private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config, CancellationToken cancellationToken) { (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); if (decoder is null) @@ -191,7 +198,7 @@ namespace SixLabors.ImageSharp return (null, null); } - Image img = await decoder.DecodeAsync(config, stream).ConfigureAwait(false); + Image img = await decoder.DecodeAsync(config, stream, cancellationToken).ConfigureAwait(false); return (img, format); } @@ -221,11 +228,12 @@ namespace SixLabors.ImageSharp /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// /// A representing the asynchronous operation with the /// property of the returned type set to null if a suitable detector /// is not found. - private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentityAsync(Stream stream, Configuration config) + private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentityAsync(Stream stream, Configuration config, CancellationToken cancellationToken) { (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); @@ -239,7 +247,7 @@ namespace SixLabors.ImageSharp return (null, format); } - IImageInfo info = await detector.IdentifyAsync(config, stream).ConfigureAwait(false); + IImageInfo info = await detector.IdentifyAsync(config, stream, cancellationToken).ConfigureAwait(false); return (info, format); } } diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index a078f2db9..bf239c3e9 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -80,15 +81,75 @@ namespace SixLabors.ImageSharp } /// - /// Create a new instance of the class from the given file. + /// Reads the raw image information from the specified stream without fully decoding it. /// - /// The file path to the image. - /// - /// Thrown if the stream is not readable nor seekable. - /// - /// The . - public static Image Load(string path) - => Load(Configuration.Default, path); + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static Task IdentifyAsync(string filePath, CancellationToken cancellationToken = default) + => IdentifyAsync(Configuration.Default, filePath, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static async Task IdentifyAsync( + Configuration configuration, + string filePath, + CancellationToken cancellationToken = default) + { + (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, filePath, cancellationToken) + .ConfigureAwait(false); + return res.ImageInfo; + } + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + string filePath, + CancellationToken cancellationToken = default) + => IdentifyWithFormatAsync(Configuration.Default, filePath, cancellationToken); + + /// + /// Reads the raw image information from the specified stream without fully decoding it. + /// + /// The configuration. + /// The image file to open and to read the header from. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. + /// + public static async Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Configuration configuration, + string filePath, + CancellationToken cancellationToken = default) + { + Guard.NotNull(configuration, nameof(configuration)); + using Stream stream = configuration.FileSystem.OpenRead(filePath); + return await IdentifyWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + } /// /// Create a new instance of the class from the given file. @@ -97,9 +158,9 @@ namespace SixLabors.ImageSharp /// /// Thrown if the stream is not readable nor seekable. /// - /// A representing the asynchronous operation. - public static Task LoadAsync(string path) - => LoadAsync(Configuration.Default, path); + /// The . + public static Image Load(string path) + => Load(Configuration.Default, path); /// /// Create a new instance of the class from the given file. @@ -131,18 +192,21 @@ namespace SixLabors.ImageSharp /// /// The configuration for the decoder. /// The file path to the image. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The path is null. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task LoadAsync(Configuration configuration, string path) + public static async Task LoadAsync( + Configuration configuration, + string path, + CancellationToken cancellationToken = default) { - using (Stream stream = configuration.FileSystem.OpenRead(path)) - { - (Image img, _) = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); - return img; - } + using Stream stream = configuration.FileSystem.OpenRead(path); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + return img; } /// @@ -168,27 +232,140 @@ namespace SixLabors.ImageSharp } } + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(string path, CancellationToken cancellationToken = default) + => LoadAsync(Configuration.Default, path, cancellationToken); + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The decoder. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync(string path, IImageDecoder decoder) + => LoadAsync(Configuration.Default, path, decoder, default); + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The decoder. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static Task> LoadAsync(string path, IImageDecoder decoder) + where TPixel : unmanaged, IPixel + => LoadAsync(Configuration.Default, path, decoder, default); + + /// + /// Create a new instance of the class from the given file. + /// + /// The Configuration. + /// The file path to the image. + /// The decoder. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// The path is null. + /// The decoder is null. + /// Image format not recognised. + /// Image contains invalid content. + /// A representing the asynchronous operation. + public static Task LoadAsync( + Configuration configuration, + string path, + IImageDecoder decoder, + CancellationToken cancellationToken = default) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(path, nameof(path)); + + using Stream stream = configuration.FileSystem.OpenRead(path); + return LoadAsync(configuration, stream, decoder, cancellationToken); + } + /// /// Create a new instance of the class from the given file. /// /// The Configuration. /// The file path to the image. /// The decoder. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The path is null. /// The decoder is null. /// Image format not recognised. /// Image contains invalid content. + /// The pixel format. /// A representing the asynchronous operation. - public static Task LoadAsync(Configuration configuration, string path, IImageDecoder decoder) + public static Task> LoadAsync( + Configuration configuration, + string path, + IImageDecoder decoder, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(path, nameof(path)); - using (Stream stream = configuration.FileSystem.OpenRead(path)) - { - return LoadAsync(configuration, stream, decoder); - } + using Stream stream = configuration.FileSystem.OpenRead(path); + return LoadAsync(configuration, stream, decoder, cancellationToken); + } + + /// + /// Create a new instance of the class from the given file. + /// + /// The file path to the image. + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static Task> LoadAsync(string path) + where TPixel : unmanaged, IPixel + => LoadAsync(Configuration.Default, path, default(CancellationToken)); + + /// + /// Create a new instance of the class from the given file. + /// + /// The configuration for the decoder. + /// The file path to the image. + /// The token to monitor for cancellation requests. + /// The configuration is null. + /// The path is null. + /// Image format not recognised. + /// Image contains invalid content. + /// The pixel format. + /// A representing the asynchronous operation. + public static async Task> LoadAsync( + Configuration configuration, + string path, + CancellationToken cancellationToken = default) + where TPixel : unmanaged, IPixel + { + using Stream stream = configuration.FileSystem.OpenRead(path); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); + return img; } /// diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index fae88f21e..ee148cd25 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -62,7 +63,8 @@ namespace SixLabors.ImageSharp => WithSeekableStreamAsync( configuration, stream, - s => InternalDetectFormatAsync(s, configuration)); + (s, _) => InternalDetectFormatAsync(s, configuration), + default); /// /// Reads the raw image information from the specified stream without fully decoding it. @@ -125,6 +127,7 @@ namespace SixLabors.ImageSharp /// /// The configuration. /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -133,9 +136,12 @@ namespace SixLabors.ImageSharp /// A representing the asynchronous operation or null if /// a suitable detector is not found. /// - public static async Task IdentifyAsync(Configuration configuration, Stream stream) + public static async Task IdentifyAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) { - (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, stream).ConfigureAwait(false); + (IImageInfo ImageInfo, IImageFormat Format) res = await IdentifyWithFormatAsync(configuration, stream, cancellationToken).ConfigureAwait(false); return res.ImageInfo; } @@ -164,35 +170,43 @@ namespace SixLabors.ImageSharp /// Reads the raw image information from the specified stream without fully decoding it. /// /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image contains invalid content. /// - /// A representing the asynchronous operation or null if - /// a suitable detector is not found. + /// The representing the asynchronous operation with the parameter type + /// property set to null if suitable info detector is not found. /// - public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Stream stream) - => IdentifyWithFormatAsync(Configuration.Default, stream); + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Stream stream, + CancellationToken cancellationToken = default) + => IdentifyWithFormatAsync(Configuration.Default, stream, cancellationToken); /// /// Reads the raw image information from the specified stream without fully decoding it. /// /// The configuration. /// The image stream to read the information from. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image contains invalid content. /// - /// The representing the asyncronous operation with the parameter type + /// The representing the asynchronous operation with the parameter type /// property set to null if suitable info detector is not found. /// - public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync(Configuration configuration, Stream stream) + public static Task<(IImageInfo ImageInfo, IImageFormat Format)> IdentifyWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) => WithSeekableStreamAsync( configuration, stream, - s => InternalIdentityAsync(s, configuration ?? Configuration.Default)); + (s, ct) => InternalIdentityAsync(s, configuration ?? Configuration.Default, ct), + cancellationToken); /// /// Decode a new instance of the class from the given stream. @@ -302,6 +316,7 @@ namespace SixLabors.ImageSharp /// The configuration for the decoder. /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The decoder is null. @@ -309,13 +324,18 @@ namespace SixLabors.ImageSharp /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static Task LoadAsync(Configuration configuration, Stream stream, IImageDecoder decoder) + public static Task LoadAsync( + Configuration configuration, + Stream stream, + IImageDecoder decoder, + CancellationToken cancellationToken = default) { Guard.NotNull(decoder, nameof(decoder)); return WithSeekableStreamAsync( configuration, stream, - s => decoder.DecodeAsync(configuration, s)); + (s, ct) => decoder.DecodeAsync(configuration, s, ct), + cancellationToken); } /// @@ -336,15 +356,17 @@ namespace SixLabors.ImageSharp /// /// The configuration for the decoder. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task LoadAsync(Configuration configuration, Stream stream) + public static async Task LoadAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken = default) { - (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + (Image Image, IImageFormat Format) fmt = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); return fmt.Image; } @@ -425,18 +447,20 @@ namespace SixLabors.ImageSharp /// /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static Task> LoadAsync(Stream stream, IImageDecoder decoder) + public static Task> LoadAsync(Stream stream, IImageDecoder decoder, CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel => WithSeekableStreamAsync( Configuration.Default, stream, - s => decoder.DecodeAsync(Configuration.Default, s)); + (s, ct) => decoder.DecodeAsync(Configuration.Default, s, ct), + cancellationToken); /// /// Create a new instance of the class from the given stream. @@ -461,6 +485,7 @@ namespace SixLabors.ImageSharp /// The Configuration. /// The stream containing image information. /// The decoder. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -468,12 +493,17 @@ namespace SixLabors.ImageSharp /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static Task> LoadAsync(Configuration configuration, Stream stream, IImageDecoder decoder) + public static Task> LoadAsync( + Configuration configuration, + Stream stream, + IImageDecoder decoder, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel => WithSeekableStreamAsync( configuration, stream, - s => decoder.DecodeAsync(configuration, s)); + (s, ct) => decoder.DecodeAsync(configuration, s, ct), + cancellationToken); /// /// Create a new instance of the class from the given stream. @@ -532,18 +562,23 @@ namespace SixLabors.ImageSharp /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. /// Image format not recognised. /// Image contains invalid content. /// A representing the asynchronous operation. - public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Configuration configuration, Stream stream) + public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) { (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( configuration, stream, - async s => await DecodeAsync(s, configuration).ConfigureAwait(false)) + async (s, ct) => await DecodeAsync(s, configuration, ct).ConfigureAwait(false), + cancellationToken) .ConfigureAwait(false); if (data.Image != null) @@ -567,6 +602,7 @@ namespace SixLabors.ImageSharp /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -574,14 +610,18 @@ namespace SixLabors.ImageSharp /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Configuration configuration, Stream stream) + public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel { (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( configuration, stream, - s => DecodeAsync(s, configuration)) + (s, ct) => DecodeAsync(s, configuration, ct), + cancellationToken) .ConfigureAwait(false); if (data.Image != null) @@ -605,6 +645,7 @@ namespace SixLabors.ImageSharp /// /// The configuration options. /// The stream containing image information. + /// The token to monitor for cancellation requests. /// The configuration is null. /// The stream is null. /// The stream is not readable. @@ -612,10 +653,14 @@ namespace SixLabors.ImageSharp /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static async Task> LoadAsync(Configuration configuration, Stream stream) + public static async Task> LoadAsync( + Configuration configuration, + Stream stream, + CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel { - (Image img, _) = await LoadWithFormatAsync(configuration, stream).ConfigureAwait(false); + (Image img, _) = await LoadWithFormatAsync(configuration, stream, cancellationToken) + .ConfigureAwait(false); return img; } @@ -700,11 +745,13 @@ namespace SixLabors.ImageSharp /// The configuration. /// The input stream. /// The action to perform. + /// The cancellation token. /// The . private static async Task WithSeekableStreamAsync( Configuration configuration, Stream stream, - Func> action) + Func> action, + CancellationToken cancellationToken) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(stream, nameof(stream)); @@ -725,14 +772,14 @@ namespace SixLabors.ImageSharp stream.Position = 0; } - return await action(stream).ConfigureAwait(false); + return await action(stream, cancellationToken).ConfigureAwait(false); } using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length); - await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize).ConfigureAwait(false); + await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; - return await action(memoryStream).ConfigureAwait(false); + return await action(memoryStream, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index 605f5e0da..fbb3ec206 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -103,15 +104,16 @@ namespace SixLabors.ImageSharp /// /// The stream to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// Thrown if the stream or encoder is null. /// A representing the asynchronous operation. - public Task SaveAsync(Stream stream, IImageEncoder encoder) + public Task SaveAsync(Stream stream, IImageEncoder encoder, CancellationToken cancellationToken = default) { Guard.NotNull(stream, nameof(stream)); Guard.NotNull(encoder, nameof(encoder)); this.EnsureNotDisposed(); - return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream)); + return this.AcceptVisitorAsync(new EncodeVisitor(encoder, stream), cancellationToken); } /// @@ -162,7 +164,8 @@ namespace SixLabors.ImageSharp /// with the pixel type of the image. /// /// The visitor. - internal abstract Task AcceptAsync(IImageVisitorAsync visitor); + /// The token to monitor for cancellation requests. + internal abstract Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken); private class EncodeVisitor : IImageVisitor, IImageVisitorAsync { @@ -179,8 +182,8 @@ namespace SixLabors.ImageSharp public void Visit(Image image) where TPixel : unmanaged, IPixel => this.encoder.Encode(image, this.stream); - public Task VisitAsync(Image image) - where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream); + public Task VisitAsync(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel => this.encoder.EncodeAsync(image, this.stream, cancellationToken); } } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index b227679a6..d40c5c271 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -30,10 +31,11 @@ namespace SixLabors.ImageSharp /// /// The source image. /// The file path to save the image to. + /// The token to monitor for cancellation requests. /// The path is null. /// A representing the asynchronous operation. - public static Task SaveAsync(this Image source, string path) - => source.SaveAsync(path, source.DetectEncoder(path)); + public static Task SaveAsync(this Image source, string path, CancellationToken cancellationToken = default) + => source.SaveAsync(path, source.DetectEncoder(path), cancellationToken); /// /// Writes the image to the given stream using the currently loaded image format. @@ -59,17 +61,20 @@ namespace SixLabors.ImageSharp /// The source image. /// The file path to save the image to. /// The encoder to save the image with. + /// The token to monitor for cancellation requests. /// The path is null. /// The encoder is null. /// A representing the asynchronous operation. - public static async Task SaveAsync(this Image source, string path, IImageEncoder encoder) + public static async Task SaveAsync( + this Image source, + string path, + IImageEncoder encoder, + CancellationToken cancellationToken = default) { Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using (Stream fs = source.GetConfiguration().FileSystem.Create(path)) - { - await source.SaveAsync(fs, encoder).ConfigureAwait(false); - } + using Stream fs = source.GetConfiguration().FileSystem.Create(path); + await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 64a496141..c3d9618c8 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -20,10 +20,10 @@ - + - + @@ -41,7 +41,7 @@ - + True True @@ -132,6 +132,11 @@ True PorterDuffFunctions.Generated.tt + + True + True + ImageExtensions.Save.tt + @@ -207,6 +212,10 @@ DefaultPixelBlenders.Generated.cs TextTemplatingFileGenerator + + TextTemplatingFileGenerator + ImageExtensions.Save.cs + diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 9d3abc1e8..255193c8e 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -290,11 +291,11 @@ namespace SixLabors.ImageSharp } /// - internal override Task AcceptAsync(IImageVisitorAsync visitor) + internal override Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken) { this.EnsureNotDisposed(); - return visitor.VisitAsync(this); + return visitor.VisitAsync(this, cancellationToken); } /// diff --git a/src/ImageSharp/Processing/EdgeDetectionOperators.cs b/src/ImageSharp/Processing/EdgeDetectionOperators.cs deleted file mode 100644 index 3e986f802..000000000 --- a/src/ImageSharp/Processing/EdgeDetectionOperators.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Enumerates the various types of defined edge detection filters. - /// - public enum EdgeDetectionOperators - { - /// - /// The Kayyali operator filter. - /// - Kayyali, - - /// - /// The Kirsch operator filter. - /// - Kirsch, - - /// - /// The Laplacian3X3 operator filter. - /// - Laplacian3x3, - - /// - /// The Laplacian5X5 operator filter. - /// - Laplacian5x5, - - /// - /// The LaplacianOfGaussian operator filter. - /// - LaplacianOfGaussian, - - /// - /// The Prewitt operator filter. - /// - Prewitt, - - /// - /// The RobertsCross operator filter. - /// - RobertsCross, - - /// - /// The Robinson operator filter. - /// - Robinson, - - /// - /// The Scharr operator filter. - /// - Scharr, - - /// - /// The Sobel operator filter. - /// - Sobel - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs index 61b900848..2377151bb 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs @@ -1,7 +1,6 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Processing.Processors.Convolution; namespace SixLabors.ImageSharp.Processing @@ -12,144 +11,230 @@ namespace SixLabors.ImageSharp.Processing public static class DetectEdgesExtensions { /// - /// Detects any edges within the image. Uses the filter - /// operating in grayscale mode. + /// Detects any edges within the image. + /// Uses the kernel operating in grayscale mode. /// /// The image this method extends. /// The to allow chaining of operations. public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) => - DetectEdges(source, new SobelProcessor(true)); + DetectEdges(source, KnownEdgeDetectorKernels.Sobel); /// - /// Detects any edges within the image. Uses the filter - /// operating in grayscale mode. + /// Detects any edges within the image. + /// Uses the kernel operating in grayscale mode. /// /// The image this method extends. /// /// The structure that specifies the portion of the image object to alter. /// /// The to allow chaining of operations. - public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, Rectangle rectangle) => - DetectEdges(source, rectangle, new SobelProcessor(true)); + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + Rectangle rectangle) => + DetectEdges(source, KnownEdgeDetectorKernels.Sobel, rectangle); /// - /// Detects any edges within the image. + /// Detects any edges within the image operating in grayscale mode. /// /// The image this method extends. - /// The filter for detecting edges. + /// The 2D edge detector kernel. /// The to allow chaining of operations. public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetectionOperators filter) => - DetectEdges(source, GetProcessor(filter, true)); + EdgeDetector2DKernel kernel) => + DetectEdges(source, kernel, true); /// - /// Detects any edges within the image. + /// Detects any edges within the image using a . /// /// The image this method extends. - /// The filter for detecting edges. - /// Whether to convert the image to grayscale first. Defaults to true. + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// /// The to allow chaining of operations. public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetectionOperators filter, - bool grayscale) => - DetectEdges(source, GetProcessor(filter, grayscale)); + EdgeDetector2DKernel kernel, + bool grayscale) + { + var processor = new EdgeDetector2DProcessor(kernel, grayscale); + source.ApplyProcessor(processor); + return source; + } /// - /// Detects any edges within the image. + /// Detects any edges within the image operating in grayscale mode. /// /// The image this method extends. - /// The filter for detecting edges. + /// The 2D edge detector kernel. /// /// The structure that specifies the portion of the image object to alter. /// - /// Whether to convert the image to grayscale first. Defaults to true. /// The to allow chaining of operations. public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - EdgeDetectionOperators filter, - Rectangle rectangle, - bool grayscale = true) => - DetectEdges(source, rectangle, GetProcessor(filter, grayscale)); + EdgeDetector2DKernel kernel, + Rectangle rectangle) => + DetectEdges(source, kernel, true, rectangle); /// - /// Detects any edges within the image. + /// Detects any edges within the image using a . /// /// The image this method extends. - /// The filter for detecting edges. + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// + /// The structure that specifies the portion of the image object to alter. + /// /// The to allow chaining of operations. - private static IImageProcessingContext DetectEdges(this IImageProcessingContext source, IImageProcessor filter) + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetector2DKernel kernel, + bool grayscale, + Rectangle rectangle) { - return source.ApplyProcessor(filter); + var processor = new EdgeDetector2DProcessor(kernel, grayscale); + source.ApplyProcessor(processor, rectangle); + return source; } /// - /// Detects any edges within the image. + /// Detects any edges within the image operating in grayscale mode. /// /// The image this method extends. - /// - /// The structure that specifies the portion of the image object to alter. + /// The edge detector kernel. + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorKernel kernel) => + DetectEdges(source, kernel, true); + + /// + /// Detects any edges within the image using a . + /// + /// The image this method extends. + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. /// - /// The filter for detecting edges. /// The to allow chaining of operations. - private static IImageProcessingContext DetectEdges( + public static IImageProcessingContext DetectEdges( this IImageProcessingContext source, - Rectangle rectangle, - IImageProcessor filter) + EdgeDetectorKernel kernel, + bool grayscale) { - source.ApplyProcessor(filter, rectangle); + var processor = new EdgeDetectorProcessor(kernel, grayscale); + source.ApplyProcessor(processor); return source; } - private static IImageProcessor GetProcessor(EdgeDetectionOperators filter, bool grayscale) - { - IImageProcessor processor; - - switch (filter) - { - case EdgeDetectionOperators.Kayyali: - processor = new KayyaliProcessor(grayscale); - break; - - case EdgeDetectionOperators.Kirsch: - processor = new KirschProcessor(grayscale); - break; - - case EdgeDetectionOperators.Laplacian3x3: - processor = new Laplacian3x3Processor(grayscale); - break; - - case EdgeDetectionOperators.Laplacian5x5: - processor = new Laplacian5x5Processor(grayscale); - break; - - case EdgeDetectionOperators.LaplacianOfGaussian: - processor = new LaplacianOfGaussianProcessor(grayscale); - break; - - case EdgeDetectionOperators.Prewitt: - processor = new PrewittProcessor(grayscale); - break; + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The image this method extends. + /// The edge detector kernel. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorKernel kernel, + Rectangle rectangle) => + DetectEdges(source, kernel, true, rectangle); - case EdgeDetectionOperators.RobertsCross: - processor = new RobertsCrossProcessor(grayscale); - break; + /// + /// Detects any edges within the image using a . + /// + /// The image this method extends. + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorKernel kernel, + bool grayscale, + Rectangle rectangle) + { + var processor = new EdgeDetectorProcessor(kernel, grayscale); + source.ApplyProcessor(processor, rectangle); + return source; + } - case EdgeDetectionOperators.Robinson: - processor = new RobinsonProcessor(grayscale); - break; + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The image this method extends. + /// Thecompass edge detector kernel. + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorCompassKernel kernel) => + DetectEdges(source, kernel, true); - case EdgeDetectionOperators.Scharr: - processor = new ScharrProcessor(grayscale); - break; + /// + /// Detects any edges within the image using a . + /// + /// The image this method extends. + /// Thecompass edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorCompassKernel kernel, + bool grayscale) + { + var processor = new EdgeDetectorCompassProcessor(kernel, grayscale); + source.ApplyProcessor(processor); + return source; + } - default: - processor = new SobelProcessor(grayscale); - break; - } + /// + /// Detects any edges within the image operating in grayscale mode. + /// + /// The image this method extends. + /// Thecompass edge detector kernel. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorCompassKernel kernel, + Rectangle rectangle) => + DetectEdges(source, kernel, true, rectangle); - return processor; + /// + /// Detects any edges within the image using a . + /// + /// The image this method extends. + /// Thecompass edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext DetectEdges( + this IImageProcessingContext source, + EdgeDetectorCompassKernel kernel, + bool grayscale, + Rectangle rectangle) + { + var processor = new EdgeDetectorCompassProcessor(kernel, grayscale); + source.ApplyProcessor(processor, rectangle); + return source; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/KnownEdgeDetectorKernels.cs b/src/ImageSharp/Processing/KnownEdgeDetectorKernels.cs new file mode 100644 index 000000000..2e279d340 --- /dev/null +++ b/src/ImageSharp/Processing/KnownEdgeDetectorKernels.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Contains reusable static instances of known edge detection kernels. + /// + public static class KnownEdgeDetectorKernels + { + /// + /// Gets the Kayyali edge detector kernel. + /// + public static EdgeDetector2DKernel Kayyali { get; } = EdgeDetector2DKernel.KayyaliKernel; + + /// + /// Gets the Kirsch edge detector kernel. + /// + public static EdgeDetectorCompassKernel Kirsch { get; } = EdgeDetectorCompassKernel.Kirsch; + + /// + /// Gets the Laplacian 3x3 edge detector kernel. + /// + public static EdgeDetectorKernel Laplacian3x3 { get; } = EdgeDetectorKernel.Laplacian3x3; + + /// + /// Gets the Laplacian 5x5 edge detector kernel. + /// + public static EdgeDetectorKernel Laplacian5x5 { get; } = EdgeDetectorKernel.Laplacian5x5; + + /// + /// Gets the Laplacian of Gaussian edge detector kernel. + /// + public static EdgeDetectorKernel LaplacianOfGaussian { get; } = EdgeDetectorKernel.LaplacianOfGaussian; + + /// + /// Gets the Prewitt edge detector kernel. + /// + public static EdgeDetector2DKernel Prewitt { get; } = EdgeDetector2DKernel.PrewittKernel; + + /// + /// Gets the Roberts-Cross edge detector kernel. + /// + public static EdgeDetector2DKernel RobertsCross { get; } = EdgeDetector2DKernel.RobertsCrossKernel; + + /// + /// Gets the Robinson edge detector kernel. + /// + public static EdgeDetectorCompassKernel Robinson { get; } = EdgeDetectorCompassKernel.Robinson; + + /// + /// Gets the Scharr edge detector kernel. + /// + public static EdgeDetector2DKernel Scharr { get; } = EdgeDetector2DKernel.ScharrKernel; + + /// + /// Gets the Sobel edge detector kernel. + /// + public static EdgeDetector2DKernel Sobel { get; } = EdgeDetector2DKernel.SobelKernel; + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs new file mode 100644 index 000000000..1d0a29a35 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Defines edge detection using the two 1D gradient operators. + /// + public sealed class EdgeDetector2DProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The 2D edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetector2DProcessor(EdgeDetector2DKernel kernel, bool grayscale) + { + this.Kernel = kernel; + this.Grayscale = grayscale; + } + + /// + /// Gets the 2D edge detector kernel. + /// + public EdgeDetector2DKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. + /// + public bool Grayscale { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetector2DProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs index 6c831e727..80f8a7706 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetector2DProcessor{TPixel}.cs @@ -13,42 +13,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution internal class EdgeDetector2DProcessor : ImageProcessor where TPixel : unmanaged, IPixel { + private readonly DenseMatrix kernelX; + private readonly DenseMatrix kernelY; + private readonly bool grayscale; + /// /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The horizontal gradient operator. - /// The vertical gradient operator. - /// Whether to convert the image to grayscale before performing edge detection. + /// The defining the processor parameters. /// The source for the current processor instance. /// The source area to process for the current processor instance. - internal EdgeDetector2DProcessor( + public EdgeDetector2DProcessor( Configuration configuration, - in DenseMatrix kernelX, - in DenseMatrix kernelY, - bool grayscale, + EdgeDetector2DProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - Guard.IsTrue(kernelX.Size.Equals(kernelY.Size), $"{nameof(kernelX)} {nameof(kernelY)}", "Kernel sizes must be the same."); - this.KernelX = kernelX; - this.KernelY = kernelY; - this.Grayscale = grayscale; + this.kernelX = definition.Kernel.KernelX; + this.kernelY = definition.Kernel.KernelY; + this.grayscale = definition.Grayscale; } - /// - /// Gets the horizontal gradient operator. - /// - public DenseMatrix KernelX { get; } - - /// - /// Gets the vertical gradient operator. - /// - public DenseMatrix KernelY { get; } - - public bool Grayscale { get; } - /// protected override void BeforeImageApply() { @@ -57,7 +44,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution opaque.Execute(); } - if (this.Grayscale) + if (this.grayscale) { new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); } @@ -68,7 +55,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2DProcessor(this.Configuration, this.KernelX, this.KernelY, true, this.Source, this.SourceRectangle); + using var processor = new Convolution2DProcessor( + this.Configuration, + in this.kernelX, + in this.kernelY, + true, + this.Source, + this.SourceRectangle); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs new file mode 100644 index 000000000..083a69bd2 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Defines edge detection using eight gradient operators. + /// + public sealed class EdgeDetectorCompassProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetectorCompassProcessor(EdgeDetectorCompassKernel kernel, bool grayscale) + { + this.Kernel = kernel; + this.Grayscale = grayscale; + } + + /// + /// Gets the edge detector kernel. + /// + public EdgeDetectorCompassKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. + /// + public bool Grayscale { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetectorCompassProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index 164488155..27963613e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -18,25 +18,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution internal class EdgeDetectorCompassProcessor : ImageProcessor where TPixel : unmanaged, IPixel { + private readonly DenseMatrix[] kernels; + private readonly bool grayscale; + /// /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// Gets the kernels to use. - /// Whether to convert the image to grayscale before performing edge detection. + /// The defining the processor parameters. /// The source for the current processor instance. /// The source area to process for the current processor instance. - internal EdgeDetectorCompassProcessor(Configuration configuration, CompassKernels kernels, bool grayscale, Image source, Rectangle sourceRectangle) + internal EdgeDetectorCompassProcessor( + Configuration configuration, + EdgeDetectorCompassProcessor definition, + Image source, + Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.Grayscale = grayscale; - this.Kernels = kernels; + this.grayscale = definition.Grayscale; + this.kernels = definition.Kernel.Flatten(); } - private CompassKernels Kernels { get; } - - private bool Grayscale { get; } - /// protected override void BeforeImageApply() { @@ -45,7 +47,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution opaque.Execute(); } - if (this.Grayscale) + if (this.grayscale) { new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); } @@ -56,29 +58,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// protected override void OnFrameApply(ImageFrame source) { - DenseMatrix[] kernels = this.Kernels.Flatten(); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We need a clean copy for each pass to start from using ImageFrame cleanCopy = source.Clone(); - using (var processor = new ConvolutionProcessor(this.Configuration, kernels[0], true, this.Source, interest)) + using (var processor = new ConvolutionProcessor(this.Configuration, in this.kernels[0], true, this.Source, interest)) { processor.Apply(source); } - if (kernels.Length == 1) + if (this.kernels.Length == 1) { return; } // Additional runs - for (int i = 1; i < kernels.Length; i++) + for (int i = 1; i < this.kernels.Length; i++) { using ImageFrame pass = cleanCopy.Clone(); - using (var processor = new ConvolutionProcessor(this.Configuration, kernels[i], true, this.Source, interest)) + using (var processor = new ConvolutionProcessor(this.Configuration, in this.kernels[i], true, this.Source, interest)) { processor.Apply(pass); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs index a9d20b547..6fcfca662 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor.cs @@ -6,26 +6,37 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Convolution { /// - /// Defines a processor that detects edges within an image using a single two dimensional matrix. + /// Defines edge detection using a single 2D gradient operator. /// - public abstract class EdgeDetectorProcessor : IImageProcessor + public sealed class EdgeDetectorProcessor : IImageProcessor { /// /// Initializes a new instance of the class. /// - /// A value indicating whether to convert the image to grayscale before performing edge detection. - protected EdgeDetectorProcessor(bool grayscale) + /// The edge detector kernel. + /// + /// Whether to convert the image to grayscale before performing edge detection. + /// + public EdgeDetectorProcessor(EdgeDetectorKernel kernel, bool grayscale) { + this.Kernel = kernel; this.Grayscale = grayscale; } /// - /// Gets a value indicating whether to convert the image to grayscale before performing edge detection. + /// Gets the edge detector kernel. + /// + public EdgeDetectorKernel Kernel { get; } + + /// + /// Gets a value indicating whether to convert the image to grayscale before performing + /// edge detection. /// public bool Grayscale { get; } /// - public abstract IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel; + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new EdgeDetectorProcessor(configuration, this, source, sourceRectangle); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs index 45639d93a..62dd54919 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs @@ -13,33 +13,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution internal class EdgeDetectorProcessor : ImageProcessor where TPixel : unmanaged, IPixel { + private readonly bool grayscale; + private readonly DenseMatrix kernelXY; + /// /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The 2d gradient operator. - /// Whether to convert the image to grayscale before performing edge detection. + /// The defining the processor parameters. /// The source for the current processor instance. /// The target area to process for the current processor instance. public EdgeDetectorProcessor( Configuration configuration, - in DenseMatrix kernelXY, - bool grayscale, + EdgeDetectorProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.KernelXY = kernelXY; - this.Grayscale = grayscale; + this.kernelXY = definition.Kernel.KernelXY; + this.grayscale = definition.Grayscale; } - public bool Grayscale { get; } - - /// - /// Gets the 2d gradient operator. - /// - public DenseMatrix KernelXY { get; } - /// protected override void BeforeImageApply() { @@ -48,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution opaque.Execute(); } - if (this.Grayscale) + if (this.grayscale) { new GrayscaleBt709Processor(1F).Execute(this.Configuration, this.Source, this.SourceRectangle); } @@ -59,10 +53,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// protected override void OnFrameApply(ImageFrame source) { - using (var processor = new ConvolutionProcessor(this.Configuration, this.KernelXY, true, this.Source, this.SourceRectangle)) - { - processor.Apply(source); - } + using var processor = new ConvolutionProcessor(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle); + processor.Apply(source); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/KayyaliProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/KayyaliProcessor.cs deleted file mode 100644 index c13e8b543..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/KayyaliProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection processing using the Kayyali operator filter. - /// See . - /// - public sealed class KayyaliProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public KayyaliProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetector2DProcessor( - configuration, - KayyaliKernels.KayyaliX, - KayyaliKernels.KayyaliY, - this.Grayscale, - source, - sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/CompassKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/CompassKernels.cs deleted file mode 100644 index 24caa40f8..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/Kernels/CompassKernels.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - internal abstract class CompassKernels - { - /// - /// Gets the North gradient operator. - /// - public abstract DenseMatrix North { get; } - - /// - /// Gets the NorthWest gradient operator. - /// - public abstract DenseMatrix NorthWest { get; } - - /// - /// Gets the West gradient operator. - /// - public abstract DenseMatrix West { get; } - - /// - /// Gets the SouthWest gradient operator. - /// - public abstract DenseMatrix SouthWest { get; } - - /// - /// Gets the South gradient operator. - /// - public abstract DenseMatrix South { get; } - - /// - /// Gets the SouthEast gradient operator. - /// - public abstract DenseMatrix SouthEast { get; } - - /// - /// Gets the East gradient operator. - /// - public abstract DenseMatrix East { get; } - - /// - /// Gets the NorthEast gradient operator. - /// - public abstract DenseMatrix NorthEast { get; } - - public DenseMatrix[] Flatten() => - new[] - { - this.North, this.NorthWest, this.West, this.SouthWest, - this.South, this.SouthEast, this.East, this.NorthEast - }; - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs new file mode 100644 index 000000000..ed363595d --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetector2DKernel.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Represents an edge detection convolution kernel consisting of two 1D gradient operators. + /// + public readonly struct EdgeDetector2DKernel : IEquatable + { + /// + /// An edge detection kernel containing two Kayyali operators. + /// + public static EdgeDetector2DKernel KayyaliKernel = new EdgeDetector2DKernel(KayyaliKernels.KayyaliX, KayyaliKernels.KayyaliY); + + /// + /// An edge detection kernel containing two Prewitt operators. + /// . + /// + public static EdgeDetector2DKernel PrewittKernel = new EdgeDetector2DKernel(PrewittKernels.PrewittX, PrewittKernels.PrewittY); + + /// + /// An edge detection kernel containing two Roberts-Cross operators. + /// . + /// + public static EdgeDetector2DKernel RobertsCrossKernel = new EdgeDetector2DKernel(RobertsCrossKernels.RobertsCrossX, RobertsCrossKernels.RobertsCrossY); + + /// + /// An edge detection kernel containing two Scharr operators. + /// + public static EdgeDetector2DKernel ScharrKernel = new EdgeDetector2DKernel(ScharrKernels.ScharrX, ScharrKernels.ScharrY); + + /// + /// An edge detection kernel containing two Sobel operators. + /// . + /// + public static EdgeDetector2DKernel SobelKernel = new EdgeDetector2DKernel(SobelKernels.SobelX, SobelKernels.SobelY); + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal gradient operator. + /// The vertical gradient operator. + public EdgeDetector2DKernel(DenseMatrix kernelX, DenseMatrix kernelY) + { + Guard.IsTrue( + kernelX.Size.Equals(kernelY.Size), + $"{nameof(kernelX)} {nameof(kernelY)}", + "Kernel sizes must be the same."); + + this.KernelX = kernelX; + this.KernelY = kernelY; + } + + /// + /// Gets the horizontal gradient operator. + /// + public DenseMatrix KernelX { get; } + + /// + /// Gets the vertical gradient operator. + /// + public DenseMatrix KernelY { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetector2DKernel left, EdgeDetector2DKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetector2DKernel left, EdgeDetector2DKernel right) + => !(left == right); + + /// + public override bool Equals(object obj) + => obj is EdgeDetector2DKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetector2DKernel other) + => this.KernelX.Equals(other.KernelX) + && this.KernelY.Equals(other.KernelY); + + /// + public override int GetHashCode() => HashCode.Combine(this.KernelX, this.KernelY); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs new file mode 100644 index 000000000..bda861799 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorCompassKernel.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Represents an edge detection convolution kernel consisting of eight gradient operators. + /// + public readonly struct EdgeDetectorCompassKernel : IEquatable + { + /// + /// An edge detection kenel comprised of Kirsch gradient operators. + /// . + /// + public static EdgeDetectorCompassKernel Kirsch = + new EdgeDetectorCompassKernel( + KirschKernels.North, + KirschKernels.NorthWest, + KirschKernels.West, + KirschKernels.SouthWest, + KirschKernels.South, + KirschKernels.SouthEast, + KirschKernels.East, + KirschKernels.NorthEast); + + /// + /// An edge detection kenel comprised of Robinson gradient operators. + /// + /// + public static EdgeDetectorCompassKernel Robinson = + new EdgeDetectorCompassKernel( + RobinsonKernels.North, + RobinsonKernels.NorthWest, + RobinsonKernels.West, + RobinsonKernels.SouthWest, + RobinsonKernels.South, + RobinsonKernels.SouthEast, + RobinsonKernels.East, + RobinsonKernels.NorthEast); + + /// + /// Initializes a new instance of the struct. + /// + /// The north gradient operator. + /// The north-west gradient operator. + /// The west gradient operator. + /// The south-west gradient operator. + /// The south gradient operator. + /// The south-east gradient operator. + /// The east gradient operator. + /// The north-east gradient operator. + public EdgeDetectorCompassKernel( + DenseMatrix north, + DenseMatrix northWest, + DenseMatrix west, + DenseMatrix southWest, + DenseMatrix south, + DenseMatrix southEast, + DenseMatrix east, + DenseMatrix northEast) + { + this.North = north; + this.NorthWest = northWest; + this.West = west; + this.SouthWest = southWest; + this.South = south; + this.SouthEast = southEast; + this.East = east; + this.NorthEast = northEast; + } + + /// + /// Gets the North gradient operator. + /// + public DenseMatrix North { get; } + + /// + /// Gets the NorthWest gradient operator. + /// + public DenseMatrix NorthWest { get; } + + /// + /// Gets the West gradient operator. + /// + public DenseMatrix West { get; } + + /// + /// Gets the SouthWest gradient operator. + /// + public DenseMatrix SouthWest { get; } + + /// + /// Gets the South gradient operator. + /// + public DenseMatrix South { get; } + + /// + /// Gets the SouthEast gradient operator. + /// + public DenseMatrix SouthEast { get; } + + /// + /// Gets the East gradient operator. + /// + public DenseMatrix East { get; } + + /// + /// Gets the NorthEast gradient operator. + /// + public DenseMatrix NorthEast { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetectorCompassKernel left, EdgeDetectorCompassKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetectorCompassKernel left, EdgeDetectorCompassKernel right) + => !(left == right); + + /// + public override bool Equals(object obj) => obj is EdgeDetectorCompassKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetectorCompassKernel other) => this.North.Equals(other.North) && this.NorthWest.Equals(other.NorthWest) && this.West.Equals(other.West) && this.SouthWest.Equals(other.SouthWest) && this.South.Equals(other.South) && this.SouthEast.Equals(other.SouthEast) && this.East.Equals(other.East) && this.NorthEast.Equals(other.NorthEast); + + /// + public override int GetHashCode() + => HashCode.Combine( + this.North, + this.NorthWest, + this.West, + this.SouthWest, + this.South, + this.SouthEast, + this.East, + this.NorthEast); + + internal DenseMatrix[] Flatten() => + new[] + { + this.North, this.NorthWest, this.West, this.SouthWest, + this.South, this.SouthEast, this.East, this.NorthEast + }; + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs new file mode 100644 index 000000000..86b8a24d9 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/EdgeDetectorKernel.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Represents an edge detection convolution kernel consisting of a single 2D gradient operator. + /// + public readonly struct EdgeDetectorKernel : IEquatable + { + /// + /// An edge detection kernel containing a 3x3 Laplacian operator. + /// + /// + public static EdgeDetectorKernel Laplacian3x3 = new EdgeDetectorKernel(LaplacianKernels.Laplacian3x3); + + /// + /// An edge detection kernel containing a 5x5 Laplacian operator. + /// + /// + public static EdgeDetectorKernel Laplacian5x5 = new EdgeDetectorKernel(LaplacianKernels.Laplacian5x5); + + /// + /// An edge detection kernel containing a Laplacian of Gaussian operator. + /// . + /// + public static EdgeDetectorKernel LaplacianOfGaussian = new EdgeDetectorKernel(LaplacianKernels.LaplacianOfGaussianXY); + + /// + /// Initializes a new instance of the struct. + /// + /// The 2D gradient operator. + public EdgeDetectorKernel(DenseMatrix kernelXY) + => this.KernelXY = kernelXY; + + /// + /// Gets the 2D gradient operator. + /// + public DenseMatrix KernelXY { get; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(EdgeDetectorKernel left, EdgeDetectorKernel right) + => left.Equals(right); + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(EdgeDetectorKernel left, EdgeDetectorKernel right) + => !(left == right); + + /// + public override bool Equals(object obj) + => obj is EdgeDetectorKernel kernel && this.Equals(kernel); + + /// + public bool Equals(EdgeDetectorKernel other) + => this.KernelXY.Equals(other.KernelXY); + + /// + public override int GetHashCode() => this.KernelXY.GetHashCode(); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/KayyaliKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/KayyaliKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KayyaliKernels.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/KirschKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs similarity index 79% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/KirschKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs index 28c5590ef..87ccb174d 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Kernels/KirschKernels.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/KirschKernels.cs @@ -1,17 +1,18 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Processing.Processors.Convolution { /// - /// Contains the eight matrices used for Kirsch edge detection + /// Contains the eight matrices used for Kirsch edge detection. + /// . /// - internal class KirschKernels : CompassKernels + internal static class KirschKernels { /// /// Gets the North gradient operator /// - public override DenseMatrix North => + public static DenseMatrix North => new float[,] { { 5, 5, 5 }, @@ -22,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the NorthWest gradient operator /// - public override DenseMatrix NorthWest => + public static DenseMatrix NorthWest => new float[,] { { 5, 5, -3 }, @@ -33,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the West gradient operator /// - public override DenseMatrix West => + public static DenseMatrix West => new float[,] { { 5, -3, -3 }, @@ -44,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the SouthWest gradient operator /// - public override DenseMatrix SouthWest => + public static DenseMatrix SouthWest => new float[,] { { -3, -3, -3 }, @@ -55,7 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the South gradient operator /// - public override DenseMatrix South => + public static DenseMatrix South => new float[,] { { -3, -3, -3 }, @@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the SouthEast gradient operator /// - public override DenseMatrix SouthEast => + public static DenseMatrix SouthEast => new float[,] { { -3, -3, -3 }, @@ -77,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the East gradient operator /// - public override DenseMatrix East => + public static DenseMatrix East => new float[,] { { -3, -3, 5 }, @@ -88,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the NorthEast gradient operator /// - public override DenseMatrix NorthEast => + public static DenseMatrix NorthEast => new float[,] { { -3, 5, 5 }, @@ -96,4 +97,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { -3, -3, -3 } }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/LaplacianKernelFactory.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/LaplacianKernelFactory.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernelFactory.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/LaplacianKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs similarity index 80% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/LaplacianKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs index ce574341f..7036300c7 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Kernels/LaplacianKernels.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/LaplacianKernels.cs @@ -1,10 +1,12 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Processing.Processors.Convolution { /// - /// Contains Laplacian kernels of different sizes + /// Contains Laplacian kernels of different sizes. + /// + /// . /// internal static class LaplacianKernels { @@ -31,4 +33,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { 0, 0, -1, 0, 0 } }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/PrewittKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/PrewittKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/PrewittKernels.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/RobertsCrossKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/RobertsCrossKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobertsCrossKernels.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/RobinsonKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs similarity index 78% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/RobinsonKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs index 857c772b0..7d0478aa5 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Kernels/RobinsonKernels.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/RobinsonKernels.cs @@ -1,17 +1,18 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Processing.Processors.Convolution { /// /// Contains the kernels used for Robinson edge detection. + /// /// - internal class RobinsonKernels : CompassKernels + internal static class RobinsonKernels { /// /// Gets the North gradient operator /// - public override DenseMatrix North => + public static DenseMatrix North => new float[,] { { 1, 2, 1 }, @@ -22,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the NorthWest gradient operator /// - public override DenseMatrix NorthWest => + public static DenseMatrix NorthWest => new float[,] { { 2, 1, 0 }, @@ -33,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the West gradient operator /// - public override DenseMatrix West => + public static DenseMatrix West => new float[,] { { 1, 0, -1 }, @@ -44,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the SouthWest gradient operator /// - public override DenseMatrix SouthWest => + public static DenseMatrix SouthWest => new float[,] { { 0, -1, -2 }, @@ -55,7 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the South gradient operator /// - public override DenseMatrix South => + public static DenseMatrix South => new float[,] { { -1, -2, -1 }, @@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the SouthEast gradient operator /// - public override DenseMatrix SouthEast => + public static DenseMatrix SouthEast => new float[,] { { -2, -1, 0 }, @@ -77,7 +78,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the East gradient operator /// - public override DenseMatrix East => + public static DenseMatrix East => new float[,] { { -1, 0, 1 }, @@ -88,7 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Gets the NorthEast gradient operator /// - public override DenseMatrix NorthEast => + public static DenseMatrix NorthEast => new float[,] { { 0, 1, 2 }, @@ -96,4 +97,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { -2, -1, 0 } }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/ScharrKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/ScharrKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/ScharrKernels.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernels/SobelKernels.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Convolution/Kernels/SobelKernels.cs rename to src/ImageSharp/Processing/Processors/Convolution/Kernels/Implementation/SobelKernels.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/KirschProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/KirschProcessor.cs deleted file mode 100644 index 62bd17016..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/KirschProcessor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection using the Kirsch operator filter. - /// See . - /// - public sealed class KirschProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public KirschProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetectorCompassProcessor(configuration, new KirschKernels(), this.Grayscale, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Laplacian3x3Processor.cs b/src/ImageSharp/Processing/Processors/Convolution/Laplacian3x3Processor.cs deleted file mode 100644 index 957c61b07..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/Laplacian3x3Processor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Applies edge detection processing to the image using the Laplacian 3x3 operator filter. - /// - /// - public sealed class Laplacian3x3Processor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public Laplacian3x3Processor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetectorProcessor(configuration, LaplacianKernels.Laplacian3x3, this.Grayscale, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/Laplacian5x5Processor.cs b/src/ImageSharp/Processing/Processors/Convolution/Laplacian5x5Processor.cs deleted file mode 100644 index 8b28662ae..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/Laplacian5x5Processor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection processing using the Laplacian 5x5 operator filter. - /// . - /// - public sealed class Laplacian5x5Processor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public Laplacian5x5Processor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetectorProcessor(configuration, LaplacianKernels.Laplacian5x5, this.Grayscale, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/LaplacianOfGaussianProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/LaplacianOfGaussianProcessor.cs deleted file mode 100644 index 5c8f7c40d..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/LaplacianOfGaussianProcessor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Applies edge detection processing to the image using the Laplacian of Gaussian operator filter. - /// See . - /// - public sealed class LaplacianOfGaussianProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public LaplacianOfGaussianProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetectorProcessor(configuration, LaplacianKernels.LaplacianOfGaussianXY, this.Grayscale, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/PrewittProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/PrewittProcessor.cs deleted file mode 100644 index c5ae14935..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/PrewittProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection using the Prewitt operator filter. - /// See . - /// - public sealed class PrewittProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public PrewittProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetector2DProcessor( - configuration, - PrewittKernels.PrewittX, - PrewittKernels.PrewittY, - this.Grayscale, - source, - sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/RobertsCrossProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/RobertsCrossProcessor.cs deleted file mode 100644 index 57df83a14..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/RobertsCrossProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection processing using the Roberts Cross operator filter. - /// See . - /// - public sealed class RobertsCrossProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public RobertsCrossProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetector2DProcessor( - configuration, - RobertsCrossKernels.RobertsCrossX, - RobertsCrossKernels.RobertsCrossY, - this.Grayscale, - source, - sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/RobinsonProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/RobinsonProcessor.cs deleted file mode 100644 index 22c8562e2..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/RobinsonProcessor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection using the Robinson operator filter. - /// See . - /// - public sealed class RobinsonProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public RobinsonProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetectorCompassProcessor(configuration, new RobinsonKernels(), this.Grayscale, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/ScharrProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/ScharrProcessor.cs deleted file mode 100644 index d9a0745e2..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/ScharrProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection processing using the Scharr operator filter. - /// - /// - public sealed class ScharrProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public ScharrProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetector2DProcessor( - configuration, - ScharrKernels.ScharrX, - ScharrKernels.ScharrY, - this.Grayscale, - source, - sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Convolution/SobelProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/SobelProcessor.cs deleted file mode 100644 index 73632392e..000000000 --- a/src/ImageSharp/Processing/Processors/Convolution/SobelProcessor.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Convolution -{ - /// - /// Defines edge detection using the Sobel operator filter. - /// See . - /// - public sealed class SobelProcessor : EdgeDetectorProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// Whether to convert the image to grayscale before performing edge detection. - public SobelProcessor(bool grayscale) - : base(grayscale) - { - } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new EdgeDetector2DProcessor( - configuration, - SobelKernels.SobelX, - SobelKernels.SobelY, - this.Grayscale, - source, - sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs index ebc8f0e4f..dd9c06938 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Configuration configuration = this.Source.GetConfiguration(); // Detect the edges. - new SobelProcessor(false).Execute(this.Configuration, temp, this.SourceRectangle); + new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle); // Apply threshold binarization filter. new BinaryThresholdProcessor(this.definition.Threshold).Execute(this.Configuration, temp, this.SourceRectangle); diff --git a/tests/ImageSharp.Benchmarks/Samplers/DetectEdges.cs b/tests/ImageSharp.Benchmarks/Samplers/DetectEdges.cs index d40201bd8..ce2fa988c 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/DetectEdges.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/DetectEdges.cs @@ -37,17 +37,17 @@ namespace SixLabors.ImageSharp.Benchmarks [Benchmark(Description = "ImageSharp DetectEdges")] public void ImageProcessorCoreDetectEdges() { - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Kayyali)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Kayyali)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Kirsch)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Laplacian3x3)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Laplacian5x5)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.LaplacianOfGaussian)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Prewitt)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.RobertsCross)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Robinson)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Scharr)); - this.image.Mutate(x => x.DetectEdges(EdgeDetectionOperators.Sobel)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Kayyali)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Kayyali)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Kirsch)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Laplacian3x3)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Laplacian5x5)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.LaplacianOfGaussian)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Prewitt)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.RobertsCross)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Robinson)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Scharr)); + this.image.Mutate(x => x.DetectEdges(KnownEdgeDetectorKernels.Sobel)); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 912f606b2..78218aec9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.IO; @@ -103,7 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Theory] [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)] - public void DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + public void Decode_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) where TPixel : unmanaged, IPixel { provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); @@ -112,6 +114,60 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.IsType(ex.InnerException); } + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)] + public async Task DecodeAsnc_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); + InvalidImageContentException ex = await Assert.ThrowsAsync(() => provider.GetImageAsync(JpegDecoder)); + this.Output.WriteLine(ex.Message); + Assert.IsType(ex.InnerException); + } + + [Theory] + [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 10)] + [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 10)] + [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)] + public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs) + { + // Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay. + string hugeFile = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + if (cancellationDelayMs == 0) + { + cts.Cancel(); + } + else + { + cts.CancelAfter(cancellationDelayMs); + } + + await Assert.ThrowsAsync(() => Image.LoadAsync(hugeFile, cts.Token)); + } + + [Theory(Skip = "Identify is too fast, doesn't work reliably.")] + [InlineData(TestImages.Jpeg.Baseline.Exif)] + [InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)] + public async Task Identify_IsCancellable(string fileName) + { + string file = Path.Combine( + TestEnvironment.InputImagesDirectoryFullPath, + fileName); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromTicks(1)); + await Assert.ThrowsAsync(() => Image.IdentifyAsync(file, cts.Token)); + } + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 6c9a74463..981270a5f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -284,5 +287,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg IccProfile values = input.Metadata.IccProfile; Assert.Equal(values.Entries, actual.Entries); } + + [Theory] + [InlineData(JpegSubsample.Ratio420, 0)] + [InlineData(JpegSubsample.Ratio420, 3)] + [InlineData(JpegSubsample.Ratio420, 10)] + [InlineData(JpegSubsample.Ratio444, 0)] + [InlineData(JpegSubsample.Ratio444, 3)] + [InlineData(JpegSubsample.Ratio444, 10)] + public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs) + { + using var image = new Image(5000, 5000); + using MemoryStream stream = new MemoryStream(); + var cts = new CancellationTokenSource(); + if (cancellationDelayMs == 0) + { + cts.Cancel(); + } + else + { + cts.CancelAfter(cancellationDelayMs); + } + + var encoder = new JpegEncoder() { Subsample = subsample }; + await Assert.ThrowsAsync(() => image.SaveAsync(stream, encoder, cts.Token)); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs index 12e1ec22b..0dd2abcc1 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegImagePostProcessorTests.cs @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) using (var image = new Image(decoder.ImageWidth, decoder.ImageHeight)) { - pp.PostProcess(image.Frames.RootFrame); + pp.PostProcess(image.Frames.RootFrame, default); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs new file mode 100644 index 000000000..317a5129c --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public partial class ImageTests + { + public class Decode_Cancellation : ImageLoadTestBase + { + private bool isTestStreamSeekable; + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore = new SemaphoreSlim(0); + private readonly SemaphoreSlim continueSemaphore = new SemaphoreSlim(0); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + + public Decode_Cancellation() + { + this.TopLevelConfiguration.StreamProcessingBufferSize = 128; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task LoadAsync_Specific_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task LoadAsync_Agnostic_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task LoadAsync_Agnostic_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Fact] + public async Task LoadAsync_Specific_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.LoadAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task IdentifyAsync_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task IdentifyAsync_CustomConfiguration_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task IdentifyWithFormatAsync_CustomConfiguration_Stream(bool isInputStreamSeekable) + { + this.isTestStreamSeekable = isInputStreamSeekable; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.TopLevelConfiguration, this.DataStream, this.cts.Token)); + } + + [Fact] + public async Task IdentifyWithFormatAsync_CustomConfiguration_Path() + { + this.isTestStreamSeekable = true; + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.TopLevelConfiguration, this.MockFilePath, this.cts.Token)); + } + + [Fact] + public async Task IdentifyWithFormatAsync_DefaultConfiguration_Stream() + { + _ = Task.Factory.StartNew(this.DoCancel, TaskCreationOptions.LongRunning); + + await Assert.ThrowsAsync(() => Image.IdentifyWithFormatAsync(this.DataStream, this.cts.Token)); + } + + private async Task DoCancel() + { + // wait until we reach the middle of the steam + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + + // set the cancellation + this.cts.Cancel(); + + // continue processing the stream + this.continueSemaphore.Release(); + } + + protected override Stream CreateStream() => this.TestFormat.CreateAsyncSamaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable); + } + } +} diff --git a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs index 6327b3eac..5f3b16806 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs @@ -24,8 +24,6 @@ namespace SixLabors.ImageSharp.Tests private ReadOnlySpan ActualImageSpan => this.ActualImageBytes.AsSpan(); - private byte[] ByteArray => this.DataStream.ToArray(); - private IImageFormat LocalImageFormat => this.localImageFormatMock.Object; private static readonly IImageFormat ExpectedGlobalFormat = diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs index 69b1d21a6..72de3fcc4 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs @@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Tests { private static readonly string ActualImagePath = TestFile.GetInputFileFullPath(TestImages.Bmp.F); - private byte[] ActualImageBytes => TestFile.Create(TestImages.Bmp.F).Bytes; + private static readonly Size ExpectedImageSize = new Size(108, 202); - private byte[] ByteArray => this.DataStream.ToArray(); + private byte[] ActualImageBytes => TestFile.Create(TestImages.Bmp.F).Bytes; private IImageInfo LocalImageInfo => this.localImageInfoMock.Object; @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests { IImageInfo info = Image.Identify(this.ActualImageBytes, out IImageFormat type); - Assert.NotNull(info); + Assert.Equal(ExpectedImageSize, info.Size()); Assert.Equal(ExpectedGlobalFormat, type); } @@ -133,13 +133,45 @@ namespace SixLabors.ImageSharp.Tests using (var stream = new MemoryStream(this.ActualImageBytes)) { var asyncStream = new AsyncStreamWrapper(stream, () => false); - (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(asyncStream); + (IImageInfo ImageInfo, IImageFormat Format) res = await Image.IdentifyWithFormatAsync(asyncStream); - Assert.NotNull(info.ImageInfo); - Assert.Equal(ExpectedGlobalFormat, info.Format); + Assert.Equal(ExpectedImageSize, res.ImageInfo.Size()); + Assert.Equal(ExpectedGlobalFormat, res.Format); } } + [Fact] + public async Task FromPathAsync_CustomConfiguration() + { + IImageInfo info = await Image.IdentifyAsync(this.LocalConfiguration, this.MockFilePath); + Assert.Equal(this.LocalImageInfo, info); + } + + [Fact] + public async Task IdentifyWithFormatAsync_FromPath_CustomConfiguration() + { + (IImageInfo ImageInfo, IImageFormat Format) info = await Image.IdentifyWithFormatAsync(this.LocalConfiguration, this.MockFilePath); + Assert.NotNull(info.ImageInfo); + Assert.Equal(this.LocalImageFormat, info.Format); + } + + [Fact] + public async Task IdentifyWithFormatAsync_FromPath_GlobalConfiguration() + { + (IImageInfo ImageInfo, IImageFormat Format) res = await Image.IdentifyWithFormatAsync(ActualImagePath); + + Assert.Equal(ExpectedImageSize, res.ImageInfo.Size()); + Assert.Equal(ExpectedGlobalFormat, res.Format); + } + + [Fact] + public async Task FromPathAsync_GlobalConfiguration() + { + IImageInfo info = await Image.IdentifyAsync(ActualImagePath); + + Assert.Equal(ExpectedImageSize, info.Size()); + } + [Fact] public async Task FromStreamAsync_CustomConfiguration() { diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index 70d572d60..44d7daa74 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -3,7 +3,7 @@ using System; using System.IO; - +using System.Threading; using Moq; using SixLabors.ImageSharp.Formats; @@ -16,6 +16,8 @@ namespace SixLabors.ImageSharp.Tests { public abstract class ImageLoadTestBase : IDisposable { + private Lazy dataStreamLazy; + protected Image localStreamReturnImageRgba32; protected Image localStreamReturnImageAgnostic; @@ -46,10 +48,12 @@ namespace SixLabors.ImageSharp.Tests public byte[] Marker { get; } - public MemoryStream DataStream { get; } + public Stream DataStream => this.dataStreamLazy.Value; public byte[] DecodedData { get; private set; } + protected byte[] ByteArray => ((MemoryStream)this.DataStream).ToArray(); + protected ImageLoadTestBase() { this.localStreamReturnImageRgba32 = new Image(1, 1); @@ -60,10 +64,9 @@ namespace SixLabors.ImageSharp.Tests var detector = new Mock(); detector.Setup(x => x.Identify(It.IsAny(), It.IsAny())).Returns(this.localImageInfoMock.Object); - detector.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny())).ReturnsAsync(this.localImageInfoMock.Object); - this.localDecoder = detector.As(); - this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormatMock.Object); + detector.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(this.localImageInfoMock.Object); + this.localDecoder = detector.As(); this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny())) .Callback((c, s) => { @@ -86,6 +89,8 @@ namespace SixLabors.ImageSharp.Tests }) .Returns(this.localStreamReturnImageAgnostic); + this.localMimeTypeDetector = new MockImageFormatDetector(this.localImageFormatMock.Object); + this.LocalConfiguration = new Configuration(); this.LocalConfiguration.ImageFormatsManager.AddImageFormatDetector(this.localMimeTypeDetector); this.LocalConfiguration.ImageFormatsManager.SetDecoder(this.localImageFormatMock.Object, this.localDecoder.Object); @@ -93,10 +98,12 @@ namespace SixLabors.ImageSharp.Tests this.TopLevelConfiguration = new Configuration(this.TestFormat); this.Marker = Guid.NewGuid().ToByteArray(); - this.DataStream = this.TestFormat.CreateStream(this.Marker); - this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(this.DataStream); - this.topLevelFileSystem.AddFile(this.MockFilePath, this.DataStream); + this.dataStreamLazy = new Lazy(this.CreateStream); + Stream StreamFactory() => this.DataStream; + + this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory); + this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory); this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object; this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem; } @@ -107,6 +114,8 @@ namespace SixLabors.ImageSharp.Tests this.localStreamReturnImageRgba32?.Dispose(); this.localStreamReturnImageAgnostic?.Dispose(); } + + protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs index 77e5679f6..a5034e43b 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_UseDefaultConfiguration.cs @@ -25,84 +25,80 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void Path_Specific() { - using (var img = Image.Load(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path); + VerifyDecodedImage(img); } [Fact] public void Path_Agnostic() { - using (var img = Image.Load(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path); + VerifyDecodedImage(img); } [Fact] public async Task Path_Agnostic_Async() { - using (var img = await Image.LoadAsync(this.Path)) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); + } + + [Fact] + public async Task Path_Specific_Async() + { + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); } [Fact] public async Task Path_Agnostic_Configuration_Async() { - using (var img = await Image.LoadAsync(Configuration.Default, this.Path)) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path); + VerifyDecodedImage(img); } [Fact] public void Path_Decoder_Specific() { - using (var img = Image.Load(this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public void Path_Decoder_Agnostic() { - using (var img = Image.Load(this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = Image.Load(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public async Task Path_Decoder_Agnostic_Async() { - using (var img = await Image.LoadAsync(Configuration.Default, this.Path, new BmpDecoder())) - { - VerifyDecodedImage(img); - } + using var img = await Image.LoadAsync(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); + } + + [Fact] + public async Task Path_Decoder_Specific_Async() + { + using var img = await Image.LoadAsync(this.Path, new BmpDecoder()); + VerifyDecodedImage(img); } [Fact] public void Path_OutFormat_Specific() { - using (var img = Image.Load(this.Path, out IImageFormat format)) - { - VerifyDecodedImage(img); - Assert.IsType(format); - } + using var img = Image.Load(this.Path, out IImageFormat format); + VerifyDecodedImage(img); + Assert.IsType(format); } [Fact] public void Path_OutFormat_Agnostic() { - using (var img = Image.Load(this.Path, out IImageFormat format)) - { - VerifyDecodedImage(img); - Assert.IsType(format); - } + using var img = Image.Load(this.Path, out IImageFormat format); + VerifyDecodedImage(img); + Assert.IsType(format); } [Fact] @@ -124,6 +120,20 @@ namespace SixLabors.ImageSharp.Tests Image.Load((string)null); }); } + + [Fact] + public Task Async_WhenFileNotFound_Throws() + { + return Assert.ThrowsAsync( + () => Image.LoadAsync(Guid.NewGuid().ToString())); + } + + [Fact] + public Task Async_WhenPathIsNull_Throws() + { + return Assert.ThrowsAsync( + () => Image.LoadAsync((string)null)); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs index d275a11d8..320f3696d 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs @@ -14,8 +14,6 @@ namespace SixLabors.ImageSharp.Tests { public class Load_FromBytes_PassLocalConfiguration : ImageLoadTestBase { - private byte[] ByteArray => this.DataStream.ToArray(); - private ReadOnlySpan ByteSpan => this.ByteArray.AsSpan(); [Theory] diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 4a6c96ae8..40c3b65b5 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -3,9 +3,9 @@ using System; using System.IO; - +using System.Threading; using Moq; - +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("test.bmp")] [InlineData("test.jpg")] [InlineData("test.gif")] - public async Task SaveNeverCallsSyncMethods(string filename) + public async Task SaveAsync_NeverCallsSyncMethods(string filename) { using (var image = new Image(5, 5)) { @@ -102,6 +102,20 @@ namespace SixLabors.ImageSharp.Tests } } } + + [Fact] + public async Task SaveAsync_WithNonSeekableStream_IsCancellable() + { + using var image = new Image(4000, 4000); + var encoder = new PngEncoder() { CompressionLevel = PngCompressionLevel.BestCompression }; + using var stream = new MemoryStream(); + var asyncStream = new AsyncStreamWrapper(stream, () => false); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromTicks(1)); + + await Assert.ThrowsAnyAsync(() => + image.SaveAsync(asyncStream, encoder, cts.Token)); + } } } } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 98f8e9574..0761b0978 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs index a0e9be110..3ffb8f4e3 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/DetectEdgesTest.cs @@ -1,75 +1,201 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Collections.Generic; - +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Convolution; -using SixLabors.ImageSharp.Tests.TestUtilities; - using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Convolution { + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "OK. Used for TheoryData compatibility.")] public class DetectEdgesTest : BaseImageOperationsExtensionTest { [Fact] - public void DetectEdges_SobelProcessorDefaultsSet() + public void DetectEdges_EdgeDetector2DProcessorDefaultsSet() { this.operations.DetectEdges(); + EdgeDetector2DProcessor processor = this.Verify(); - // TODO: Enable once we have updated the images - SobelProcessor processor = this.Verify(); Assert.True(processor.Grayscale); + Assert.Equal(KnownEdgeDetectorKernels.Sobel, processor.Kernel); } [Fact] - public void DetectEdges_Rect_SobelProcessorDefaultsSet() + public void DetectEdges_Rect_EdgeDetector2DProcessorDefaultsSet() { this.operations.DetectEdges(this.rect); + EdgeDetector2DProcessor processor = this.Verify(this.rect); + + Assert.True(processor.Grayscale); + Assert.Equal(KnownEdgeDetectorKernels.Sobel, processor.Kernel); + } + + public static TheoryData EdgeDetector2DKernelData = + new TheoryData + { + { KnownEdgeDetectorKernels.Kayyali, true }, + { KnownEdgeDetectorKernels.Kayyali, false }, + { KnownEdgeDetectorKernels.Prewitt, true }, + { KnownEdgeDetectorKernels.Prewitt, false }, + { KnownEdgeDetectorKernels.RobertsCross, true }, + { KnownEdgeDetectorKernels.RobertsCross, false }, + { KnownEdgeDetectorKernels.Scharr, true }, + { KnownEdgeDetectorKernels.Scharr, false }, + { KnownEdgeDetectorKernels.Sobel, true }, + { KnownEdgeDetectorKernels.Sobel, false }, + }; + + [Theory] + [MemberData(nameof(EdgeDetector2DKernelData))] + public void DetectEdges_EdgeDetector2DProcessor_DefaultGrayScale_Set(EdgeDetector2DKernel kernel, bool _) + { + this.operations.DetectEdges(kernel); + EdgeDetector2DProcessor processor = this.Verify(); + + Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetector2DKernelData))] + public void DetectEdges_Rect_EdgeDetector2DProcessor_DefaultGrayScale_Set(EdgeDetector2DKernel kernel, bool _) + { + this.operations.DetectEdges(kernel, this.rect); + EdgeDetector2DProcessor processor = this.Verify(this.rect); + + Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetector2DKernelData))] + public void DetectEdges_EdgeDetector2DProcessorSet(EdgeDetector2DKernel kernel, bool grayscale) + { + this.operations.DetectEdges(kernel, grayscale); + EdgeDetector2DProcessor processor = this.Verify(); + + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetector2DKernelData))] + public void DetectEdges_Rect_EdgeDetector2DProcessorSet(EdgeDetector2DKernel kernel, bool grayscale) + { + this.operations.DetectEdges(kernel, grayscale, this.rect); + EdgeDetector2DProcessor processor = this.Verify(this.rect); + + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + public static TheoryData EdgeDetectorKernelData = + new TheoryData + { + { KnownEdgeDetectorKernels.Laplacian3x3, true }, + { KnownEdgeDetectorKernels.Laplacian3x3, false }, + { KnownEdgeDetectorKernels.Laplacian5x5, true }, + { KnownEdgeDetectorKernels.Laplacian5x5, false }, + { KnownEdgeDetectorKernels.LaplacianOfGaussian, true }, + { KnownEdgeDetectorKernels.LaplacianOfGaussian, false }, + }; + + [Theory] + [MemberData(nameof(EdgeDetectorKernelData))] + public void DetectEdges_EdgeDetectorProcessor_DefaultGrayScale_Set(EdgeDetectorKernel kernel, bool _) + { + this.operations.DetectEdges(kernel); + EdgeDetectorProcessor processor = this.Verify(); - // TODO: Enable once we have updated the images - SobelProcessor processor = this.Verify(this.rect); Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); } - public static IEnumerable EdgeDetectionTheoryData => new[] + [Theory] + [MemberData(nameof(EdgeDetectorKernelData))] + public void DetectEdges_Rect_EdgeDetectorProcessor_DefaultGrayScale_Set(EdgeDetectorKernel kernel, bool _) { - new object[] { new TestType(), EdgeDetectionOperators.Kayyali }, - new object[] { new TestType(), EdgeDetectionOperators.Kirsch }, - new object[] { new TestType(), EdgeDetectionOperators.Laplacian3x3 }, - new object[] { new TestType(), EdgeDetectionOperators.Laplacian5x5 }, - new object[] { new TestType(), EdgeDetectionOperators.LaplacianOfGaussian }, - new object[] { new TestType(), EdgeDetectionOperators.Prewitt }, - new object[] { new TestType(), EdgeDetectionOperators.RobertsCross }, - new object[] { new TestType(), EdgeDetectionOperators.Robinson }, - new object[] { new TestType(), EdgeDetectionOperators.Scharr }, - new object[] { new TestType(), EdgeDetectionOperators.Sobel }, - }; + this.operations.DetectEdges(kernel, this.rect); + EdgeDetectorProcessor processor = this.Verify(this.rect); + + Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } [Theory] - [MemberData(nameof(EdgeDetectionTheoryData))] - public void DetectEdges_filter_SobelProcessorDefaultsSet(TestType type, EdgeDetectionOperators filter) - where TProcessor : EdgeDetectorProcessor + [MemberData(nameof(EdgeDetectorKernelData))] + public void DetectEdges_EdgeDetectorProcessorSet(EdgeDetectorKernel kernel, bool grayscale) { - this.operations.DetectEdges(filter); + this.operations.DetectEdges(kernel, grayscale); + EdgeDetectorProcessor processor = this.Verify(); + + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetectorKernelData))] + public void DetectEdges_Rect_EdgeDetectorProcessorSet(EdgeDetectorKernel kernel, bool grayscale) + { + this.operations.DetectEdges(kernel, grayscale, this.rect); + EdgeDetectorProcessor processor = this.Verify(this.rect); + + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + public static TheoryData EdgeDetectorCompassKernelData = + new TheoryData + { + { KnownEdgeDetectorKernels.Kirsch, true }, + { KnownEdgeDetectorKernels.Kirsch, false }, + { KnownEdgeDetectorKernels.Robinson, true }, + { KnownEdgeDetectorKernels.Robinson, false }, + }; + + [Theory] + [MemberData(nameof(EdgeDetectorCompassKernelData))] + public void DetectEdges_EdgeDetectorCompassProcessor_DefaultGrayScale_Set(EdgeDetectorCompassKernel kernel, bool _) + { + this.operations.DetectEdges(kernel); + EdgeDetectorCompassProcessor processor = this.Verify(); + + Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetectorCompassKernelData))] + public void DetectEdges_Rect_EdgeDetectorCompassProcessor_DefaultGrayScale_Set(EdgeDetectorCompassKernel kernel, bool _) + { + this.operations.DetectEdges(kernel, this.rect); + EdgeDetectorCompassProcessor processor = this.Verify(this.rect); - // TODO: Enable once we have updated the images - var processor = this.Verify(); Assert.True(processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); + } + + [Theory] + [MemberData(nameof(EdgeDetectorCompassKernelData))] + public void DetectEdges_EdgeDetectorCompassProcessorSet(EdgeDetectorCompassKernel kernel, bool grayscale) + { + this.operations.DetectEdges(kernel, grayscale); + EdgeDetectorCompassProcessor processor = this.Verify(); + + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); } [Theory] - [MemberData(nameof(EdgeDetectionTheoryData))] - public void DetectEdges_filter_grayscale_SobelProcessorDefaultsSet(TestType type, EdgeDetectionOperators filter) - where TProcessor : EdgeDetectorProcessor + [MemberData(nameof(EdgeDetectorCompassKernelData))] + public void DetectEdges_Rect_EdgeDetectorCompassProcessorSet(EdgeDetectorCompassKernel kernel, bool grayscale) { - bool grey = (int)filter % 2 == 0; - this.operations.DetectEdges(filter, grey); + this.operations.DetectEdges(kernel, grayscale, this.rect); + EdgeDetectorCompassProcessor processor = this.Verify(this.rect); - // TODO: Enable once we have updated the images - var processor = this.Verify(); - Assert.Equal(grey, processor.Grayscale); + Assert.Equal(grayscale, processor.Grayscale); + Assert.Equal(kernel, processor.Kernel); } } } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/Processors/EdgeDetectorKernelTests.cs b/tests/ImageSharp.Tests/Processing/Convolution/Processors/EdgeDetectorKernelTests.cs new file mode 100644 index 000000000..b50e5d7cc --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Convolution/Processors/EdgeDetectorKernelTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Convolution.Processors +{ + public class EdgeDetectorKernelTests + { + [Fact] + public void EdgeDetectorKernelEqualityOperatorTest() + { + EdgeDetectorKernel kernel0 = KnownEdgeDetectorKernels.Laplacian3x3; + EdgeDetectorKernel kernel1 = KnownEdgeDetectorKernels.Laplacian3x3; + EdgeDetectorKernel kernel2 = KnownEdgeDetectorKernels.Laplacian5x5; + + Assert.True(kernel0 == kernel1); + Assert.False(kernel0 != kernel1); + + Assert.True(kernel0 != kernel2); + Assert.False(kernel0 == kernel2); + + Assert.True(kernel0.Equals((object)kernel1)); + Assert.True(kernel0.Equals(kernel1)); + + Assert.False(kernel0.Equals((object)kernel2)); + Assert.False(kernel0.Equals(kernel2)); + + Assert.Equal(kernel0.GetHashCode(), kernel1.GetHashCode()); + Assert.NotEqual(kernel0.GetHashCode(), kernel2.GetHashCode()); + } + + [Fact] + public void EdgeDetector2DKernelEqualityOperatorTest() + { + EdgeDetector2DKernel kernel0 = KnownEdgeDetectorKernels.Prewitt; + EdgeDetector2DKernel kernel1 = KnownEdgeDetectorKernels.Prewitt; + EdgeDetector2DKernel kernel2 = KnownEdgeDetectorKernels.RobertsCross; + + Assert.True(kernel0 == kernel1); + Assert.False(kernel0 != kernel1); + + Assert.True(kernel0 != kernel2); + Assert.False(kernel0 == kernel2); + + Assert.True(kernel0.Equals((object)kernel1)); + Assert.True(kernel0.Equals(kernel1)); + + Assert.False(kernel0.Equals((object)kernel2)); + Assert.False(kernel0.Equals(kernel2)); + + Assert.Equal(kernel0.GetHashCode(), kernel1.GetHashCode()); + Assert.NotEqual(kernel0.GetHashCode(), kernel2.GetHashCode()); + } + + [Fact] + public void EdgeDetectorCompassKernelEqualityOperatorTest() + { + EdgeDetectorCompassKernel kernel0 = KnownEdgeDetectorKernels.Kirsch; + EdgeDetectorCompassKernel kernel1 = KnownEdgeDetectorKernels.Kirsch; + EdgeDetectorCompassKernel kernel2 = KnownEdgeDetectorKernels.Robinson; + + Assert.True(kernel0 == kernel1); + Assert.False(kernel0 != kernel1); + + Assert.True(kernel0 != kernel2); + Assert.False(kernel0 == kernel2); + + Assert.True(kernel0.Equals((object)kernel1)); + Assert.True(kernel0.Equals(kernel1)); + + Assert.False(kernel0.Equals((object)kernel2)); + Assert.False(kernel0.Equals(kernel2)); + + Assert.Equal(kernel0.GetHashCode(), kernel1.GetHashCode()); + Assert.NotEqual(kernel0.GetHashCode(), kernel2.GetHashCode()); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index dbbf624db..e468778de 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -1,8 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -10,6 +12,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution { [GroupOutput("Convolution")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "OK. Used for TheoryData compatibility.")] public class DetectEdgesTest { private static readonly ImageComparer OpaqueComparer = ImageComparer.TolerantPercentage(0.01F); @@ -20,18 +23,29 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; - public static readonly TheoryData DetectEdgesFilters = new TheoryData + public static readonly TheoryData DetectEdgesFilters + = new TheoryData { - EdgeDetectionOperators.Kayyali, - EdgeDetectionOperators.Kirsch, - EdgeDetectionOperators.Laplacian3x3, - EdgeDetectionOperators.Laplacian5x5, - EdgeDetectionOperators.LaplacianOfGaussian, - EdgeDetectionOperators.Prewitt, - EdgeDetectionOperators.RobertsCross, - EdgeDetectionOperators.Robinson, - EdgeDetectionOperators.Scharr, - EdgeDetectionOperators.Sobel + { KnownEdgeDetectorKernels.Laplacian3x3, nameof(KnownEdgeDetectorKernels.Laplacian3x3) }, + { KnownEdgeDetectorKernels.Laplacian5x5, nameof(KnownEdgeDetectorKernels.Laplacian5x5) }, + { KnownEdgeDetectorKernels.LaplacianOfGaussian, nameof(KnownEdgeDetectorKernels.LaplacianOfGaussian) }, + }; + + public static readonly TheoryData DetectEdges2DFilters + = new TheoryData + { + { KnownEdgeDetectorKernels.Kayyali, nameof(KnownEdgeDetectorKernels.Kayyali) }, + { KnownEdgeDetectorKernels.Prewitt, nameof(KnownEdgeDetectorKernels.Prewitt) }, + { KnownEdgeDetectorKernels.RobertsCross, nameof(KnownEdgeDetectorKernels.RobertsCross) }, + { KnownEdgeDetectorKernels.Scharr, nameof(KnownEdgeDetectorKernels.Scharr) }, + { KnownEdgeDetectorKernels.Sobel, nameof(KnownEdgeDetectorKernels.Sobel) }, + }; + + public static readonly TheoryData DetectEdgesCompassFilters + = new TheoryData + { + { KnownEdgeDetectorKernels.Kirsch, nameof(KnownEdgeDetectorKernels.Kirsch) }, + { KnownEdgeDetectorKernels.Robinson, nameof(KnownEdgeDetectorKernels.Robinson) }, }; [Theory] @@ -53,7 +67,48 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution [Theory] [WithTestPatternImages(nameof(DetectEdgesFilters), 100, 100, PixelTypes.Rgba32)] [WithFileCollection(nameof(TestImages), nameof(DetectEdgesFilters), PixelTypes.Rgba32)] - public void DetectEdges_WorksWithAllFilters(TestImageProvider provider, EdgeDetectionOperators detector) + public void DetectEdges_WorksWithAllFilters( + TestImageProvider provider, + EdgeDetectorKernel detector, + string name) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = provider.SourceFileOrDescription.Contains("TestPattern"); + ImageComparer comparer = hasAlpha ? TransparentComparer : OpaqueComparer; + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.DetectEdges(detector)); + image.DebugSave(provider, name); + image.CompareToReferenceOutput(comparer, provider, name); + } + } + + [Theory] + [WithTestPatternImages(nameof(DetectEdges2DFilters), 100, 100, PixelTypes.Rgba32)] + [WithFileCollection(nameof(TestImages), nameof(DetectEdges2DFilters), PixelTypes.Rgba32)] + public void DetectEdges2D_WorksWithAllFilters( + TestImageProvider provider, + EdgeDetector2DKernel detector, + string name) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = provider.SourceFileOrDescription.Contains("TestPattern"); + ImageComparer comparer = hasAlpha ? TransparentComparer : OpaqueComparer; + using (Image image = provider.GetImage()) + { + image.Mutate(x => x.DetectEdges(detector)); + image.DebugSave(provider, name); + image.CompareToReferenceOutput(comparer, provider, name); + } + } + + [Theory] + [WithTestPatternImages(nameof(DetectEdgesCompassFilters), 100, 100, PixelTypes.Rgba32)] + [WithFileCollection(nameof(TestImages), nameof(DetectEdgesCompassFilters), PixelTypes.Rgba32)] + public void DetectEdgesCompass_WorksWithAllFilters( + TestImageProvider provider, + EdgeDetectorCompassKernel detector, + string name) where TPixel : unmanaged, IPixel { bool hasAlpha = provider.SourceFileOrDescription.Contains("TestPattern"); @@ -61,8 +116,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution using (Image image = provider.GetImage()) { image.Mutate(x => x.DetectEdges(detector)); - image.DebugSave(provider, detector.ToString()); - image.CompareToReferenceOutput(comparer, provider, detector.ToString()); + image.DebugSave(provider, name); + image.CompareToReferenceOutput(comparer, provider, name); } } @@ -115,7 +170,38 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution [Theory] [WithFile(Tests.TestImages.Png.Bike, nameof(DetectEdgesFilters), PixelTypes.Rgba32)] - public void WorksWithDiscoBuffers(TestImageProvider provider, EdgeDetectionOperators detector) + public void WorksWithDiscoBuffers( + TestImageProvider provider, + EdgeDetectorKernel detector, + string _) + where TPixel : unmanaged, IPixel + { + provider.RunBufferCapacityLimitProcessorTest( + 41, + c => c.DetectEdges(detector), + detector); + } + + [Theory] + [WithFile(Tests.TestImages.Png.Bike, nameof(DetectEdges2DFilters), PixelTypes.Rgba32)] + public void WorksWithDiscoBuffers2D( + TestImageProvider provider, + EdgeDetector2DKernel detector, + string _) + where TPixel : unmanaged, IPixel + { + provider.RunBufferCapacityLimitProcessorTest( + 41, + c => c.DetectEdges(detector), + detector); + } + + [Theory] + [WithFile(Tests.TestImages.Png.Bike, nameof(DetectEdgesCompassFilters), PixelTypes.Rgba32)] + public void WorksWithDiscoBuffersCompass( + TestImageProvider provider, + EdgeDetectorCompassKernel detector, + string _) where TPixel : unmanaged, IPixel { provider.RunBufferCapacityLimitProcessorTest( diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs index 960f13637..da4362956 100644 --- a/tests/ImageSharp.Tests/TestFileSystem.cs +++ b/tests/ImageSharp.Tests/TestFileSystem.cs @@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp.Tests /// public class TestFileSystem : ImageSharp.IO.IFileSystem { - private readonly Dictionary fileSystem = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> fileSystem = new Dictionary>(StringComparer.OrdinalIgnoreCase); - public void AddFile(string path, Stream data) + public void AddFile(string path, Func data) { lock (this.fileSystem) { @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests { if (this.fileSystem.ContainsKey(path)) { - Stream stream = this.fileSystem[path]; + Stream stream = this.fileSystem[path](); stream.Position = 0; return stream; } @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests { if (this.fileSystem.ContainsKey(path)) { - Stream stream = this.fileSystem[path]; + Stream stream = this.fileSystem[path](); stream.Position = 0; return stream; } @@ -54,4 +54,4 @@ namespace SixLabors.ImageSharp.Tests return File.OpenRead(path); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index a883039f2..7273a65f7 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests @@ -32,9 +34,9 @@ namespace SixLabors.ImageSharp.Tests public List DecodeCalls { get; } = new List(); - public IImageEncoder Encoder { get; } + public TestEncoder Encoder { get; } - public IImageDecoder Decoder { get; } + public TestDecoder Decoder { get; } private byte[] header = Guid.NewGuid().ToByteArray(); @@ -52,6 +54,14 @@ namespace SixLabors.ImageSharp.Tests return ms; } + public Stream CreateAsyncSamaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512) + { + var buffer = new byte[size]; + this.header.CopyTo(buffer, 0); + var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore); + return seeakable ? (Stream)semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false); + } + public void VerifySpecificDecodeCall(byte[] marker, Configuration config) where TPixel : unmanaged, IPixel { @@ -187,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests } } - public class TestDecoder : IImageDecoder + public class TestDecoder : IImageDecoder, IImageInfoDetector { private TestFormat testFormat; @@ -204,9 +214,17 @@ namespace SixLabors.ImageSharp.Tests public Image Decode(Configuration config, Stream stream) where TPixel : unmanaged, IPixel + => this.DecodeImpl(config, stream, default).GetAwaiter().GetResult(); + + public Task> DecodeAsync(Configuration config, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.DecodeImpl(config, stream, cancellationToken); + + private async Task> DecodeImpl(Configuration config, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { var ms = new MemoryStream(); - stream.CopyTo(ms); + await stream.CopyToAsync(ms, config.StreamProcessingBufferSize, cancellationToken); var marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray(); this.testFormat.DecodeCalls.Add(new DecodeOperation { @@ -219,15 +237,18 @@ namespace SixLabors.ImageSharp.Tests return this.testFormat.Sample(); } - public Task> DecodeAsync(Configuration config, Stream stream) - where TPixel : unmanaged, IPixel - => Task.FromResult(this.Decode(config, stream)); - public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header); public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); + + public IImageInfo Identify(Configuration configuration, Stream stream) => + this.IdentifyAsync(configuration, stream, default).GetAwaiter().GetResult(); + + public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeImpl(configuration, stream, cancellationToken); } public class TestEncoder : ImageSharp.Formats.IImageEncoder @@ -249,7 +270,7 @@ namespace SixLabors.ImageSharp.Tests // TODO record this happened so we can verify it. } - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // TODO record this happened so we can verify it. diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 78567f926..440baaa63 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Reflection; - +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -164,6 +165,15 @@ namespace SixLabors.ImageSharp.Tests return cachedImage.Clone(this.Configuration); } + public override Task> GetImageAsync(IImageDecoder decoder) + { + Guard.NotNull(decoder, nameof(decoder)); + + // Used in small subset of decoder tests, no caching. + string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.FilePath); + return Image.LoadAsync(this.Configuration, path, decoder); + } + public override void Deserialize(IXunitSerializationInfo info) { this.FilePath = info.GetValue("path"); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index da641a296..700c40b72 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using System.Threading.Tasks; using Castle.Core.Internal; using SixLabors.ImageSharp.Formats; @@ -97,6 +98,11 @@ namespace SixLabors.ImageSharp.Tests throw new NotSupportedException($"Decoder specific GetImage() is not supported with {this.GetType().Name}!"); } + public virtual Task> GetImageAsync(IImageDecoder decoder) + { + throw new NotSupportedException($"Decoder specific GetImageAsync() is not supported with {this.GetType().Name}!"); + } + /// /// Returns an instance to the test case with the necessary traits. /// diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index fae3ff5a5..de8278a33 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using ImageMagick; using SixLabors.ImageSharp.Formats; @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel => Task.FromResult(this.Decode(configuration, stream)); @@ -82,6 +83,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index ae6e2e3f1..1eb1328ef 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.Formats; @@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel => Task.FromResult(this.Decode(configuration, stream)); @@ -48,7 +49,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Task IdentifyAsync(Configuration configuration, Stream stream) + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) => Task.FromResult(this.Identify(configuration, stream)); public IImageInfo Identify(Configuration configuration, Stream stream) @@ -62,6 +63,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs index 2aaf7bf80..4c28079c1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs @@ -3,6 +3,7 @@ using System.Drawing.Imaging; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; @@ -31,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Task EncodeAsync(Image image, Stream stream) + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using (System.Drawing.Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) diff --git a/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs b/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs new file mode 100644 index 000000000..f03d2c493 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/SemaphoreReadMemoryStream.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SixLabors.ImageSharp.Tests.TestUtilities +{ + internal class SemaphoreReadMemoryStream : MemoryStream + { + private readonly SemaphoreSlim continueSemaphore; + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore; + private int pauseDone; + private readonly long waitPosition; + + public SemaphoreReadMemoryStream( + byte[] buffer, + long waitPosition, + SemaphoreSlim notifyWaitPositionReachedSemaphore, + SemaphoreSlim continueSemaphore) + : base(buffer) + { + this.continueSemaphore = continueSemaphore; + this.notifyWaitPositionReachedSemaphore = notifyWaitPositionReachedSemaphore; + this.waitPosition = waitPosition; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = base.Read(buffer, offset, count); + if (this.Position > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + this.continueSemaphore.Wait(); + } + + return read; + } + + private bool TryPause() => Interlocked.CompareExchange(ref this.pauseDone, 1, 0) == 0; + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = await base.ReadAsync(buffer, offset, count, cancellationToken); + if (this.Position > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + await this.continueSemaphore.WaitAsync(); + } + + return read; + } + + public override int ReadByte() + { + if (this.Position + 1 > this.waitPosition && this.TryPause()) + { + this.notifyWaitPositionReachedSemaphore.Release(); + this.continueSemaphore.Wait(); + } + + int result = base.ReadByte(); + return result; + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs new file mode 100644 index 000000000..87f8cb8c1 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/SemaphoreReadMemoryStreamTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class SemaphoreReadMemoryStreamTests + { + private readonly SemaphoreSlim continueSemaphore = new SemaphoreSlim(0); + private readonly SemaphoreSlim notifyWaitPositionReachedSemaphore = new SemaphoreSlim(0); + private readonly byte[] buffer = new byte[128]; + + [Fact] + public void Read_BeforeWaitLimit_ShouldFinish() + { + using Stream stream = this.CreateTestStream(); + int read = stream.Read(this.buffer); + Assert.Equal(this.buffer.Length, read); + } + + [Fact] + public async Task ReadAsync_BeforeWaitLimit_ShouldFinish() + { + using Stream stream = this.CreateTestStream(); + int read = await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + Assert.Equal(this.buffer.Length, read); + } + + [Fact] + public async Task Read_AfterWaitLimit_ShouldPause() + { + using Stream stream = this.CreateTestStream(); + stream.Read(this.buffer); + Assert.Equal(0, this.notifyWaitPositionReachedSemaphore.CurrentCount); + + Task readTask = Task.Factory.StartNew( + () => + { + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + stream.Read(this.buffer); + }, TaskCreationOptions.LongRunning); + + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + this.continueSemaphore.Release(); + await readTask; + } + + [Fact] + public async Task ReadAsync_AfterWaitLimit_ShouldPause() + { + using Stream stream = this.CreateTestStream(); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + + Task readTask = Task.Factory.StartNew( + async () => + { + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + await stream.ReadAsync(this.buffer, 0, this.buffer.Length); + }, TaskCreationOptions.LongRunning); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + await this.notifyWaitPositionReachedSemaphore.WaitAsync(); + await Task.Delay(5); + Assert.False(readTask.IsCompleted); + this.continueSemaphore.Release(); + await readTask; + } + + private Stream CreateTestStream(int size = 1024, int waitAfterPosition = 256) + { + byte[] buffer = new byte[size]; + return new SemaphoreReadMemoryStream(buffer, waitAfterPosition, this.notifyWaitPositionReachedSemaphore, this.continueSemaphore); + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 978c0555c..129d17f4d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; @@ -371,7 +372,7 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCountsAsync[this.callerName]++; @@ -391,7 +392,8 @@ namespace SixLabors.ImageSharp.Tests public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } private class TestDecoderWithParameters : IImageDecoder @@ -425,7 +427,7 @@ namespace SixLabors.ImageSharp.Tests return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream) + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCountsAsync[this.callerName]++; @@ -445,7 +447,8 @@ namespace SixLabors.ImageSharp.Tests public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - public async Task DecodeAsync(Configuration configuration, Stream stream) => await this.DecodeAsync(configuration, stream); + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken); } } } diff --git a/tests/Images/External b/tests/Images/External index 0d1f91e2f..6a0030806 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 0d1f91e2fe1491f6dc2c137a8ea20460fde4404c +Subproject commit 6a003080674d1fedc66292c13ce5a357b2a33083