Browse Source

Merge pull request #2844 from SixLabors/js/normalize-transparency-encoding

Normalize Encoder Transparency Handling For Alpha Aware Image Formats
pull/2851/head
James Jackson-South 1 year ago
committed by GitHub
parent
commit
4be26452b9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 15
      src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
  2. 227
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  3. 47
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  4. 97
      src/ImageSharp/Formats/EncodingUtilities.cs
  5. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  6. 153
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  7. 2
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  8. 2
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  9. 47
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  10. 8
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  11. 75
      src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
  12. 22
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  13. 68
      src/ImageSharp/Formats/Pbm/PlainEncoder.cs
  14. 6
      src/ImageSharp/Formats/Png/PngEncoder.cs
  15. 169
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  16. 21
      src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
  17. 2
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  18. 219
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  19. 2
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  20. 32
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  21. 50
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  22. 22
      src/ImageSharp/Formats/TransparentColorMode.cs
  23. 2
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  24. 12
      src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
  25. 4
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  26. 8
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  27. 2
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  28. 15
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  29. 20
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  30. 20
      src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs
  31. 2
      src/ImageSharp/ImageFrame.cs
  32. 2
      src/ImageSharp/ImageFrame{TPixel}.cs
  33. 62
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  34. 2
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  35. 7
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  36. 12
      src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
  37. 5
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  38. 19
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  39. 10
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  40. 22
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  41. 2
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  42. 2
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  43. 2
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  44. 2
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  45. 2
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
  46. 2
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  47. 8
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs
  48. 4
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  49. 2
      src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs
  50. 8
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  51. 4
      src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs
  52. 29
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
  53. 10
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  54. 4
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs
  55. 4
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  56. 4
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  57. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  58. 71
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  59. 10
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  60. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  61. 10
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  62. 16
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  63. 3
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  64. 60
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  65. 58
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  66. 8
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  67. 67
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
  68. 28
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  69. 59
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
  70. 6
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  71. 59
      tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
  72. 152
      tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs
  73. 5
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  74. 2
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  75. 12
      tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs
  76. 131
      tests/ImageSharp.Tests/Image/ImageTests.EncodeCancellation.cs
  77. 1
      tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs
  78. 84
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  79. 116
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  80. 13
      tests/ImageSharp.Tests/TestUtilities/PausedMemoryStream.cs
  81. 4
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

15
src/ImageSharp/Formats/AlphaAwareImageEncoder.cs

@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
/// </summary>
public abstract class AlphaAwareImageEncoder : ImageEncoder
{
/// <summary>
/// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
/// </summary>
public TransparentColorMode TransparentColorMode { get; init; }
}

227
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -91,6 +91,11 @@ internal sealed class BmpEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The transparent color mode.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask;
@ -113,6 +118,7 @@ internal sealed class BmpEncoderCore
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader;
@ -181,14 +187,14 @@ internal sealed class BmpEncoderCore
Span<byte> buffer = stackalloc byte[infoHeaderSize];
// for ico/cur encoder.
// For ico/cur encoder.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image);
this.WriteImage(configuration, stream, image, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
@ -345,44 +351,65 @@ internal sealed class BmpEncoderCore
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WriteImage<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WriteImage<TPixel>(
Configuration configuration,
Stream stream,
Image<TPixel> image,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel)
ImageFrame<TPixel>? clonedFrame = null;
try
{
case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels);
break;
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
}
case BmpBitsPerPixel.Bit24:
this.Write24BitPixelData(configuration, stream, pixels);
break;
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
case BmpBitsPerPixel.Bit16:
this.Write16BitPixelData(configuration, stream, pixels);
break;
switch (this.bitsPerPixel)
{
case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit8:
this.Write8BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit24:
this.Write24BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit4:
this.Write4BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit16:
this.Write16BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit2:
this.Write2BitPixelData(configuration, stream, image);
break;
case BmpBitsPerPixel.Bit8:
this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
case BmpBitsPerPixel.Bit1:
this.Write1BitPixelData(configuration, stream, image);
break;
}
case BmpBitsPerPixel.Bit4:
this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
case BmpBitsPerPixel.Bit2:
this.Write2BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
if (this.processedAlphaMask)
case BmpBitsPerPixel.Bit1:
this.Write1BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break;
}
if (this.processedAlphaMask)
{
ProcessedAlphaMask(stream, encodingFrame);
}
}
finally
{
ProcessedAlphaMask(stream, image);
clonedFrame?.Dispose();
}
}
@ -396,7 +423,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write32BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -404,6 +436,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration,
@ -421,7 +455,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write24BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -431,6 +470,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration,
@ -448,7 +489,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write16BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
Buffer2D<TPixel> pixels,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -458,6 +504,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
@ -476,21 +524,32 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
bool isL8 = typeof(TPixel) == typeof(L8);
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
bool is8BitLuminance =
info.BitsPerPixel == 8
&& info.ColorType == PixelColorType.Luminance
&& info.AlphaRepresentation == PixelAlphaRepresentation.None
&& info.ComponentInfo!.Value.ComponentCount == 1;
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
if (isL8)
if (is8BitLuminance)
{
this.Write8BitPixelData(stream, image, colorPalette);
this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken);
}
else
{
this.Write8BitColor(configuration, stream, image, colorPalette);
this.Write8BitColor(configuration, stream, encodingFrame, colorPalette, cancellationToken);
}
}
@ -500,21 +559,29 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitColor<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image, Span<byte> colorPalette)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitColor<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
Span<byte> colorPalette,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
ReadOnlySpan<byte> pixelSpan = quantized.DangerousGetRowSpan(y);
stream.Write(pixelSpan);
@ -530,9 +597,14 @@ internal sealed class BmpEncoderCore
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write8BitLuminancePixelData<TPixel>(
Stream stream,
ImageFrame<TPixel> encodingFrame,
Span<byte> colorPalette,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Create a color palette with 256 different gray values.
@ -549,9 +621,11 @@ internal sealed class BmpEncoderCore
}
stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer();
for (int y = image.Height - 1; y >= 0; y--)
Buffer2D<TPixel> imageBuffer = encodingFrame.PixelBuffer;
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
stream.Write(outputPixelRow);
@ -569,8 +643,13 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write4BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -580,9 +659,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -591,8 +670,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
@ -619,8 +700,13 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write2BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -630,9 +716,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -641,8 +727,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4;
@ -678,8 +766,13 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
/// <param name="encodingFrame"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void Write1BitPixelData<TPixel>(
Configuration configuration,
Stream stream,
ImageFrame<TPixel> encodingFrame,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -689,9 +782,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -700,8 +793,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
cancellationToken.ThrowIfCancellationRequested();
quantizedPixelRow = quantized.DangerousGetRowSpan(y);
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
@ -766,10 +861,10 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices);
}
private static void ProcessedAlphaMask<TPixel>(Stream stream, Image<TPixel> image)
private static void ProcessedAlphaMask<TPixel>(Stream stream, ImageFrame<TPixel> encodingFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
int arrayWidth = image.Width / 8;
int arrayWidth = encodingFrame.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
@ -777,10 +872,10 @@ internal sealed class BmpEncoderCore
}
Span<byte> mask = stackalloc byte[arrayWidth];
for (int y = image.Height - 1; y >= 0; y--)
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
mask.Clear();
Span<TPixel> row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y);
Span<TPixel> row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int i = 0; i < arrayWidth; i++)
{

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
};
}

97
src/ImageSharp/Formats/EncodingUtilities.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Provides utilities for encoding images.
/// </summary>
internal static class EncodingUtilities
{
public static bool ShouldClearTransparentPixels<TPixel>(TransparentColorMode mode)
where TPixel : unmanaged, IPixel<TPixel>
=> mode == TransparentColorMode.Clear &&
TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned <see cref="ImageFrame{TPixel}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2DRegion<TPixel> buffer = clone.PixelBuffer.GetRegion();
ClearTransparentPixels(clone.Configuration, ref buffer, color);
}
/// <summary>
/// Convert transparent pixels, to pixels represented by <paramref name="color"/>, which can yield
/// to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="clone">The cloned <see cref="Buffer2DRegion{T}"/> where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
public static void ClearTransparentPixels<TPixel>(
Configuration configuration,
ref Buffer2DRegion<TPixel> clone,
Color color)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Vector4> vectors = configuration.MemoryAllocator.Allocate<Vector4>(clone.Width);
Span<Vector4> vectorsSpan = vectors.GetSpan();
Vector4 replacement = color.ToScaledVector4();
for (int y = 0; y < clone.Height; y++)
{
Span<TPixel> span = clone.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
ClearTransparentPixelRow(vectorsSpan, replacement);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
}
}
private static void ClearTransparentPixelRow(
Span<Vector4> vectorsSpan,
Vector4 replacement)
{
if (Vector128.IsHardwareAccelerated)
{
Vector128<float> replacement128 = replacement.AsVector128();
for (int i = 0; i < vectorsSpan.Length; i++)
{
ref Vector4 v = ref vectorsSpan[i];
Vector128<float> v128 = v.AsVector128();
// Do `vector == 0`
Vector128<float> mask = Vector128.Equals(v128, Vector128<float>.Zero);
// Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3));
// Use the mask to select the replacement vector
// (replacement & mask) | (v128 & ~mask)
v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
}
}
else
{
for (int i = 0; i < vectorsSpan.Length; i++)
{
if (vectorsSpan[i].W == 0F)
{
vectorsSpan[i] = replacement;
}
}
}
}
}

2
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -665,7 +665,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
return;
}
Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();

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

@ -67,6 +67,8 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly ushort? repeatCount;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
@ -83,6 +85,7 @@ internal sealed class GifEncoderCore
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.transparentColorMode = encoder.TransparentColorMode;
}
/// <summary>
@ -131,18 +134,40 @@ internal sealed class GifEncoderCore
}
}
// Quantize the first frame. Checking to see whether we can clear the transparent pixels
// to allow for a smaller color palette and encoded result.
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
ImageFrame<TPixel>? clonedFrame = null;
Configuration configuration = this.configuration;
TransparentColorMode mode = this.transparentColorMode;
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
clonedFrame = image.Frames.RootFrame.Clone();
GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
if (useGlobalTable)
{
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(configuration, mode, strategy, image);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
else
{
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
clonedFrame?.Dispose();
}
// Write the header.
@ -182,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();
// If the token is cancelled during encoding of frames we must ensure the
// quantized frame is disposed.
try
{
this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken);
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
stream.WriteByte(GifConstants.EndIntroducer);
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)
@ -236,61 +268,61 @@ internal sealed class GifEncoderCore
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
try
{
if (cancellationToken.IsCancellationRequested)
for (int i = 1; i < image.Frames.Count; i++)
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
cancellationToken.ThrowIfCancellationRequested();
return;
}
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
if (hasPaletteQuantizer)
finally
{
paletteQuantizer.Dispose();
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
}
}
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;
@ -324,7 +356,9 @@ internal sealed class GifEncoderCore
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground
? null :
previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
@ -341,6 +375,11 @@ internal sealed class GifEncoderCore
background,
true);
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,
bounds,

2
src/ImageSharp/Formats/IAnimatedImageEncoder.cs

@ -30,7 +30,7 @@ public interface IAnimatedImageEncoder
/// <summary>
/// Acts as a base class for all image encoders that allow encoding animation sequences.
/// </summary>
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }

2
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

@ -24,7 +24,7 @@ public interface IQuantizingImageEncoder
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder
{
/// <inheritdoc/>
public IQuantizer? Quantizer { get; init; }

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
};
}

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

@ -63,7 +63,6 @@ internal abstract class IconEncoderCore
this.entries[i].Entry.ImageOffset = (uint)stream.Position;
// We crop the frame to the size specified in the metadata.
// TODO: we can optimize this by cropping the frame only if the new size is both required and different.
using Image<TPixel> encodingFrame = new(width, height);
for (int y = 0; y < height; y++)
{
@ -82,6 +81,8 @@ internal abstract class IconEncoderCore
UseDoubleHeight = true,
SkipFileHeader = true,
SupportTransparency = false,
TransparentColorMode = this.encoder.TransparentColorMode,
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
BitsPerPixel = encodingMetadata.BmpBitsPerPixel
},
IconFrameCompression.Png => new PngEncoder()
@ -90,6 +91,7 @@ internal abstract class IconEncoderCore
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
BitDepth = PngBitDepth.Bit8,
ColorType = PngColorType.RgbWithAlpha,
TransparentColorMode = this.encoder.TransparentColorMode,
CompressionLevel = PngCompressionLevel.BestCompression
},
_ => throw new NotSupportedException(),
@ -121,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;
}

6
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -68,12 +68,6 @@ public class PngEncoder : QuantizingAnimatedImageEncoder
/// </summary>
public PngChunkFilter? ChunkFilter { get; init; }
/// <summary>
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
/// should be converted to transparent black, which can yield in better compression in some cases.
/// </summary>
public PngTransparentColorMode TransparentColorMode { get; init; }
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{

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

@ -7,6 +7,7 @@ using System.IO.Hashing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -188,18 +189,18 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode);
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame, Color.Transparent);
EncodingUtilities.ClearTransparentPixels(currentFrame, Color.Transparent);
}
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
currentFrame,
currentFrame.Bounds(),
currentFrame.Bounds,
null);
this.WriteHeaderChunk(stream);
@ -225,91 +226,94 @@ 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++;
}
if (image.Frames.Count > 1)
try
{
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
if (image.Frames.Count > 1)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
else
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
}
currentFrameIndex++;
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
else
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
}
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
currentFrameIndex++;
// Write following frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
// Write following frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
background,
blend);
if (clearTransparency)
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
ClearTransparentPixels(encodingFrame, background);
}
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
background,
blend);
if (clearTransparency)
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode;
}
}
}
this.WriteEndChunk(stream);
this.WriteEndChunk(stream);
stream.Flush();
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
stream.Flush();
}
finally
{
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
}
}
/// <inheritdoc />
@ -319,33 +323,6 @@ internal sealed class PngEncoderCore : IDisposable
this.currentScanline?.Dispose();
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
/// <param name="color">The color to replace transparent pixels with.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
where TPixel : unmanaged, IPixel<TPixel>
=> clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 transparent = color.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<TPixel> span = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
ref TPixel pixel = ref span[x];
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)
{
pixel = TPixel.FromRgba32(transparent);
}
}
}
});
/// <summary>
/// Creates the quantized image and calculates and sets the bit depth.
/// </summary>

21
src/ImageSharp/Formats/Png/PngTransparentColorMode.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Enum indicating how the transparency should be handled on encoding.
/// </summary>
public enum PngTransparentColorMode
{
/// <summary>
/// The transparency will be kept as is.
/// </summary>
Preserve = 0,
/// <summary>
/// Converts fully transparent pixels that may contain R, G, B values which are not 0,
/// to transparent black, which can yield in better compression in some cases.
/// </summary>
Clear = 1,
}

2
src/ImageSharp/Formats/Qoi/QoiEncoder.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Image encoder for writing an image to a stream as a QOI image
/// </summary>
public class QoiEncoder : ImageEncoder
public class QoiEncoder : AlphaAwareImageEncoder
{
/// <summary>
/// Gets the color channels on the image that can be

219
src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs

@ -55,7 +55,7 @@ internal class QoiEncoderCore
Guard.NotNull(stream, nameof(stream));
this.WriteHeader(image, stream);
this.WritePixels(image, stream);
this.WritePixels(image, stream, cancellationToken);
WriteEndOfStream(stream);
stream.Flush();
}
@ -78,7 +78,7 @@ internal class QoiEncoderCore
stream.WriteByte((byte)qoiColorSpace);
}
private void WritePixels<TPixel>(Image<TPixel> image, Stream stream)
private void WritePixels<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Start image encoding
@ -86,137 +86,156 @@ internal class QoiEncoderCore
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255);
Rgba32 currentRgba32 = default;
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer;
using IMemoryOwner<Rgba32> rgbaRowBuffer = this.memoryAllocator.Allocate<Rgba32>(pixels.Width);
Span<Rgba32> rgbaRow = rgbaRowBuffer.GetSpan();
for (int i = 0; i < pixels.Height; i++)
ImageFrame<TPixel>? clonedFrame = null;
try
{
Span<TPixel> row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
for (int j = 0; j < row.Length && i < pixels.Height; j++)
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode))
{
// We get the RGBA value from pixels
currentRgba32 = rgbaRow[j];
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
using IMemoryOwner<Rgba32> rgbaRowBuffer = this.memoryAllocator.Allocate<Rgba32>(pixels.Width);
Span<Rgba32> rgbaRow = rgbaRowBuffer.GetSpan();
Configuration configuration = this.configuration;
for (int i = 0; i < pixels.Height; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// First, we check if the current pixel is equal to the previous one
// If so, we do a QOI_OP_RUN
if (currentRgba32.Equals(previousPixel))
Span<TPixel> row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
for (int j = 0; j < row.Length && i < pixels.Height; j++)
{
/* It looks like this isn't an error, but this makes possible that
* files start with a QOI_OP_RUN if their first pixel is a fully opaque
* black. However, the decoder of this project takes that into consideration
*
* To further details, see https://github.com/phoboslab/qoi/issues/258,
* and we should discuss what to do about this approach and
* if it's correct
*/
int repetitions = 0;
do
// We get the RGBA value from pixels
currentRgba32 = rgbaRow[j];
// First, we check if the current pixel is equal to the previous one
// If so, we do a QOI_OP_RUN
if (currentRgba32.Equals(previousPixel))
{
repetitions++;
j++;
if (j == row.Length)
/* It looks like this isn't an error, but this makes possible that
* files start with a QOI_OP_RUN if their first pixel is a fully opaque
* black. However, the decoder of this project takes that into consideration
*
* To further details, see https://github.com/phoboslab/qoi/issues/258,
* and we should discuss what to do about this approach and
* if it's correct
*/
int repetitions = 0;
do
{
j = 0;
i++;
if (i == pixels.Height)
repetitions++;
j++;
if (j == row.Length)
{
break;
j = 0;
i++;
if (i == pixels.Height)
{
break;
}
row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, row, rgbaRow);
}
row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
currentRgba32 = rgbaRow[j];
}
while (currentRgba32.Equals(previousPixel) && repetitions < 62);
currentRgba32 = rgbaRow[j];
}
while (currentRgba32.Equals(previousPixel) && repetitions < 62);
j--;
stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
j--;
stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
/* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
* it will be taken and compared on the next iteration
*/
continue;
}
/* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
* it will be taken and compared on the next iteration
*/
continue;
}
// else, we check if it exists in the previously seen pixels
// If so, we do a QOI_OP_INDEX
int pixelArrayPosition = GetArrayPosition(currentRgba32);
if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
{
stream.WriteByte((byte)pixelArrayPosition);
}
else
{
// else, we check if the difference is less than -2..1
// Since it wasn't found on the previously seen pixels, we save it
previouslySeenPixels[pixelArrayPosition] = currentRgba32;
int diffRed = currentRgba32.R - previousPixel.R;
int diffGreen = currentRgba32.G - previousPixel.G;
int diffBlue = currentRgba32.B - previousPixel.B;
// If so, we do a QOI_OP_DIFF
if (diffRed is >= -2 and <= 1 &&
diffGreen is >= -2 and <= 1 &&
diffBlue is >= -2 and <= 1 &&
currentRgba32.A == previousPixel.A)
// else, we check if it exists in the previously seen pixels
// If so, we do a QOI_OP_INDEX
int pixelArrayPosition = GetArrayPosition(currentRgba32);
if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
{
// Bottom limit is -2, so we add 2 to make it equal to 0
int dr = diffRed + 2;
int dg = diffGreen + 2;
int db = diffBlue + 2;
byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
stream.WriteByte(valueToWrite);
stream.WriteByte((byte)pixelArrayPosition);
}
else
{
// else, we check if the green difference is less than -32..31 and the rest -8..7
// If so, we do a QOI_OP_LUMA
int diffRedGreen = diffRed - diffGreen;
int diffBlueGreen = diffBlue - diffGreen;
if (diffGreen is >= -32 and <= 31 &&
diffRedGreen is >= -8 and <= 7 &&
diffBlueGreen is >= -8 and <= 7 &&
// else, we check if the difference is less than -2..1
// Since it wasn't found on the previously seen pixels, we save it
previouslySeenPixels[pixelArrayPosition] = currentRgba32;
int diffRed = currentRgba32.R - previousPixel.R;
int diffGreen = currentRgba32.G - previousPixel.G;
int diffBlue = currentRgba32.B - previousPixel.B;
// If so, we do a QOI_OP_DIFF
if (diffRed is >= -2 and <= 1 &&
diffGreen is >= -2 and <= 1 &&
diffBlue is >= -2 and <= 1 &&
currentRgba32.A == previousPixel.A)
{
int dr_dg = diffRedGreen + 8;
int db_dg = diffBlueGreen + 8;
byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
stream.WriteByte(byteToWrite1);
stream.WriteByte(byteToWrite2);
// Bottom limit is -2, so we add 2 to make it equal to 0
int dr = diffRed + 2;
int dg = diffGreen + 2;
int db = diffBlue + 2;
byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
stream.WriteByte(valueToWrite);
}
else
{
// else, we check if the alpha is equal to the previous pixel
// If so, we do a QOI_OP_RGB
if (currentRgba32.A == previousPixel.A)
// else, we check if the green difference is less than -32..31 and the rest -8..7
// If so, we do a QOI_OP_LUMA
int diffRedGreen = diffRed - diffGreen;
int diffBlueGreen = diffBlue - diffGreen;
if (diffGreen is >= -32 and <= 31 &&
diffRedGreen is >= -8 and <= 7 &&
diffBlueGreen is >= -8 and <= 7 &&
currentRgba32.A == previousPixel.A)
{
stream.WriteByte((byte)QoiChunk.QoiOpRgb);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
int dr_dg = diffRedGreen + 8;
int db_dg = diffBlueGreen + 8;
byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
stream.WriteByte(byteToWrite1);
stream.WriteByte(byteToWrite2);
}
else
{
// else, we do a QOI_OP_RGBA
stream.WriteByte((byte)QoiChunk.QoiOpRgba);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
stream.WriteByte(currentRgba32.A);
// else, we check if the alpha is equal to the previous pixel
// If so, we do a QOI_OP_RGB
if (currentRgba32.A == previousPixel.A)
{
stream.WriteByte((byte)QoiChunk.QoiOpRgb);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
}
else
{
// else, we do a QOI_OP_RGBA
stream.WriteByte((byte)QoiChunk.QoiOpRgba);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
stream.WriteByte(currentRgba32.A);
}
}
}
}
}
previousPixel = currentRgba32;
previousPixel = currentRgba32;
}
}
}
finally
{
clonedFrame?.Dispose();
}
}
private static void WriteEndOfStream(Stream stream)

2
src/ImageSharp/Formats/Tga/TgaEncoder.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
/// <summary>
/// Image encoder for writing an image to a stream as a Targa true-vision image.
/// </summary>
public sealed class TgaEncoder : ImageEncoder
public sealed class TgaEncoder : AlphaAwareImageEncoder
{
/// <summary>
/// Gets the number of bits per pixel.

32
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -29,6 +29,8 @@ internal sealed class TgaEncoderCore
/// </summary>
private readonly TgaCompression compression;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Initializes a new instance of the <see cref="TgaEncoderCore"/> class.
/// </summary>
@ -39,6 +41,7 @@ internal sealed class TgaEncoderCore
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = encoder.Compression;
this.transparentColorMode = encoder.TransparentColorMode;
}
/// <summary>
@ -103,16 +106,33 @@ internal sealed class TgaEncoderCore
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, TgaFileHeader.Size);
if (this.compression is TgaCompression.RunLength)
ImageFrame<TPixel>? clonedFrame = null;
try
{
this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame, cancellationToken);
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
if (this.compression is TgaCompression.RunLength)
{
this.WriteRunLengthEncodedImage(stream, encodingFrame, cancellationToken);
}
else
{
this.WriteImage(image.Configuration, stream, encodingFrame, cancellationToken);
}
stream.Flush();
}
else
finally
{
this.WriteImage(image.Configuration, stream, image.Frames.RootFrame, cancellationToken);
clonedFrame?.Dispose();
}
stream.Flush();
}
/// <summary>

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

@ -49,6 +49,11 @@ internal sealed class TiffEncoderCore
/// </summary>
private readonly DeflateCompressionLevel compressionLevel;
/// <summary>
/// The transparent color mode to use when encoding.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
/// </summary>
@ -59,20 +64,21 @@ internal sealed class TiffEncoderCore
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
/// <param name="options">The options for the encoder.</param>
/// <param name="encoder">The options for the encoder.</param>
/// <param name="configuration">The global configuration.</param>
public TiffEncoderCore(TiffEncoder options, Configuration configuration)
public TiffEncoderCore(TiffEncoder encoder, Configuration configuration)
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = options.PixelSamplingStrategy;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
this.CompressionType = options.Compression;
this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
this.skipMetadata = options.SkipMetadata;
this.PhotometricInterpretation = encoder.PhotometricInterpretation;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.BitsPerPixel = encoder.BitsPerPixel;
this.HorizontalPredictor = encoder.HorizontalPredictor;
this.CompressionType = encoder.Compression;
this.compressionLevel = encoder.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
this.skipMetadata = encoder.SkipMetadata;
this.transparentColorMode = encoder.TransparentColorMode;
}
/// <summary>
@ -131,14 +137,30 @@ internal sealed class TiffEncoderCore
long ifdMarker = WriteHeader(writer, buffer);
Image<TPixel>? metadataImage = image;
Image<TPixel>? imageMetadata = image;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? clonedFrame = null;
try
{
cancellationToken.ThrowIfCancellationRequested();
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
metadataImage = null;
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = frame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? frame;
ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
imageMetadata = null;
}
finally
{
clonedFrame?.Dispose();
}
}
long currentOffset = writer.BaseStream.Position;

22
src/ImageSharp/Formats/TransparentColorMode.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Specifies how transparent pixels should be handled during encoding.
/// </summary>
public enum TransparentColorMode
{
/// <summary>
/// Retains the original color values of transparent pixels.
/// </summary>
Preserve = 0,
/// <summary>
/// Converts transparent pixels with non-zero color components
/// to fully transparent pixels (all components set to zero),
/// which may improve compression.
/// </summary>
Clear = 1,
}

2
src/ImageSharp/Formats/Webp/AlphaEncoder.cs

@ -49,7 +49,7 @@ internal static class AlphaEncoder
quality,
skipMetadata,
effort,
WebpTransparentColorMode.Preserve,
TransparentColorMode.Preserve,
false,
0);

12
src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs

@ -47,7 +47,7 @@ internal static unsafe class PredictorEncoder
int[][] bestHisto,
bool nearLossless,
int nearLosslessQuality,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool lowEffort)
{
@ -202,7 +202,7 @@ internal static unsafe class PredictorEncoder
int[][] histoArgb,
int[][] bestHisto,
int maxQuantization,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
Span<uint> modes,
@ -340,19 +340,20 @@ internal static unsafe class PredictorEncoder
int xEnd,
int y,
int maxQuantization,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
Span<uint> output,
Span<short> scratch)
{
if (transparentColorMode == WebpTransparentColorMode.Preserve)
if (transparentColorMode == TransparentColorMode.Preserve)
{
PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch);
}
else
{
#pragma warning disable SA1503 // Braces should not be omitted
#pragma warning disable RCS1001 // Add braces (when expression spans over multiple lines)
fixed (uint* currentRow = currentRowSpan)
fixed (uint* upperRow = upperRowSpan)
{
@ -466,6 +467,7 @@ internal static unsafe class PredictorEncoder
}
}
}
#pragma warning restore RCS1001 // Add braces (when expression spans over multiple lines)
#pragma warning restore SA1503 // Braces should not be omitted
/// <summary>
@ -577,7 +579,7 @@ internal static unsafe class PredictorEncoder
Span<uint> argbScratch,
Span<uint> argb,
int maxQuantization,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
bool lowEffort)

4
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -69,7 +69,7 @@ internal class Vp8LEncoder : IDisposable
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
@ -114,7 +114,7 @@ internal class Vp8LEncoder : IDisposable
uint quality,
bool skipMetadata,
WebpEncodingMethod method,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool nearLossless,
int nearLosslessQuality)
{

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;

2
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -343,7 +343,7 @@ internal class WebpAnimationDecoder : IDisposable
return;
}
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
pixelRegion.Fill(backgroundPixel);

15
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.
@ -58,13 +66,6 @@ public sealed class WebpEncoder : AnimatedImageEncoder
/// </summary>
public int FilterStrength { get; init; } = 60;
/// <summary>
/// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// The default value is Clear.
/// </summary>
public WebpTransparentColorMode TransparentColorMode { get; init; } = WebpTransparentColorMode.Clear;
/// <summary>
/// Gets a value indicating whether near lossless mode should be used.
/// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.

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

@ -54,7 +54,7 @@ internal sealed class WebpEncoderCore
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
@ -166,7 +166,10 @@ internal sealed class WebpEncoderCore
// Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
cancellationToken.ThrowIfCancellationRequested();
hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation);
if (hasAnimation)
{
@ -178,10 +181,7 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
@ -253,7 +253,7 @@ internal sealed class WebpEncoderCore
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds, frameMetadata);
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@ -261,10 +261,7 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
@ -310,6 +307,7 @@ internal sealed class WebpEncoderCore
}
else
{
cancellationToken.ThrowIfCancellationRequested();
encoder.EncodeStatic(stream, image);
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}

20
src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Enum indicating how the transparency should be handled on encoding.
/// </summary>
public enum WebpTransparentColorMode
{
/// <summary>
/// Discard the transparency information for better compression.
/// </summary>
Clear = 0,
/// <summary>
/// The transparency will be kept as is.
/// </summary>
Preserve = 1,
}

2
src/ImageSharp/ImageFrame.cs

@ -56,7 +56,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// Gets the bounds of the frame.
/// </summary>
/// <returns>The <see cref="Rectangle"/></returns>
public Rectangle Bounds() => new(0, 0, this.Width, this.Height);
public Rectangle Bounds => new(0, 0, this.Width, this.Height);
/// <inheritdoc />
public void Dispose()

2
src/ImageSharp/ImageFrame{TPixel}.cs

@ -429,7 +429,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
ParallelRowIterator.IterateRowIntervals(
configuration,
this.Bounds(),
this.Bounds,
in operation);
return target;

62
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -25,27 +25,65 @@ public static class Buffer2DExtensions
return buffer.FastMemoryGroup.View;
}
/// <summary>
/// Performs a deep clone of the buffer covering the specified <paramref name="rectangle"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="source">The source buffer.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="rectangle">The rectangle to clone.</param>
/// <returns>The <see cref="Buffer2D{T}"/>.</returns>
internal static Buffer2D<T> CloneRegion<T>(this Buffer2D<T> source, Configuration configuration, Rectangle rectangle)
where T : unmanaged
{
Buffer2D<T> buffer = configuration.MemoryAllocator.Allocate2D<T>(
rectangle.Width,
rectangle.Height,
configuration.PreferContiguousImageBuffers);
// Optimization for when the size of the area is the same as the buffer size.
Buffer2DRegion<T> sourceRegion = source.GetRegion(rectangle);
if (sourceRegion.IsFullBufferArea)
{
sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup);
}
else
{
for (int y = 0; y < rectangle.Height; y++)
{
sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y));
}
}
return buffer;
}
/// <summary>
/// TODO: Does not work with multi-buffer groups, should be specific to Resize.
/// Copy <paramref name="columnCount"/> columns of <paramref name="buffer"/> inplace,
/// from positions starting at <paramref name="sourceIndex"/> to positions at <paramref name="destIndex"/>.
/// Copy <paramref name="columnCount"/> columns of <paramref name="buffer"/> in-place,
/// from positions starting at <paramref name="sourceIndex"/> to positions at <paramref name="destinationIndex"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/>.</param>
/// <param name="sourceIndex">The source column index.</param>
/// <param name="destinationIndex">The destination column index.</param>
/// <param name="columnCount">The number of columns to copy.</param>
internal static unsafe void DangerousCopyColumns<T>(
this Buffer2D<T> buffer,
int sourceIndex,
int destIndex,
int destinationIndex,
int columnCount)
where T : struct
{
DebugGuard.NotNull(buffer, nameof(buffer));
DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex));
DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex));
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount);
DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex));
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount);
int elementSize = Unsafe.SizeOf<T>();
int width = buffer.Width * elementSize;
int sOffset = sourceIndex * elementSize;
int dOffset = destIndex * elementSize;
int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize;
Span<byte> span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span);
@ -73,9 +111,7 @@ public static class Buffer2DExtensions
/// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle FullRectangle<T>(this Buffer2D<T> buffer)
where T : struct
{
return new Rectangle(0, 0, buffer.Width, buffer.Height);
}
=> new(0, 0, buffer.Width, buffer.Height);
/// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the subregion represented by 'rectangle'
@ -86,11 +122,11 @@ public static class Buffer2DExtensions
/// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, Rectangle rectangle)
where T : unmanaged =>
new Buffer2DRegion<T>(buffer, rectangle);
new(buffer, rectangle);
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : unmanaged =>
new Buffer2DRegion<T>(buffer, new Rectangle(x, y, width, height));
new(buffer, new Rectangle(x, y, width, height));
/// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the whole area of 'buffer'
@ -100,7 +136,7 @@ public static class Buffer2DExtensions
/// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer)
where T : unmanaged =>
new Buffer2DRegion<T>(buffer);
new(buffer);
/// <summary>
/// Returns the size of the buffer.
@ -115,6 +151,8 @@ public static class Buffer2DExtensions
/// <summary>
/// Gets the bounds of the buffer.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle Bounds<T>(this Buffer2D<T> buffer)
where T : struct =>

2
src/ImageSharp/Memory/Buffer2DRegion{T}.cs

@ -107,7 +107,7 @@ public readonly struct Buffer2DRegion<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion<T> GetSubRegion(int x, int y, int width, int height)
{
var rectangle = new Rectangle(x, y, width, height);
Rectangle rectangle = new(x, y, width, height);
return this.GetSubRegion(rectangle);
}

7
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs

@ -36,8 +36,13 @@ internal static class MemoryGroupExtensions
/// <summary>
/// Returns a slice that is expected to be within the bounds of a single buffer.
/// Otherwise <see cref="ArgumentOutOfRangeException"/> is thrown.
/// </summary>
/// <typeparam name="T">The type of element.</typeparam>
/// <param name="group">The group.</param>
/// <param name="start">The start index of the slice.</param>
/// <param name="length">The length of the slice.</param>
/// <exception cref="ArgumentOutOfRangeException">Slice is out of bounds.</exception>
/// <returns>The <see cref="MemoryGroup{T}"/> slice.</returns>
internal static Memory<T> GetBoundedMemorySlice<T>(this IMemoryGroup<T> group, long start, int length)
where T : struct
{

12
src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs

@ -61,9 +61,7 @@ internal class DefaultImageProcessorContext<TPixel> : IInternalImageProcessingCo
/// <inheritdoc/>
public IImageProcessingContext ApplyProcessor(IImageProcessor processor)
{
return this.ApplyProcessor(processor, this.GetCurrentBounds());
}
=> this.ApplyProcessor(processor, this.GetCurrentBounds());
/// <inheritdoc/>
public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle)
@ -74,11 +72,9 @@ internal class DefaultImageProcessorContext<TPixel> : IInternalImageProcessingCo
// interim clone if the first processor in the pipeline is a cloning processor.
if (processor is ICloningImageProcessor cloningImageProcessor)
{
using (ICloningImageProcessor<TPixel> pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle))
{
this.destination = pixelProcessor.CloneAndExecute();
return this;
}
using ICloningImageProcessor<TPixel> pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle);
this.destination = pixelProcessor.CloneAndExecute();
return this;
}
// Not a cloning processor? We need to create a clone to operate on.

5
src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -42,7 +41,7 @@ public static partial class ProcessingExtensions
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> source.CalculateIntegralImage(source.Bounds());
=> source.CalculateIntegralImage(source.Bounds);
/// <summary>
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/>
@ -56,7 +55,7 @@ public static partial class ProcessingExtensions
{
Configuration configuration = source.Configuration;
var interest = Rectangle.Intersect(bounds, source.Bounds());
Rectangle interest = Rectangle.Intersect(bounds, source.Bounds);
int startY = interest.Y;
int startX = interest.X;
int endY = interest.Height;

19
src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization;
/// <summary>
/// Performs Bradley Adaptive Threshold filter against an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
@ -30,7 +31,7 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
Configuration configuration = this.Configuration;
TPixel upper = this.definition.Upper.ToPixel<TPixel>();
@ -97,19 +98,23 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
Span<TPixel> rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToL8(this.configuration, rowSpan, span);
int startY = this.startY;
int maxX = this.bounds.Width - 1;
int maxY = this.bounds.Height - 1;
int clusterSize = this.clusterSize;
float thresholdLimit = this.thresholdLimit;
Buffer2D<ulong> image = this.intImage;
for (int x = 0; x < rowSpan.Length; x++)
{
int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX);
int x2 = Math.Min(x + this.clusterSize + 1, maxX);
int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY);
int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY);
int x1 = Math.Clamp(x - clusterSize + 1, 0, maxX);
int x2 = Math.Min(x + clusterSize + 1, maxX);
int y1 = Math.Clamp(y - startY - clusterSize + 1, 0, maxY);
int y2 = Math.Min(y - startY + clusterSize + 1, maxY);
uint count = (uint)((x2 - x1) * (y2 - y1));
ulong sum = Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], ulong.MaxValue);
ulong sum = Math.Min(image[x2, y2] - image[x1, y2] - image[x2, y1] + image[x1, y1], ulong.MaxValue);
if (span[x].PackedValue * count <= sum * this.thresholdLimit)
if (span[x].PackedValue * count <= sum * thresholdLimit)
{
rowSpan[x] = this.lower;
}

10
src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs

@ -38,8 +38,8 @@ internal class BinaryThresholdProcessor<TPixel> : ImageProcessor<TPixel>
Rectangle sourceRectangle = this.SourceRectangle;
Configuration configuration = this.Configuration;
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
var operation = new RowOperation(
Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds);
RowOperation operation = new(
interest.X,
source.PixelBuffer,
upper,
@ -169,10 +169,8 @@ internal class BinaryThresholdProcessor<TPixel> : ImageProcessor<TPixel>
{
return chroma / (max + min);
}
else
{
return chroma / (2F - max - min);
}
return chroma / (2F - max - min);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

22
src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs

@ -75,12 +75,12 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// Preliminary gamma highlight pass
if (this.gamma == 3F)
{
var gammaOperation = new ApplyGamma3ExposureRowOperation(sourceRectangle, source.PixelBuffer, this.Configuration);
ApplyGamma3ExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration);
ParallelRowIterator.IterateRows<ApplyGamma3ExposureRowOperation, Vector4>(
this.Configuration,
sourceRectangle,
@ -88,7 +88,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
}
else
{
var gammaOperation = new ApplyGammaExposureRowOperation(sourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ApplyGammaExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration, this.gamma);
ParallelRowIterator.IterateRows<ApplyGammaExposureRowOperation, Vector4>(
this.Configuration,
sourceRectangle,
@ -104,7 +104,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
// Apply the inverse gamma exposure pass, and write the final pixel data
if (this.gamma == 3F)
{
var operation = new ApplyInverseGamma3ExposureRowOperation(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration);
ApplyInverseGamma3ExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration);
ParallelRowIterator.IterateRows(
this.Configuration,
sourceRectangle,
@ -112,7 +112,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
}
else
{
var operation = new ApplyInverseGammaExposureRowOperation(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, 1 / this.gamma);
ApplyInverseGammaExposureRowOperation operation = new(sourceRectangle, source.PixelBuffer, processingBuffer, this.Configuration, 1 / this.gamma);
ParallelRowIterator.IterateRows(
this.Configuration,
sourceRectangle,
@ -146,7 +146,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
// doing two 1D convolutions with the same kernel, we can use a single kernel sampling map as if
// we were using a 2D kernel with each dimension being the same as the length of our kernel, and
// use the two sampling offset spans resulting from this same map. This saves some extra work.
using var mapXY = new KernelSamplingMap(configuration.MemoryAllocator);
using KernelSamplingMap mapXY = new(configuration.MemoryAllocator);
mapXY.BuildSamplingOffsetMap(this.kernelSize, this.kernelSize, sourceRectangle);
@ -161,7 +161,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
Vector4 parameters = Unsafe.Add(ref paramsRef, (uint)i);
// Horizontal convolution
var horizontalOperation = new FirstPassConvolutionRowOperation(
FirstPassConvolutionRowOperation horizontalOperation = new(
sourceRectangle,
firstPassBuffer,
source.PixelBuffer,
@ -175,7 +175,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
in horizontalOperation);
// Vertical 1D convolutions to accumulate the partial results on the target buffer
var verticalOperation = new BokehBlurProcessor.SecondPassConvolutionRowOperation(
BokehBlurProcessor.SecondPassConvolutionRowOperation verticalOperation = new(
sourceRectangle,
processingBuffer,
firstPassBuffer,
@ -342,9 +342,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds)
{
return bounds.Width;
}
=> bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
@ -391,7 +389,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
public void Invoke(int y)
{
Vector4 low = Vector4.Zero;
var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
Vector4 high = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
Span<TPixel> targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..];
Span<Vector4> sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y)[this.bounds.X..];

2
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs

@ -62,7 +62,7 @@ internal class Convolution2DProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (KernelSamplingMap map = new(allocator))
{

2
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs

@ -98,7 +98,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
{
using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// We can create a single sampling map with the size as if we were using the non separated 2D kernel
// the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur.

2
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs

@ -89,7 +89,7 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (KernelSamplingMap map = new(allocator))
{

2
src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs

@ -58,7 +58,7 @@ internal class EdgeDetectorCompassProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// We need a clean copy for each pass to start from
using ImageFrame<TPixel> cleanCopy = source.Clone();

2
src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs

@ -29,7 +29,7 @@ internal sealed class MedianBlurProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using KernelSamplingMap map = new(this.Configuration.MemoryAllocator);
map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY);

2
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -46,7 +46,7 @@ internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
}

8
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs

@ -36,14 +36,12 @@ internal sealed class PixelRowDelegateProcessor : IImageProcessor
/// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged, IPixel<TPixel>
{
return new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
=> new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
new PixelRowDelegate(this.PixelRowOperation),
configuration,
this.Modifiers,
source,
sourceRectangle);
}
/// <summary>
/// A <see langword="struct"/> implementing the row processing logic for <see cref="PixelRowDelegateProcessor"/>.
@ -54,9 +52,7 @@ internal sealed class PixelRowDelegateProcessor : IImageProcessor
[MethodImpl(InliningOptions.ShortMethod)]
public PixelRowDelegate(PixelRowOperation pixelRowOperation)
{
this.pixelRowOperation = pixelRowOperation;
}
=> this.pixelRowOperation = pixelRowOperation;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]

4
src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs

@ -48,8 +48,8 @@ internal sealed class PixelRowDelegateProcessor<TPixel, TDelegate> : ImageProces
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowOperation(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
RowOperation operation = new(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,

2
src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs

@ -32,7 +32,7 @@ internal class PixelateProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
int size = this.Size;
Guard.MustBeBetweenOrEqualTo(size, 0, interest.Width, nameof(size));

8
src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs

@ -27,15 +27,13 @@ internal class FilterProcessor<TPixel> : ImageProcessor<TPixel>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public FilterProcessor(Configuration configuration, FilterProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
}
=> this.definition = definition;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
var operation = new RowOperation(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
RowOperation operation = new(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,

4
src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs

@ -23,9 +23,9 @@ internal sealed class OpaqueProcessor<TPixel> : ImageProcessor<TPixel>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
var operation = new OpaqueRowOperation(this.Configuration, source.PixelBuffer, interest);
OpaqueRowOperation operation = new(this.Configuration, source.PixelBuffer, interest);
ParallelRowIterator.IterateRows<OpaqueRowOperation, Vector4>(this.Configuration, interest, in operation);
}

29
src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs

@ -28,9 +28,9 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
/// </param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
public AutoLevelProcessor(
Configuration configuration,
int luminanceLevels,
@ -40,9 +40,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle)
{
this.SyncChannels = syncChannels;
}
=> this.SyncChannels = syncChannels;
/// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel.
@ -54,12 +52,12 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
{
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels.
var grayscaleOperation = new GrayscaleLevelsRowOperation<TPixel>(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
GrayscaleLevelsRowOperation<TPixel> grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration,
interest,
@ -83,7 +81,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
if (this.SyncChannels)
{
var cdfOperation = new SynchronizedChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
SynchronizedChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<SynchronizedChannelsRowOperation, Vector4>(
this.Configuration,
interest,
@ -91,7 +89,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
}
else
{
var cdfOperation = new SeperateChannelsRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
SeperateChannelsRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<SeperateChannelsRowOperation, Vector4>(
this.Configuration,
interest,
@ -136,10 +134,10 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
Span<Vector4> vectorBuffer = span[..this.bounds.Width];
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
var sourceAccess = new PixelAccessor<TPixel>(this.source);
PixelAccessor<TPixel> sourceAccess = new(this.source);
int levels = this.luminanceLevels;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
@ -148,12 +146,11 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, (uint)x);
Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x);
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin;
float scalingFactor = scaledLuminance * levels / luminance;
Vector4 scaledVector = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W);
Unsafe.Add(ref vectorRef, (uint)x) = scaledVector;
Unsafe.Add(ref vectorRef, (uint)x) = new(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow);
@ -197,10 +194,10 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
Span<Vector4> vectorBuffer = span[..this.bounds.Width];
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
var sourceAccess = new PixelAccessor<TPixel>(this.source);
PixelAccessor<TPixel> sourceAccess = new(this.source);
int levelsMinusOne = this.luminanceLevels - 1;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
@ -209,7 +206,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne;
Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x) * levelsMinusOne;
uint originalX = (uint)MathF.Round(vector.X);
float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin;

10
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs

@ -46,12 +46,12 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
{
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels.
var grayscaleOperation = new GrayscaleLevelsRowOperation<TPixel>(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
GrayscaleLevelsRowOperation<TPixel> grayscaleOperation = new(this.Configuration, interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration,
interest,
@ -74,7 +74,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
// Apply the cdf to each pixel of the image
var cdfOperation = new CdfApplicationRowOperation(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
CdfApplicationRowOperation cdfOperation = new(this.Configuration, interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin);
ParallelRowIterator.IterateRows<CdfApplicationRowOperation, Vector4>(
this.Configuration,
interest,
@ -118,7 +118,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
{
Span<Vector4> vectorBuffer = span.Slice(0, this.bounds.Width);
Span<Vector4> vectorBuffer = span[..this.bounds.Width];
ref Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
int levels = this.luminanceLevels;
@ -129,7 +129,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
for (int x = 0; x < this.bounds.Width; x++)
{
var vector = Unsafe.Add(ref vectorRef, (uint)x);
Vector4 vector = Unsafe.Add(ref vectorRef, (uint)x);
int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin;
Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W);

4
src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs

@ -35,7 +35,7 @@ internal class BackgroundColorProcessor<TPixel> : ImageProcessor<TPixel>
TPixel color = this.definition.Color.ToPixel<TPixel>();
GraphicsOptions graphicsOptions = this.definition.GraphicsOptions;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
Configuration configuration = this.Configuration;
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
@ -48,7 +48,7 @@ internal class BackgroundColorProcessor<TPixel> : ImageProcessor<TPixel>
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions);
var operation = new RowOperation(configuration, interest, blender, amount, colors, source.PixelBuffer);
RowOperation operation = new(configuration, interest, blender, amount, colors, source.PixelBuffer);
ParallelRowIterator.IterateRows(
configuration,
interest,

4
src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs

@ -40,7 +40,7 @@ internal class GlowProcessor<TPixel> : ImageProcessor<TPixel>
TPixel glowColor = this.definition.GlowColor.ToPixel<TPixel>();
float blendPercent = this.definition.GraphicsOptions.BlendPercentage;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
Vector2 center = Rectangle.Center(interest);
float finalRadius = this.definition.Radius.Calculate(interest.Size);
@ -54,7 +54,7 @@ internal class GlowProcessor<TPixel> : ImageProcessor<TPixel>
using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(glowColor);
var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer);
RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer);
ParallelRowIterator.IterateRows<RowOperation, float>(
configuration,
interest,

4
src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs

@ -40,7 +40,7 @@ internal class VignetteProcessor<TPixel> : ImageProcessor<TPixel>
TPixel vignetteColor = this.definition.VignetteColor.ToPixel<TPixel>();
float blendPercent = this.definition.GraphicsOptions.BlendPercentage;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
Vector2 center = Rectangle.Center(interest);
float finalRadiusX = this.definition.RadiusX.Calculate(interest.Size);
@ -62,7 +62,7 @@ internal class VignetteProcessor<TPixel> : ImageProcessor<TPixel>
using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(vignetteColor);
var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer);
RowOperation operation = new(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer);
ParallelRowIterator.IterateRows<RowOperation, float>(
configuration,
interest,

2
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -32,7 +32,7 @@ internal class QuantizeProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
Rectangle interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle);
Rectangle interest = Rectangle.Intersect(source.Bounds, this.SourceRectangle);
Configuration configuration = this.Configuration;
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);

71
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -50,7 +51,7 @@ public static class QuantizerUtilities
Guard.NotNull(quantizer, nameof(quantizer));
Guard.NotNull(source, nameof(source));
Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds);
Rectangle interest = Rectangle.Intersect(source.Bounds, bounds);
Buffer2DRegion<TPixel> region = source.PixelBuffer.GetRegion(interest);
// Collect the palette. Required before the second pass runs.
@ -77,7 +78,7 @@ public static class QuantizerUtilities
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(source, nameof(source));
Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds);
Rectangle interest = Rectangle.Intersect(source.Bounds, bounds);
IndexedImageFrame<TPixel> destination = new(
quantizer.Configuration,
@ -111,10 +112,39 @@ public static class QuantizerUtilities
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source);
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The pixel specific quantizer.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="mode">The transparent color mode.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image to sample from.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
Configuration configuration,
TransparentColorMode mode,
IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
using Buffer2D<TPixel> clone = region.Buffer.CloneRegion(configuration, region.Rectangle);
quantizer.AddPaletteColors(clone.GetRegion());
}
}
else
{
quantizer.AddPaletteColors(region);
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
}
}
}
@ -130,10 +160,39 @@ public static class QuantizerUtilities
IPixelSamplingStrategy pixelSamplingStrategy,
ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
=> quantizer.BuildPalette(source.Configuration, TransparentColorMode.Preserve, pixelSamplingStrategy, source);
/// <summary>
/// Adds colors to the quantized palette from the given pixel regions.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The pixel specific quantizer.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="mode">The transparent color mode.</param>
/// <param name="pixelSamplingStrategy">The pixel sampling strategy.</param>
/// <param name="source">The source image frame to sample from.</param>
public static void BuildPalette<TPixel>(
this IQuantizer<TPixel> quantizer,
Configuration configuration,
TransparentColorMode mode,
IPixelSamplingStrategy pixelSamplingStrategy,
ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
using Buffer2D<TPixel> clone = region.Buffer.CloneRegion(configuration, region.Rectangle);
quantizer.AddPaletteColors(clone.GetRegion());
}
}
else
{
quantizer.AddPaletteColors(region);
foreach (Buffer2DRegion<TPixel> region in pixelSamplingStrategy.EnumeratePixelRegions(source))
{
quantizer.AddPaletteColors(region);
}
}
}

10
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -61,7 +61,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
if (matrix.Equals(Matrix3x2.Identity))
{
// The clone will be blank here copy all the pixel data over
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds);
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++)
@ -79,13 +79,13 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
{
NNAffineOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
matrix);
ParallelRowIterator.IterateRows(
configuration,
destination.Bounds(),
destination.Bounds,
in nnOperation);
return;
@ -94,14 +94,14 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
AffineOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
in sampler,
matrix);
ParallelRowIterator.IterateRowIntervals<AffineOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
destination.Bounds,
in operation);
}

4
src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs

@ -72,10 +72,10 @@ internal class FlipProcessor<TPixel> : ImageProcessor<TPixel>
/// <param name="configuration">The configuration.</param>
private static void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{
var operation = new RowOperation(source.PixelBuffer);
RowOperation operation = new(source.PixelBuffer);
ParallelRowIterator.IterateRows(
configuration,
source.Bounds(),
source.Bounds,
in operation);
}

10
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -61,7 +61,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (matrix.Equals(Matrix4x4.Identity))
{
// The clone will be blank here copy all the pixel data over
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds);
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++)
@ -79,13 +79,13 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
{
NNProjectiveOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
matrix);
ParallelRowIterator.IterateRows(
configuration,
destination.Bounds(),
destination.Bounds,
in nnOperation);
return;
@ -94,14 +94,14 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
ProjectiveOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
in sampler,
matrix);
ParallelRowIterator.IterateRowIntervals<ProjectiveOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
destination.Bounds,
in operation);
}

16
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs

@ -130,40 +130,40 @@ internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
/// <param name="configuration">The configuration.</param>
private static void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
var operation = new Rotate180RowOperation(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
Rotate180RowOperation operation = new(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
ParallelRowIterator.IterateRows(
configuration,
source.Bounds(),
source.Bounds,
in operation);
}
/// <summary>
/// Rotates the image 270 degrees clockwise at the centre point.
/// Rotates the image 270 degrees clockwise at the center point.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
private static void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
Rotate270RowIntervalOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
ParallelRowIterator.IterateRowIntervals(
configuration,
source.Bounds(),
source.Bounds,
in operation);
}
/// <summary>
/// Rotates the image 90 degrees clockwise at the centre point.
/// Rotates the image 90 degrees clockwise at the center point.
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param>
private static void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration)
{
var operation = new Rotate90RowOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
Rotate90RowOperation operation = new(destination.Bounds, source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer);
ParallelRowIterator.IterateRows(
configuration,
source.Bounds(),
source.Bounds,
in operation);
}

3
tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs

@ -4,6 +4,7 @@
using BenchmarkDotNet.Attributes;
using ImageMagick;
using ImageMagick.Formats;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests;
@ -102,7 +103,7 @@ public class EncodeWebp
Quality = 75,
// This is equal to exact = false in libwebp, which is the default.
TransparentColorMode = WebpTransparentColorMode.Clear
TransparentColorMode = TransparentColorMode.Clear
});
}

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]);
}
}
});
}
}

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

@ -340,13 +340,13 @@ 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 = PngTransparentColorMode.Clear,
TransparentColorMode = TransparentColorMode.Clear,
ColorType = colorType
};
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();

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);
}
}

2
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -311,7 +311,7 @@ public class WebpEncoderTests
{
FileFormat = WebpFileFormatType.Lossless,
Method = method,
TransparentColorMode = WebpTransparentColorMode.Preserve
TransparentColorMode = TransparentColorMode.Preserve
};
using Image<TPixel> image = provider.GetImage();

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)));
}
}
}

1
tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

84
tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

@ -13,10 +13,10 @@ public class QuantizedImageTests
[Fact]
public void QuantizersDitherByDefault()
{
var werner = new WernerPaletteQuantizer();
var webSafe = new WebSafePaletteQuantizer();
var octree = new OctreeQuantizer();
var wu = new WuQuantizer();
WernerPaletteQuantizer werner = new();
WebSafePaletteQuantizer webSafe = new();
OctreeQuantizer octree = new();
WuQuantizer wu = new();
Assert.NotNull(werner.Options.Dither);
Assert.NotNull(webSafe.Options.Dither);
@ -52,27 +52,23 @@ public class QuantizedImageTests
bool dither)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
Assert.True(image[0, 0].Equals(default));
using Image<TPixel> image = provider.GetImage();
Assert.True(image[0, 0].Equals(default));
var options = new QuantizerOptions();
if (!dither)
{
options.Dither = null;
}
QuantizerOptions options = new();
if (!dither)
{
options.Dither = null;
}
var quantizer = new OctreeQuantizer(options);
OctreeQuantizer quantizer = new(options);
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using (IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration))
using (IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]);
}
}
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]);
}
}
@ -82,27 +78,23 @@ public class QuantizedImageTests
public void WuQuantizerYieldsCorrectTransparentPixel<TPixel>(TestImageProvider<TPixel> provider, bool dither)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
Assert.True(image[0, 0].Equals(default));
using Image<TPixel> image = provider.GetImage();
Assert.True(image[0, 0].Equals(default));
var options = new QuantizerOptions();
if (!dither)
{
options.Dither = null;
}
QuantizerOptions options = new();
if (!dither)
{
options.Dither = null;
}
var quantizer = new WuQuantizer(options);
WuQuantizer quantizer = new(options);
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using (IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration))
using (IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]);
}
}
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.Configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]);
}
}
@ -112,13 +104,11 @@ public class QuantizedImageTests
public void Issue1505<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var octreeQuantizer = new OctreeQuantizer();
IQuantizer<TPixel> quantizer = octreeQuantizer.CreatePixelSpecificQuantizer<TPixel>(Configuration.Default, new QuantizerOptions() { MaxColors = 128 });
ImageFrame<TPixel> frame = image.Frames[0];
quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
}
using Image<TPixel> image = provider.GetImage();
OctreeQuantizer octreeQuantizer = new();
IQuantizer<TPixel> quantizer = octreeQuantizer.CreatePixelSpecificQuantizer<TPixel>(Configuration.Default, new QuantizerOptions() { MaxColors = 128 });
ImageFrame<TPixel> frame = image.Frames[0];
quantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
}
private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)

116
tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

@ -12,13 +12,13 @@ public class WuQuantizerTests
public void SinglePixelOpaque()
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
using var image = new Image<Rgba32>(config, 1, 1, Color.Black.ToPixel<Rgba32>());
using Image<Rgba32> image = new(config, 1, 1, Color.Black.ToPixel<Rgba32>());
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config);
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.Width);
@ -32,13 +32,13 @@ public class WuQuantizerTests
public void SinglePixelTransparent()
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
using var image = new Image<Rgba32>(config, 1, 1, default(Rgba32));
using Image<Rgba32> image = new(config, 1, 1, default(Rgba32));
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config);
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.Width);
@ -66,7 +66,7 @@ public class WuQuantizerTests
[Fact]
public void Palette256()
{
using var image = new Image<Rgba32>(1, 256);
using Image<Rgba32> image = new(1, 256);
for (int i = 0; i < 256; i++)
{
@ -79,18 +79,18 @@ public class WuQuantizerTests
}
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config);
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
Assert.Equal(256, result.Palette.Length);
Assert.Equal(1, result.Width);
Assert.Equal(256, result.Height);
using var actualImage = new Image<Rgba32>(1, 256);
using Image<Rgba32> actualImage = new(1, 256);
actualImage.ProcessPixelRows(accessor =>
{
@ -123,72 +123,68 @@ public class WuQuantizerTests
where TPixel : unmanaged, IPixel<TPixel>
{
// See https://github.com/SixLabors/ImageSharp/issues/866
using (Image<TPixel> image = provider.GetImage())
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
ImageFrame<TPixel> frame = image.Frames.RootFrame;
using Image<TPixel> image = provider.GetImage();
Configuration config = Configuration.Default;
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
ImageFrame<TPixel> frame = image.Frames.RootFrame;
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(config);
using IndexedImageFrame<TPixel> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds());
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(config);
using IndexedImageFrame<TPixel> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds);
Assert.Equal(48, result.Palette.Length);
}
Assert.Equal(48, result.Palette.Length);
}
private static void TestScale(Func<byte, Rgba32> pixelBuilder)
{
using (var image = new Image<Rgba32>(1, 256))
using (var expectedImage = new Image<Rgba32>(1, 256))
using (var actualImage = new Image<Rgba32>(1, 256))
using Image<Rgba32> image = new(1, 256);
using Image<Rgba32> expectedImage = new(1, 256);
using Image<Rgba32> actualImage = new(1, 256);
for (int i = 0; i < 256; i++)
{
for (int i = 0; i < 256; i++)
{
byte c = (byte)i;
image[0, i] = pixelBuilder.Invoke(c);
}
byte c = (byte)i;
image[0, i] = pixelBuilder.Invoke(c);
}
for (int i = 0; i < 256; i++)
{
byte c = (byte)((i & ~7) + 4);
expectedImage[0, i] = pixelBuilder.Invoke(c);
}
for (int i = 0; i < 256; i++)
{
byte c = (byte)((i & ~7) + 4);
expectedImage[0, i] = pixelBuilder.Invoke(c);
}
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
Configuration config = Configuration.Default;
WuQuantizer quantizer = new(new QuantizerOptions { Dither = null });
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config))
using (IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()))
{
Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(1, result.Width);
Assert.Equal(256, result.Height);
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IQuantizer<Rgba32> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<Rgba32>(config))
using (IndexedImageFrame<Rgba32> result = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds))
{
Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(1, result.Width);
Assert.Equal(256, result.Height);
actualImage.ProcessPixelRows(accessor =>
actualImage.ProcessPixelRows(accessor =>
{
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < accessor.Height; y++)
{
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < accessor.Height; y++)
{
Span<Rgba32> row = accessor.GetRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.DangerousGetRowSpan(y);
Span<Rgba32> row = accessor.GetRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.DangerousGetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])];
}
for (int x = 0; x < accessor.Width; x++)
{
row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])];
}
});
}
expectedImage.ProcessPixelRows(actualImage, static (expectedAccessor, actualAccessor) =>
{
for (int y = 0; y < expectedAccessor.Height; y++)
{
Assert.True(expectedAccessor.GetRowSpan(y).SequenceEqual(actualAccessor.GetRowSpan(y)));
}
});
}
expectedImage.ProcessPixelRows(actualImage, static (expectedAccessor, actualAccessor) =>
{
for (int y = 0; y < expectedAccessor.Height; y++)
{
Assert.True(expectedAccessor.GetRowSpan(y).SequenceEqual(actualAccessor.GetRowSpan(y)));
}
});
}
}

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