Browse Source

Add tests and fix issues.

pull/2844/head
James Jackson-South 1 year ago
parent
commit
502a354262
  1. 47
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  2. 42
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 47
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  4. 4
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  5. 75
      src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
  6. 22
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  7. 68
      src/ImageSharp/Formats/Pbm/PlainEncoder.cs
  8. 1
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  9. 6
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  10. 8
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  11. 8
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  12. 4
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  13. 60
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  14. 58
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  15. 8
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  16. 67
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
  17. 28
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  18. 59
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
  19. 4
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  20. 59
      tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
  21. 152
      tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs
  22. 5
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  23. 12
      tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
  24. 131
      tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs
  25. 13
      tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs
  26. 4
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

47
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -48,13 +48,13 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
@ -80,20 +80,6 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
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 class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
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 class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
@ -156,7 +142,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
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 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
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 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
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
};
}

42
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -207,22 +207,29 @@ internal sealed class GifEncoderCore
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<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
@ -310,9 +317,12 @@ internal sealed class GifEncoderCore
private void EncodeFirstFrame<TPixel>(
Stream stream,
GifFrameMetadata metadata,
IndexedImageFrame<TPixel> quantized)
IndexedImageFrame<TPixel> quantized,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
cancellationToken.ThrowIfCancellationRequested();
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;

47
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -41,13 +41,13 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
@ -73,20 +73,6 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
}
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 class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
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 class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
@ -147,7 +133,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
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 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
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 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
}
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
};
}

4
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -123,13 +123,13 @@ internal abstract class IconEncoderCore
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(),
};

75
src/ImageSharp/Formats/Pbm/BinaryEncoder.cs

@ -17,25 +17,32 @@ internal class BinaryEncoder
/// </summary>
/// <typeparam name="TPixel">The type of input pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="stream">The bytestream to write to.</param>
/// <param name="stream">The byte stream to write to.</param>
/// <param name="image">The input image.</param>
/// <param name="colorType">The ColorType to use.</param>
/// <param name="componentType">Data type of the pixles components.</param>
/// <exception cref="InvalidImageContentException">
/// <param name="componentType">Data type of the pixels components.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="ImageFormatException">
/// Thrown if an invalid combination of setting is requested.
/// </exception>
public static void WritePixels<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image, PbmColorType colorType, PbmComponentType componentType)
public static void WritePixels<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
PbmColorType colorType,
PbmComponentType componentType,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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 @@ internal class BinaryEncoder
{
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<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteGrayscale<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -82,6 +86,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8Bytes(
@ -94,7 +100,11 @@ internal class BinaryEncoder
}
}
private static void WriteWideGrayscale<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteWideGrayscale<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
const int bytesPerPixel = 2;
@ -107,6 +117,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL16Bytes(
@ -119,7 +131,11 @@ internal class BinaryEncoder
}
}
private static void WriteRgb<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteRgb<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
const int bytesPerPixel = 3;
@ -132,6 +148,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
@ -144,7 +162,11 @@ internal class BinaryEncoder
}
}
private static void WriteWideRgb<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteWideRgb<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
const int bytesPerPixel = 6;
@ -157,6 +179,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb48Bytes(
@ -169,7 +193,12 @@ internal class BinaryEncoder
}
}
private static void WriteBlackAndWhite<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteBlackAndWhite<TPixel>(
Configuration
configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -181,6 +210,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8(

22
src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs

@ -68,8 +68,7 @@ internal sealed class PbmEncoderCore
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 @@ internal sealed class PbmEncoderCore
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WritePixels<TPixel>(Stream stream, ImageFrame<TPixel> image)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WritePixels<TPixel>(Stream stream, ImageFrame<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
}

68
src/ImageSharp/Formats/Pbm/PlainEncoder.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
/// <summary>
/// Pixel encoding methods for the PBM plain encoding.
/// </summary>
internal class PlainEncoder
internal static class PlainEncoder
{
private const byte NewLine = 0x0a;
private const byte Space = 0x20;
@ -31,45 +31,56 @@ internal class PlainEncoder
/// </summary>
/// <typeparam name="TPixel">The type of input pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="stream">The bytestream to write to.</param>
/// <param name="stream">The byte stream to write to.</param>
/// <param name="image">The input image.</param>
/// <param name="colorType">The ColorType to use.</param>
/// <param name="componentType">Data type of the pixles components.</param>
public static void WritePixels<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image, PbmColorType colorType, PbmComponentType componentType)
/// <param name="componentType">Data type of the pixels components.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
public static void WritePixels<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
PbmColorType colorType,
PbmComponentType componentType,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteGrayscale<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -83,6 +94,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8(
configuration,
@ -102,7 +115,11 @@ internal class PlainEncoder
}
}
private static void WriteWideGrayscale<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteWideGrayscale<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -116,6 +133,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL16(
configuration,
@ -135,7 +154,11 @@ internal class PlainEncoder
}
}
private static void WriteRgb<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteRgb<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -149,6 +172,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb24(
configuration,
@ -174,7 +199,11 @@ internal class PlainEncoder
}
}
private static void WriteWideRgb<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteWideRgb<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -188,6 +217,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgb48(
configuration,
@ -213,7 +244,11 @@ internal class PlainEncoder
}
}
private static void WriteBlackAndWhite<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
private static void WriteBlackAndWhite<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
@ -227,6 +262,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8(
configuration,
@ -236,8 +273,7 @@ internal class PlainEncoder
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;
}

1
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -226,6 +226,7 @@ internal sealed class PngEncoderCore : IDisposable
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++;

6
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -137,7 +137,7 @@ internal sealed class TiffEncoderCore
long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel>? metadataImage = image;
Image<TPixel>? imageMetadata = image;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
@ -154,8 +154,8 @@ internal sealed class TiffEncoderCore
ImageFrame<TPixel> 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
{

8
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -388,7 +388,13 @@ internal class Vp8Encoder : IDisposable
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
/// <param name="image">The image to encode from.</param>
/// <returns>A <see cref="bool"/> indicating whether the frame contains an alpha channel.</returns>
private bool Encode<TPixel>(Stream stream, ImageFrame<TPixel> frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image<TPixel> image)
private bool Encode<TPixel>(
Stream stream,
ImageFrame<TPixel> frame,
Rectangle bounds,
WebpFrameMetadata frameMetadata,
bool hasAnimation,
Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = bounds.Width;

8
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -8,6 +8,14 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// </summary>
public sealed class WebpEncoder : AnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="WebpEncoder"/> class.
/// </summary>
public WebpEncoder()
// Match the default behavior of the native reference encoder.
=> this.TransparentColorMode = TransparentColorMode.Clear;
/// <summary>
/// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.

4
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -166,6 +166,9 @@ internal sealed class WebpEncoderCore
// Encode the first frame.
ImageFrame<TPixel> 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 @@ internal sealed class WebpEncoderCore
}
else
{
cancellationToken.ThrowIfCancellationRequested();
encoder.EncodeStatic(stream, image);
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}

60
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -397,6 +397,66 @@ public class BmpEncoderTests
reencodedImage.CompareToOriginal(provider);
}
[Fact]
public void Encode_WithTransparentColorBehaviorClear_Works()
{
// arrange
using Image<Rgba32> image = new(50, 50);
BmpEncoder encoder = new()
{
BitsPerPixel = BmpBitsPerPixel.Bit32,
SupportTransparency = true,
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> 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<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel,

58
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -361,4 +361,62 @@ public class GifEncoderTests
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<Rgba32> image = new(50, 50);
GifEncoder encoder = new()
{
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
if (y > 25)
{
expectedColor = transparent;
}
for (int x = 0; x < accessor.Width; x++)
{
Assert.Equal(expectedColor, rowSpan[x]);
}
}
});
}
}

8
tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs

@ -20,8 +20,8 @@ public class CurDecoderTests
using Image<Rgba32> 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 class CurDecoderTests
{
using Image<Rgba32> 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);
}

67
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 class CurEncoderTests
Assert.Equal(icoFrame.EncodingHeight, curFrame.EncodingHeight);
}
}
[Fact]
public void Encode_WithTransparentColorBehaviorClear_Works()
{
// arrange
using Image<Rgba32> image = new(50, 50);
CurEncoder encoder = new()
{
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
Span<Rgba32> 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]);
}
}
});
}
}

28
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

@ -53,8 +53,8 @@ public class IcoDecoderTests
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 class IcoDecoderTests
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 class IcoDecoderTests
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 class IcoDecoderTests
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 class IcoDecoderTests
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 class IcoDecoderTests
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 class IcoDecoderTests
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);
}

59
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 class IcoEncoderTests
Assert.Equal(curFrame.ColorTable, icoFrame.ColorTable);
}
}
[Fact]
public void Encode_WithTransparentColorBehaviorClear_Works()
{
// arrange
using Image<Rgba32> image = new(50, 50);
IcoEncoder encoder = new()
{
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
if (y > 25)
{
expectedColor = transparent;
}
for (int x = 0; x < accessor.Width; x++)
{
Assert.Equal(expectedColor, rowSpan[x]);
}
}
});
}
}

4
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -340,10 +340,10 @@ public partial class PngEncoderTests
[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<Rgba32> image = new(50, 50);
using Image<Rgba32> image = new(50, 50);
PngEncoder encoder = new()
{
TransparentColorMode = TransparentColorMode.Clear,

59
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 class QoiEncoderTests
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
[Fact]
public void Encode_WithTransparentColorBehaviorClear_Works()
{
// arrange
using Image<Rgba32> image = new(50, 50);
QoiEncoder encoder = new()
{
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
if (y > 25)
{
expectedColor = transparent;
}
for (int x = 0; x < accessor.Width; x++)
{
Assert.Equal(expectedColor, rowSpan[x]);
}
}
});
}
}

152
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 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tga;
namespace SixLabors.ImageSharp.Tests.Formats.Tga;
[Trait("Format", "Tga")]
[ValidateDisposedMemoryAllocations]
public class TgaEncoderTests
{
public static readonly TheoryData<TgaBitsPerPixel> 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<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
TgaMetadata meta = output.Metadata.GetTgaMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
}
}
}
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, options);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(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<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
TgaMetadata meta = output.Metadata.GetTgaMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
}
}
}
TgaEncoder options = new() { Compression = TgaCompression.RunLength };
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, options);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
TgaMetadata meta = output.Metadata.GetTgaMetadata();
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
}
[Theory]
@ -136,17 +130,13 @@ public class TgaEncoderTests
[Fact]
public void TgaEncoder_RunLengthDoesNotCrossRowBoundaries()
{
var options = new TgaEncoder() { Compression = TgaCompression.RunLength };
TgaEncoder options = new() { Compression = TgaCompression.RunLength };
using (var input = new Image<Rgba32>(30, 30))
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
byte[] imageBytes = memStream.ToArray();
Assert.Equal(138, imageBytes.Length);
}
}
using Image<Rgba32> 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 class TgaEncoderTests
TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength);
}
[Fact]
public void Encode_WithTransparentColorBehaviorClear_Works()
{
// arrange
using Image<Rgba32> image = new(50, 50);
TgaEncoder encoder = new()
{
BitsPerPixel = TgaBitsPerPixel.Bit32,
TransparentColorMode = TransparentColorMode.Clear,
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < image.Height; y++)
{
Span<Rgba32> 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<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue.ToPixel<Rgba32>();
actual.ProcessPixelRows(accessor =>
{
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> 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<TPixel>(
TestImageProvider<TPixel> provider,
TgaBitsPerPixel bitsPerPixel,
@ -167,20 +216,15 @@ public class TgaEncoderTests
float compareTolerance = 0.01f)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var encoder = new TgaEncoder { BitsPerPixel = bitsPerPixel, Compression = compression };
using Image<TPixel> 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<TPixel>)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<TPixel> encodedImage = (Image<TPixel>)Image.Load(memStream);
ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage, useExactComparer, compareTolerance);
}
}

5
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -256,7 +256,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
TiffEncoder tiffEncoder = new();
using MemoryStream memStream = new();
using Image<Rgba32> image = new(1, 1);
byte[] expectedIfdOffsetBytes = { 12, 0 };
byte[] expectedIfdOffsetBytes = [12, 0];
// act
image.Save(memStream, tiffEncoder);
@ -613,8 +613,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200);
using Image<TPixel> image = provider.GetImage();
TiffEncoder encoder = new()
{ PhotometricInterpretation = photometricInterpretation };
TiffEncoder encoder = new() { PhotometricInterpretation = photometricInterpretation };
image.DebugSave(provider, encoder);
}
}

12
tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs

@ -12,8 +12,8 @@ public partial class ImageTests
{
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 partial class ImageTests
//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 partial class ImageTests
[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<OperationCanceledException>(async () => await Image.IdentifyAsync(fs, preCancelled));
}
private static TheoryData<bool, string, double> CreateLoadData()
{
double[] percentages = new[] { 0, 0.3, 0.7 };
double[] percentages = [0, 0.3, 0.7];
TheoryData<bool, string, double> data = new();
TheoryData<bool, string, double> data = [];
foreach (string file in TestFileForEachCodec)
{

131
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<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsBmpAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Cur()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsCurAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Gif()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Animated_Gif()
{
using Image<Rgba32> image = new(10, 10);
image.Frames.CreateFrame();
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsGifAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Ico()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsIcoAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Jpeg()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsJpegAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Pbm()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsPbmAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Png()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Animated_Png()
{
using Image<Rgba32> image = new(10, 10);
image.Frames.CreateFrame();
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsPngAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Qoi()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsQoiAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Tga()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsTgaAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Tiff()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsTiffAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Webp()
{
using Image<Rgba32> image = new(10, 10);
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true)));
}
[Fact]
public async Task Encode_PreCancellation_Animated_Webp()
{
using Image<Rgba32> image = new(10, 10);
image.Frames.CreateFrame();
await Assert.ThrowsAsync<TaskCanceledException>(
async () => await image.SaveAsWebpAsync(Stream.Null, new CancellationToken(canceled: true)));
}
}
}

13
tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs

@ -6,9 +6,10 @@ using System.Buffers;
namespace SixLabors.ImageSharp.Tests.TestUtilities;
/// <summary>
/// <see cref="PausedMemoryStream"/> is a variant of <see cref="PausedStream"/> that derives from <see cref="MemoryStream"/> instead of encapsulating it.
/// It is used to test decoder REacellation without relying on of our standard prefetching of arbitrary streams to <see cref="ImageSharp.IO.ChunkedMemoryStream"/>
/// on asynchronous path.
/// <see cref="PausedMemoryStream"/> is a variant of <see cref="PausedStream"/> that derives from
/// <see cref="MemoryStream"/> instead of encapsulating it.
/// It is used to test decoder cancellation without relying on of our standard prefetching of arbitrary streams
/// to <see cref="ImageSharp.IO.ChunkedMemoryStream"/> on asynchronous path.
/// </summary>
public class PausedMemoryStream : MemoryStream, IPausedStream
{
@ -108,11 +109,11 @@ public class PausedMemoryStream : MemoryStream, IPausedStream
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 class PausedMemoryStream : MemoryStream, IPausedStream
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)
{

4
tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

@ -115,7 +115,7 @@ public class PausedStream : Stream, IPausedStream
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 class PausedStream : Stream, IPausedStream
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)
{

Loading…
Cancel
Save