diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 4e9a432b16..01b7fbce08 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -48,13 +48,13 @@ private CurFrameMetadata(CurFrameMetadata other) /// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. /// - public byte EncodingWidth { get; set; } + public byte? EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingHeight { get; set; } + public byte? EncodingHeight { get; set; } /// /// Gets or sets the number of bits per pixel.
@@ -80,20 +80,6 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; } - byte encodingWidth = metadata.EncodingWidth switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingWidth, - _ => 0 - }; - - byte encodingHeight = metadata.EncodingHeight switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingHeight, - _ => 0 - }; - int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; BmpBitsPerPixel bbpp = bpp switch { @@ -116,8 +102,8 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin { BmpBitsPerPixel = bbpp, Compression = compression, - EncodingWidth = encodingWidth, - EncodingHeight = encodingHeight, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null }; } @@ -138,8 +124,8 @@ public void AfterFrameApply(ImageFrame source, ImageFrame @@ -156,7 +142,7 @@ internal void FromIconDirEntry(IconDirEntry entry) this.HotspotY = entry.BitCount; } - internal IconDirEntry ToIconDirEntry() + internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 @@ -164,8 +150,8 @@ internal IconDirEntry ToIconDirEntry() return new() { - Width = this.EncodingWidth, - Height = this.EncodingHeight, + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Planes = this.HotspotX, BitCount = this.HotspotY, ColorCount = colorCount @@ -233,13 +219,22 @@ private PixelTypeInfo GetPixelTypeInfo() }; } - private static byte Scale(byte? value, int destination, float ratio) + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) { - return (byte)Math.Clamp(destination, 0, 255); + return ClampEncodingDimension(destination); } - return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 3c6e269e43..797e825dc4 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -207,22 +207,29 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile); } - this.EncodeFirstFrame(stream, frameMetadata, quantized); - - // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. - TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); - - this.EncodeAdditionalFrames( - stream, - image, - globalPalette, - derivedTransparencyIndex, - frameMetadata.DisposalMode, - cancellationToken); - - stream.WriteByte(GifConstants.EndIntroducer); + // If the token is cancelled during encoding of frames we must ensure the + // quantized frame is disposed. + try + { + this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken); + + // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. + TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); + + this.EncodeAdditionalFrames( + stream, + image, + globalPalette, + derivedTransparencyIndex, + frameMetadata.DisposalMode, + cancellationToken); + } + finally + { + stream.WriteByte(GifConstants.EndIntroducer); - quantized?.Dispose(); + quantized?.Dispose(); + } } private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex) @@ -310,9 +317,12 @@ private void EncodeAdditionalFrames( private void EncodeFirstFrame( Stream stream, GifFrameMetadata metadata, - IndexedImageFrame quantized) + IndexedImageFrame quantized, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + cancellationToken.ThrowIfCancellationRequested(); + this.WriteGraphicalControlExtension(metadata, stream); Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index a2d1c01391..62aa705cbe 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -41,13 +41,13 @@ private IcoFrameMetadata(IcoFrameMetadata other) /// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingWidth { get; set; } + public byte? EncodingWidth { get; set; } /// /// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. ///
- public byte EncodingHeight { get; set; } + public byte? EncodingHeight { get; set; } /// /// Gets or sets the number of bits per pixel.
@@ -73,20 +73,6 @@ public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; } - byte encodingWidth = metadata.EncodingWidth switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingWidth, - _ => 0 - }; - - byte encodingHeight = metadata.EncodingHeight switch - { - > 255 => 0, - <= 255 and >= 1 => (byte)metadata.EncodingHeight, - _ => 0 - }; - int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; BmpBitsPerPixel bbpp = bpp switch { @@ -109,8 +95,8 @@ public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin { BmpBitsPerPixel = bbpp, Compression = compression, - EncodingWidth = encodingWidth, - EncodingHeight = encodingHeight, + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null }; } @@ -131,8 +117,8 @@ public void AfterFrameApply(ImageFrame source, ImageFrame @@ -147,7 +133,7 @@ internal void FromIconDirEntry(IconDirEntry entry) this.EncodingHeight = entry.Height; } - internal IconDirEntry ToIconDirEntry() + internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 @@ -155,8 +141,8 @@ internal IconDirEntry ToIconDirEntry() return new() { - Width = this.EncodingWidth, - Height = this.EncodingHeight, + Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), + Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Planes = 1, ColorCount = colorCount, BitCount = this.Compression switch @@ -228,13 +214,22 @@ private PixelTypeInfo GetPixelTypeInfo() }; } - private static byte Scale(byte? value, int destination, float ratio) + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) { - return (byte)Math.Clamp(destination, 0, 255); + return ClampEncodingDimension(destination); } - return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255)); + return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); } + + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + // Encoding dimensions can be between 0-256 where 0 means 256 or greater. + > 255 => 0, + <= 255 and >= 1 => (byte)dimension, + _ => 0 + }; } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 80c3ec4c31..03e01f912f 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -123,13 +123,13 @@ private void InitHeader(Image image) image.Frames.Select(i => { IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); }).ToArray(), IconFileType.CUR => image.Frames.Select(i => { CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry()); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); }).ToArray(), _ => throw new NotSupportedException(), }; diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs index dddc629b3e..8b379e4d76 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs @@ -17,25 +17,32 @@ internal class BinaryEncoder ///
/// The type of input pixel. /// The configuration. - /// The bytestream to write to. + /// The byte stream to write to. /// The input image. /// The ColorType to use. - /// Data type of the pixles components. - /// + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + /// /// Thrown if an invalid combination of setting is requested. /// - public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType) + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - WriteGrayscale(configuration, stream, image); + WriteGrayscale(configuration, stream, image, cancellationToken); } else if (componentType == PbmComponentType.Short) { - WriteWideGrayscale(configuration, stream, image); + WriteWideGrayscale(configuration, stream, image, cancellationToken); } else { @@ -46,31 +53,28 @@ public static void WritePixels(Configuration configuration, Stream strea { if (componentType == PbmComponentType.Byte) { - WriteRgb(configuration, stream, image); + WriteRgb(configuration, stream, image, cancellationToken); } else if (componentType == PbmComponentType.Short) { - WriteWideRgb(configuration, stream, image); + WriteWideRgb(configuration, stream, image, cancellationToken); } else { throw new ImageFormatException("Component type not supported for Color PBM."); } } - else + else if (componentType == PbmComponentType.Bit) { - if (componentType == PbmComponentType.Bit) - { - WriteBlackAndWhite(configuration, stream, image); - } - else - { - throw new ImageFormatException("Component type not supported for Black & White PBM."); - } + WriteBlackAndWhite(configuration, stream, image, cancellationToken); } } - private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -82,6 +86,8 @@ private static void WriteGrayscale(Configuration configuration, Stream s for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8Bytes( @@ -94,7 +100,11 @@ private static void WriteGrayscale(Configuration configuration, Stream s } } - private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 2; @@ -107,6 +117,8 @@ private static void WriteWideGrayscale(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL16Bytes( @@ -119,7 +131,11 @@ private static void WriteWideGrayscale(Configuration configuration, Stre } } - private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 3; @@ -132,6 +148,8 @@ private static void WriteRgb(Configuration configuration, Stream stream, for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb24Bytes( @@ -144,7 +162,11 @@ private static void WriteRgb(Configuration configuration, Stream stream, } } - private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 6; @@ -157,6 +179,8 @@ private static void WriteWideRgb(Configuration configuration, Stream str for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb48Bytes( @@ -169,7 +193,12 @@ private static void WriteWideRgb(Configuration configuration, Stream str } } - private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteBlackAndWhite( + Configuration + configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -181,6 +210,8 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index 843f1880e6..e0330ca6b4 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -68,8 +68,7 @@ public void Encode(Image image, Stream stream, CancellationToken byte signature = this.DeduceSignature(); this.WriteHeader(stream, signature, image.Size); - - this.WritePixels(stream, image.Frames.RootFrame); + this.WritePixels(stream, image.Frames.RootFrame, cancellationToken); stream.Flush(); } @@ -167,16 +166,29 @@ private void WriteHeader(Stream stream, byte signature, Size pixelSize) /// /// The containing pixel data. /// - private void WritePixels(Stream stream, ImageFrame image) + /// The token to monitor for cancellation requests. + private void WritePixels(Stream stream, ImageFrame image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (this.encoding == PbmEncoding.Plain) { - PlainEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType); + PlainEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); } else { - BinaryEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType); + BinaryEncoder.WritePixels( + this.configuration, + stream, + image, + this.colorType, + this.componentType, + cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs index 29260f54aa..bab508720d 100644 --- a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm; /// /// Pixel encoding methods for the PBM plain encoding. /// -internal class PlainEncoder +internal static class PlainEncoder { private const byte NewLine = 0x0a; private const byte Space = 0x20; @@ -31,45 +31,56 @@ internal class PlainEncoder /// /// The type of input pixel. /// The configuration. - /// The bytestream to write to. + /// The byte stream to write to. /// The input image. /// The ColorType to use. - /// Data type of the pixles components. - public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType) + /// Data type of the pixels components. + /// The token to monitor for cancellation requests. + public static void WritePixels( + Configuration configuration, + Stream stream, + ImageFrame image, + PbmColorType colorType, + PbmComponentType componentType, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - WriteGrayscale(configuration, stream, image); + WriteGrayscale(configuration, stream, image, cancellationToken); } else { - WriteWideGrayscale(configuration, stream, image); + WriteWideGrayscale(configuration, stream, image, cancellationToken); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - WriteRgb(configuration, stream, image); + WriteRgb(configuration, stream, image, cancellationToken); } else { - WriteWideRgb(configuration, stream, image); + WriteWideRgb(configuration, stream, image, cancellationToken); } } else { - WriteBlackAndWhite(configuration, stream, image); + WriteBlackAndWhite(configuration, stream, image, cancellationToken); } // Write EOF indicator, as some encoders expect it. stream.WriteByte(Space); } - private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -83,6 +94,8 @@ private static void WriteGrayscale(Configuration configuration, Stream s for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( configuration, @@ -102,7 +115,11 @@ private static void WriteGrayscale(Configuration configuration, Stream s } } - private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideGrayscale( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -116,6 +133,8 @@ private static void WriteWideGrayscale(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL16( configuration, @@ -135,7 +154,11 @@ private static void WriteWideGrayscale(Configuration configuration, Stre } } - private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -149,6 +172,8 @@ private static void WriteRgb(Configuration configuration, Stream stream, for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb24( configuration, @@ -174,7 +199,11 @@ private static void WriteRgb(Configuration configuration, Stream stream, } } - private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteWideRgb( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -188,6 +217,8 @@ private static void WriteWideRgb(Configuration configuration, Stream str for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgb48( configuration, @@ -213,7 +244,11 @@ private static void WriteWideRgb(Configuration configuration, Stream str } } - private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image) + private static void WriteBlackAndWhite( + Configuration configuration, + Stream stream, + ImageFrame image, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int width = image.Width; @@ -227,6 +262,8 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre for (int y = 0; y < height; y++) { + cancellationToken.ThrowIfCancellationRequested(); + Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8( configuration, @@ -236,8 +273,7 @@ private static void WriteBlackAndWhite(Configuration configuration, Stre int written = 0; for (int x = 0; x < width; x++) { - byte value = (rowSpan[x].PackedValue < 128) ? One : Zero; - plainSpan[written++] = value; + plainSpan[written++] = (rowSpan[x].PackedValue < 128) ? One : Zero; plainSpan[written++] = Space; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 05220e8019..ea36d9fe1e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -226,6 +226,7 @@ public void Encode(Image image, Stream stream, CancellationToken bool userAnimateRootFrame = this.animateRootFrame == true; if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1) { + cancellationToken.ThrowIfCancellationRequested(); FrameControl frameControl = new((uint)this.width, (uint)this.height); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); currentFrameIndex++; diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 4f6985f9db..da55ef9f9b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -137,7 +137,7 @@ public void Encode(Image image, Stream stream, CancellationToken long ifdMarker = WriteHeader(writer, buffer); - Image? metadataImage = image; + Image? imageMetadata = image; foreach (ImageFrame frame in image.Frames) { @@ -154,8 +154,8 @@ public void Encode(Image image, Stream stream, CancellationToken ImageFrame encodingFrame = clonedFrame ?? frame; - ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); - metadataImage = null; + ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); + imageMetadata = null; } finally { diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index d22d357fe3..e4ebe14731 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -388,7 +388,13 @@ public void EncodeStatic(Stream stream, Image image) /// Flag indicating, if an animation parameter is present. /// The image to encode from. /// A indicating whether the frame contains an alpha channel. - private bool Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image) + private bool Encode( + Stream stream, + ImageFrame frame, + Rectangle bounds, + WebpFrameMetadata frameMetadata, + bool hasAnimation, + Image image) where TPixel : unmanaged, IPixel { int width = bounds.Width; diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 622fe0181e..2e459ff58e 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// public sealed class WebpEncoder : AnimatedImageEncoder { + /// + /// Initializes a new instance of the class. + /// + public WebpEncoder() + + // Match the default behavior of the native reference encoder. + => this.TransparentColorMode = TransparentColorMode.Clear; + /// /// Gets the webp file format used. Either lossless or lossy. /// Defaults to lossy. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index b8f27a8326..b3270786d7 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -166,6 +166,9 @@ public void Encode(Image image, Stream stream, CancellationToken // Encode the first frame. ImageFrame previousFrame = image.Frames.RootFrame; WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); + + cancellationToken.ThrowIfCancellationRequested(); + hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation); if (hasAnimation) @@ -304,6 +307,7 @@ public void Encode(Image image, Stream stream, CancellationToken } else { + cancellationToken.ThrowIfCancellationRequested(); encoder.EncodeStatic(stream, image); encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index d68ec47557..09ef49a61e 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -397,6 +397,66 @@ public void Encode_Issue2467(TestImageProvider provider, BmpBits reencodedImage.CompareToOriginal(provider); } + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + BmpEncoder encoder = new() + { + BitsPerPixel = BmpBitsPerPixel.Bit32, + SupportTransparency = true, + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } + private static void TestBmpEncoderCore( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index c08db84eb6..f12f66186e 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -361,4 +361,62 @@ public void Encode_Animated_VisualTest(TestImageProvider provide provider.Utility.SaveTestOutputFile(image, "png", new PngEncoder(), "animated"); provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder(), "animated"); } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + GifEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index f7ee7614af..bac52fc728 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -20,8 +20,8 @@ public void CurDecoder_Decode(TestImageProvider provider) using Image image = provider.GetImage(CurDecoder.Instance); CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); - Assert.Equal(image.Width, meta.EncodingWidth); - Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(image.Width, meta.EncodingWidth.Value); + Assert.Equal(image.Height, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -33,8 +33,8 @@ public void CurDecoder_Decode2(TestImageProvider provider) { using Image image = provider.GetImage(CurDecoder.Instance); CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata(); - Assert.Equal(image.Width, meta.EncodingWidth); - Assert.Equal(image.Height, meta.EncodingHeight); + Assert.Equal(image.Width, meta.EncodingWidth.Value); + Assert.Equal(image.Height, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index 59c40c9245..bf94e1d489 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; @@ -63,4 +64,70 @@ public void CanConvertFromIco(TestImageProvider provider) Assert.Equal(icoFrame.EncodingHeight, curFrame.EncodingHeight); } } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + CurEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + Span rowSpanOpp = accessor.GetRowSpan(accessor.Height - y - 1); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + if (expectedColor != rowSpan[x]) + { + var xx = 0; + } + + + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index bc46df0955..e076ccab60 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -53,8 +53,8 @@ public void Bpp1Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit1, meta.BmpBitsPerPixel); } @@ -89,8 +89,8 @@ public void Bpp24Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit24, meta.BmpBitsPerPixel); } @@ -125,8 +125,8 @@ public void Bpp32Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -160,8 +160,8 @@ public void Bpp4Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit4, meta.BmpBitsPerPixel); } @@ -196,8 +196,8 @@ public void Bpp8Test(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit8, meta.BmpBitsPerPixel); } @@ -226,8 +226,8 @@ public void InvalidPngTest(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Png, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } @@ -324,8 +324,8 @@ public void IcoFakeTest(TestImageProvider provider) int expectedWidth = image.Width >= 256 ? 0 : image.Width; int expectedHeight = image.Height >= 256 ? 0 : image.Height; - Assert.Equal(expectedWidth, meta.EncodingWidth); - Assert.Equal(expectedHeight, meta.EncodingHeight); + Assert.Equal(expectedWidth, meta.EncodingWidth.Value); + Assert.Equal(expectedHeight, meta.EncodingHeight.Value); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index 751db384d7..4c7438d568 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; @@ -62,4 +63,62 @@ public void CanConvertFromCur(TestImageProvider provider) Assert.Equal(curFrame.ColorTable, icoFrame.ColorTable); } } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + IcoEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b6bb243afa..b4995d77b6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -340,10 +340,10 @@ public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth) [InlineData(PngColorType.Palette)] [InlineData(PngColorType.RgbWithAlpha)] [InlineData(PngColorType.GrayscaleWithAlpha)] - public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType) + public void Encode_WithTransparentColorBehaviorClear_Works(PngColorType colorType) { // arrange - Image image = new(50, 50); + using Image image = new(50, 50); PngEncoder encoder = new() { TransparentColorMode = TransparentColorMode.Clear, diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs index 32ade4a1e9..9da9ad3275 100644 --- a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -41,4 +42,62 @@ public static void Encode(TestImageProvider provider, QoiChannel Assert.Equal(qoiMetadata.Channels, channels); Assert.Equal(qoiMetadata.ColorSpace, colorSpace); } + + [Fact] + public void Encode_WithTransparentColorBehaviorClear_Works() + { + // arrange + using Image image = new(50, 50); + QoiEncoder encoder = new() + { + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 615e0fc921..adf0b4353d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -10,6 +11,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga; [Trait("Format", "Tga")] +[ValidateDisposedMemoryAllocations] public class TgaEncoderTests { public static readonly TheoryData BitsPerPixel = @@ -32,43 +34,35 @@ public class TgaEncoderTests [MemberData(nameof(TgaBitsPerPixelFiles))] public void TgaEncoder_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) { - var options = new TgaEncoder(); + TgaEncoder options = new(); - var testFile = TestFile.Create(imagePath); - using (Image input = testFile.CreateRgba32Image()) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - TgaMetadata meta = output.Metadata.GetTgaMetadata(); - Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); - } - } - } + TestFile testFile = TestFile.Create(imagePath); + using Image input = testFile.CreateRgba32Image(); + using MemoryStream memStream = new(); + + input.Save(memStream, options); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + TgaMetadata meta = output.Metadata.GetTgaMetadata(); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } [Theory] [MemberData(nameof(TgaBitsPerPixelFiles))] public void TgaEncoder_WithCompression_PreserveBitsPerPixel(string imagePath, TgaBitsPerPixel bmpBitsPerPixel) { - var options = new TgaEncoder() { Compression = TgaCompression.RunLength }; - var testFile = TestFile.Create(imagePath); - using (Image input = testFile.CreateRgba32Image()) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - TgaMetadata meta = output.Metadata.GetTgaMetadata(); - Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); - } - } - } + TgaEncoder options = new() { Compression = TgaCompression.RunLength }; + TestFile testFile = TestFile.Create(imagePath); + using Image input = testFile.CreateRgba32Image(); + using MemoryStream memStream = new(); + + input.Save(memStream, options); + memStream.Position = 0; + + using Image output = Image.Load(memStream); + TgaMetadata meta = output.Metadata.GetTgaMetadata(); + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } [Theory] @@ -136,17 +130,13 @@ public void TgaEncoder_DoesNotAlwaysUseRunLengthPackets(TestImageProvide [Fact] public void TgaEncoder_RunLengthDoesNotCrossRowBoundaries() { - var options = new TgaEncoder() { Compression = TgaCompression.RunLength }; + TgaEncoder options = new() { Compression = TgaCompression.RunLength }; - using (var input = new Image(30, 30)) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, options); - byte[] imageBytes = memStream.ToArray(); - Assert.Equal(138, imageBytes.Length); - } - } + using Image input = new(30, 30); + using MemoryStream memStream = new(); + input.Save(memStream, options); + byte[] imageBytes = memStream.ToArray(); + Assert.Equal(138, imageBytes.Length); } [Theory] @@ -159,6 +149,65 @@ public void TgaEncoder_WorksWithDiscontiguousBuffers(TestImageProvider image = new(50, 50); + TgaEncoder encoder = new() + { + BitsPerPixel = TgaBitsPerPixel.Bit32, + TransparentColorMode = TransparentColorMode.Clear, + }; + Rgba32 rgba32 = Color.Blue.ToPixel(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x] = Rgba32.FromRgba32(rgba32); + } + } + }); + + // act + using MemoryStream memStream = new(); + image.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using Image actual = Image.Load(memStream); + Rgba32 expectedColor = Color.Blue.ToPixel(); + + actual.ProcessPixelRows(accessor => + { + Rgba32 transparent = Color.Transparent.ToPixel(); + for (int y = 0; y < accessor.Height; y++) + { + Span rowSpan = accessor.GetRowSpan(y); + + if (y > 25) + { + expectedColor = transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } + } + }); + } + private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, @@ -167,20 +216,15 @@ private static void TestTgaEncoderCore( float compareTolerance = 0.01f) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression }; + using Image image = provider.GetImage(); + TgaEncoder encoder = new() { BitsPerPixel = bitsPerPixel, Compression = compression }; - using (var memStream = new MemoryStream()) - { - image.DebugSave(provider, encoder); - image.Save(memStream, encoder); - memStream.Position = 0; - using (var encodedImage = (Image)Image.Load(memStream)) - { - ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); - } - } - } + using MemoryStream memStream = new(); + image.DebugSave(provider, encoder); + image.Save(memStream, encoder); + memStream.Position = 0; + + using Image encodedImage = (Image)Image.Load(memStream); + ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 0d59625ca7..674ca5c5bb 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -256,7 +256,7 @@ public void TiffEncoder_WritesIfdOffsetAtWordBoundary() TiffEncoder tiffEncoder = new(); using MemoryStream memStream = new(); using Image image = new(1, 1); - byte[] expectedIfdOffsetBytes = { 12, 0 }; + byte[] expectedIfdOffsetBytes = [12, 0]; // act image.Save(memStream, tiffEncoder); @@ -613,8 +613,7 @@ public void TiffEncode_WorksWithDiscontiguousBuffers(TestImageProvider image = provider.GetImage(); - TiffEncoder encoder = new() - { PhotometricInterpretation = photometricInterpretation }; + TiffEncoder encoder = new() { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs index d8d9f4fe2a..0656f9e0bc 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -12,8 +12,8 @@ public class Decode_Cancellation : ImageLoadTestBase { public Decode_Cancellation() => this.TopLevelConfiguration.StreamProcessingBufferSize = 128; - public static readonly string[] TestFileForEachCodec = new[] - { + public static readonly string[] TestFileForEachCodec = + [ TestImages.Jpeg.Baseline.Snake, // TODO: Figure out Unix cancellation failures, and validate cancellation for each decoder. @@ -24,7 +24,7 @@ public class Decode_Cancellation : ImageLoadTestBase //TestImages.Tga.Bit32BottomRight, //TestImages.Webp.Lossless.WithExif, //TestImages.Pbm.GrayscaleBinaryWide - }; + ]; public static object[][] IdentifyData { get; } = TestFileForEachCodec.Select(f => new object[] { f }).ToArray(); @@ -32,16 +32,16 @@ public class Decode_Cancellation : ImageLoadTestBase [MemberData(nameof(IdentifyData))] public async Task IdentifyAsync_PreCancelled(string file) { - using FileStream fs = File.OpenRead(TestFile.GetInputFileFullPath(file)); + await using FileStream fs = File.OpenRead(TestFile.GetInputFileFullPath(file)); CancellationToken preCancelled = new(canceled: true); await Assert.ThrowsAnyAsync(async () => await Image.IdentifyAsync(fs, preCancelled)); } private static TheoryData CreateLoadData() { - double[] percentages = new[] { 0, 0.3, 0.7 }; + double[] percentages = [0, 0.3, 0.7]; - TheoryData data = new(); + TheoryData data = []; foreach (string file in TestFileForEachCodec) { diff --git a/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs new file mode 100644 index 0000000000..f3b1a01c94 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests; + +public partial class ImageTests +{ + [ValidateDisposedMemoryAllocations] + public class Encode_Cancellation + { + [Fact] + public async Task Encode_PreCancellation_Bmp() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsBmpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Cur() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsCurAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Gif() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Gif() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Ico() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsIcoAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Jpeg() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsJpegAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Pbm() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsPbmAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Png() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Png() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Qoi() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsQoiAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Tga() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsTgaAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Tiff() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsTiffAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Webp() + { + using Image image = new(10, 10); + await Assert.ThrowsAsync( + async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + + [Fact] + public async Task Encode_PreCancellation_Animated_Webp() + { + using Image image = new(10, 10); + image.Frames.CreateFrame(); + + await Assert.ThrowsAsync( + async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true))); + } + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs index ae4af24f14..d1149dd004 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs @@ -6,9 +6,10 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities; /// -/// is a variant of that derives from instead of encapsulating it. -/// It is used to test decoder REacellation without relying on of our standard prefetching of arbitrary streams to -/// on asynchronous path. +/// is a variant of that derives from +/// instead of encapsulating it. +/// It is used to test decoder cancellation without relying on of our standard prefetching of arbitrary streams +/// to on asynchronous path. /// public class PausedMemoryStream : MemoryStream, IPausedStream { @@ -108,11 +109,11 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override bool CanWrite => base.CanWrite; - public override void Flush() => this.Await(() => base.Flush()); + public override void Flush() => this.Await(base.Flush); public override int Read(byte[] buffer, int offset, int count) => this.Await(() => base.Read(buffer, offset, count)); - public override long Seek(long offset, SeekOrigin origin) => this.Await(() => base.Seek(offset, origin)); + public override long Seek(long offset, SeekOrigin loc) => this.Await(() => base.Seek(offset, loc)); public override void SetLength(long value) => this.Await(() => base.SetLength(value)); @@ -124,7 +125,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override void WriteByte(byte value) => this.Await(() => base.WriteByte(value)); - public override int ReadByte() => this.Await(() => base.ReadByte()); + public override int ReadByte() => this.Await(base.ReadByte); public override void CopyTo(Stream destination, int bufferSize) { diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs index 3c780f3474..42ed6b0d5e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs +++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs @@ -115,7 +115,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override long Position { get => this.innerStream.Position; set => this.innerStream.Position = value; } - public override void Flush() => this.Await(() => this.innerStream.Flush()); + public override void Flush() => this.Await(this.innerStream.Flush); public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count)); @@ -131,7 +131,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value)); - public override int ReadByte() => this.Await(() => this.innerStream.ReadByte()); + public override int ReadByte() => this.Await(this.innerStream.ReadByte); protected override void Dispose(bool disposing) {