Browse Source

Normalize handling of transparent pixels with color components on encode.

pull/2844/head
James Jackson-South 1 year ago
parent
commit
8dd4f35d5e
  1. 15
      src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
  2. 227
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  3. 97
      src/ImageSharp/Formats/EncodingUtilities.cs
  4. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  5. 115
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  6. 2
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  7. 2
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  8. 4
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  9. 6
      src/ImageSharp/Formats/Png/PngEncoder.cs
  10. 168
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  11. 21
      src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
  12. 2
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  13. 219
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  14. 2
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  15. 32
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  16. 48
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  17. 22
      src/ImageSharp/Formats/TransparentColorMode.cs
  18. 2
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  19. 12
      src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
  20. 4
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  21. 2
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  22. 7
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  23. 16
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  24. 20
      src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs
  25. 2
      src/ImageSharp/ImageFrame.cs
  26. 2
      src/ImageSharp/ImageFrame{TPixel}.cs
  27. 62
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  28. 2
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  29. 7
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  30. 12
      src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
  31. 5
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  32. 19
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  33. 10
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  34. 22
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  35. 2
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  36. 2
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  37. 2
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  38. 2
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  39. 2
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
  40. 2
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  41. 8
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs
  42. 4
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  43. 2
      src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs
  44. 8
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  45. 4
      src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs
  46. 29
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
  47. 10
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  48. 4
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs
  49. 4
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  50. 4
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  51. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  52. 71
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  53. 10
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  54. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  55. 10
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  56. 16
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  57. 3
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  58. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  59. 2
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  60. 1
      tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs
  61. 84
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  62. 116
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.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> /// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy; private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The transparent color mode.
/// </summary>
private readonly TransparentColorMode transparentColorMode;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/> /// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask; private readonly bool processedAlphaMask;
@ -113,6 +118,7 @@ internal sealed class BmpEncoderCore
// TODO: Use a palette quantizer if supplied. // TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask; this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader; this.skipFileHeader = encoder.SkipFileHeader;
@ -181,14 +187,14 @@ internal sealed class BmpEncoderCore
Span<byte> buffer = stackalloc byte[infoHeaderSize]; Span<byte> buffer = stackalloc byte[infoHeaderSize];
// for ico/cur encoder. // For ico/cur encoder.
if (!this.skipFileHeader) if (!this.skipFileHeader)
{ {
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
} }
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image); this.WriteImage(configuration, stream, image, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition); WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush(); stream.Flush();
@ -345,44 +351,65 @@ internal sealed class BmpEncoderCore
/// <param name="image"> /// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data. /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param> /// </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> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer; ImageFrame<TPixel>? clonedFrame = null;
switch (this.bitsPerPixel) try
{ {
case BmpBitsPerPixel.Bit32: if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
this.Write32BitPixelData(configuration, stream, pixels); {
break; clonedFrame = image.Frames.RootFrame.Clone();
EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
}
case BmpBitsPerPixel.Bit24: ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
this.Write24BitPixelData(configuration, stream, pixels); Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
break;
case BmpBitsPerPixel.Bit16: switch (this.bitsPerPixel)
this.Write16BitPixelData(configuration, stream, pixels); {
break; case BmpBitsPerPixel.Bit32:
this.Write32BitPixelData(configuration, stream, pixels, cancellationToken);
break;
case BmpBitsPerPixel.Bit8: case BmpBitsPerPixel.Bit24:
this.Write8BitPixelData(configuration, stream, image); this.Write24BitPixelData(configuration, stream, pixels, cancellationToken);
break; break;
case BmpBitsPerPixel.Bit4: case BmpBitsPerPixel.Bit16:
this.Write4BitPixelData(configuration, stream, image); this.Write16BitPixelData(configuration, stream, pixels, cancellationToken);
break; break;
case BmpBitsPerPixel.Bit2: case BmpBitsPerPixel.Bit8:
this.Write2BitPixelData(configuration, stream, image); this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break; break;
case BmpBitsPerPixel.Bit1: case BmpBitsPerPixel.Bit4:
this.Write1BitPixelData(configuration, stream, image); this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken);
break; 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="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4); 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--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes( PixelOperations<TPixel>.Instance.ToBgra32Bytes(
configuration, configuration,
@ -421,7 +455,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = pixels.Width; int width = pixels.Width;
@ -431,6 +470,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes( PixelOperations<TPixel>.Instance.ToBgr24Bytes(
configuration, configuration,
@ -448,7 +489,12 @@ internal sealed class BmpEncoderCore
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = pixels.Width; int width = pixels.Width;
@ -458,6 +504,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--) for (int y = pixels.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes( PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
@ -476,21 +524,32 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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>
private void Write8BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image) /// <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> 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
if (isL8) if (is8BitLuminance)
{ {
this.Write8BitPixelData(stream, image, colorPalette); this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken);
} }
else 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> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</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> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration); using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
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);
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette); 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); ReadOnlySpan<byte> pixelSpan = quantized.DangerousGetRowSpan(y);
stream.Write(pixelSpan); stream.Write(pixelSpan);
@ -530,9 +597,14 @@ internal sealed class BmpEncoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Create a color palette with 256 different gray values. // Create a color palette with 256 different gray values.
@ -549,9 +621,11 @@ internal sealed class BmpEncoderCore
} }
stream.Write(colorPalette); stream.Write(colorPalette);
Buffer2D<TPixel> imageBuffer = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> imageBuffer = encodingFrame.PixelBuffer;
for (int y = image.Height - 1; y >= 0; y--) for (int y = encodingFrame.Height - 1; y >= 0; y--)
{ {
cancellationToken.ThrowIfCancellationRequested();
ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y); ReadOnlySpan<TPixel> inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow); ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
stream.Write(outputPixelRow); stream.Write(outputPixelRow);
@ -569,8 +643,13 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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>
private void Write4BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image) /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -580,9 +659,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -591,8 +670,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; 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); pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1; 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> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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>
private void Write2BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image) /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -630,9 +716,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -641,8 +727,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding; 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); pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4; 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> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <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>
private void Write1BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image) /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
@ -689,9 +782,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale 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); using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
@ -700,8 +793,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; 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); quantizedPixelRow = quantized.DangerousGetRowSpan(y);
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8; int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
@ -766,10 +861,10 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices); 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int arrayWidth = image.Width / 8; int arrayWidth = encodingFrame.Width / 8;
int padding = arrayWidth % 4; int padding = arrayWidth % 4;
if (padding is not 0) if (padding is not 0)
{ {
@ -777,10 +872,10 @@ internal sealed class BmpEncoderCore
} }
Span<byte> mask = stackalloc byte[arrayWidth]; 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(); mask.Clear();
Span<TPixel> row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); Span<TPixel> row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int i = 0; i < arrayWidth; i++) for (int i = 0; i < arrayWidth; i++)
{ {

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; 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); Buffer2DRegion<TPixel> pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear(); pixelRegion.Clear();

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

@ -67,6 +67,8 @@ internal sealed class GifEncoderCore
/// </summary> /// </summary>
private readonly ushort? repeatCount; private readonly ushort? repeatCount;
private readonly TransparentColorMode transparentColorMode;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
@ -83,6 +85,7 @@ internal sealed class GifEncoderCore
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor; this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount; this.repeatCount = encoder.RepeatCount;
this.transparentColorMode = encoder.TransparentColorMode;
} }
/// <summary> /// <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)) 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) if (useGlobalTable)
{ {
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(configuration, mode, strategy, image);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
} }
else else
{ {
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame); frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
} }
clonedFrame?.Dispose();
} }
// Write the header. // Write the header.
@ -236,52 +261,49 @@ internal sealed class GifEncoderCore
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size); 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) cancellationToken.ThrowIfCancellationRequested();
{
paletteQuantizer.Dispose();
}
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. if (!useLocal && !hasPaletteQuantizer && i > 0)
ImageFrame<TPixel> currentFrame = image.Frames[i]; {
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); // This allows a reduction of memory usage across multi-frame gifs using a global palette
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); // 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) this.EncodeAdditionalFrame(
{ stream,
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. previousFrame,
// This allows a reduction of memory usage across multi-frame gifs using a global palette currentFrame,
// and also allows use to reuse the cache from previous runs. nextFrame,
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1; encodingFrame,
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); useLocal,
hasPaletteQuantizer = true; gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
} }
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
} }
finally
if (hasPaletteQuantizer)
{ {
paletteQuantizer.Dispose(); if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
} }
} }
@ -324,7 +346,9 @@ internal sealed class GifEncoderCore
// We use it to determine the value to use to replace duplicate pixels. // We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1; 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 Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent ? this.backgroundColor ?? Color.Transparent
@ -341,6 +365,11 @@ internal sealed class GifEncoderCore
background, background,
true); true);
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame, encodingFrame,
bounds, bounds,

2
src/ImageSharp/Formats/IAnimatedImageEncoder.cs

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

2
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

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

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

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

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

@ -68,12 +68,6 @@ public class PngEncoder : QuantizingAnimatedImageEncoder
/// </summary> /// </summary>
public PngChunkFilter? ChunkFilter { get; init; } 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/> /// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{ {

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

@ -7,6 +7,7 @@ using System.IO.Hashing;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -188,18 +189,18 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame; ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0; int currentFrameIndex = 0;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear; bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode);
if (clearTransparency) if (clearTransparency)
{ {
currentFrame = clonedFrame = currentFrame.Clone(); 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. // Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth( IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata, pngMetadata,
currentFrame, currentFrame,
currentFrame.Bounds(), currentFrame.Bounds,
null); null);
this.WriteHeaderChunk(stream); this.WriteHeaderChunk(stream);
@ -230,86 +231,88 @@ internal sealed class PngEncoderCore : IDisposable
currentFrameIndex++; currentFrameIndex++;
} }
if (image.Frames.Count > 1) try
{ {
// Write the first animated frame. if (image.Frames.Count > 1)
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); // Write the first animated frame.
} currentFrame = image.Frames[currentFrameIndex];
else PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
{ FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true); FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
} uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
currentFrameIndex++; {
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. currentFrameIndex++;
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames. // Capture the global palette for reuse on subsequent frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// This frame is reused to store de-duplicated pixel buffers. // Write following frames.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size); ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) // This frame is reused to store de-duplicated pixel buffers.
{ using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
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)
{ {
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. // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber); frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Dispose of previous quantized frame and reassign. // Dispose of previous quantized frame and reassign.
quantized?.Dispose(); quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette); quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1; sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame; previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode; previousDisposal = frameMetadata.DisposalMode;
}
} }
}
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
}
// Dispose of allocations from final frame. finally
clonedFrame?.Dispose(); {
quantized?.Dispose(); // Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -319,33 +322,6 @@ internal sealed class PngEncoderCore : IDisposable
this.currentScanline?.Dispose(); 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> /// <summary>
/// Creates the quantized image and calculates and sets the bit depth. /// Creates the quantized image and calculates and sets the bit depth.
/// </summary> /// </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> /// <summary>
/// Image encoder for writing an image to a stream as a QOI image /// Image encoder for writing an image to a stream as a QOI image
/// </summary> /// </summary>
public class QoiEncoder : ImageEncoder public class QoiEncoder : AlphaAwareImageEncoder
{ {
/// <summary> /// <summary>
/// Gets the color channels on the image that can be /// 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)); Guard.NotNull(stream, nameof(stream));
this.WriteHeader(image, stream); this.WriteHeader(image, stream);
this.WritePixels(image, stream); this.WritePixels(image, stream, cancellationToken);
WriteEndOfStream(stream); WriteEndOfStream(stream);
stream.Flush(); stream.Flush();
} }
@ -78,7 +78,7 @@ internal class QoiEncoderCore
stream.WriteByte((byte)qoiColorSpace); 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Start image encoding // Start image encoding
@ -86,137 +86,156 @@ internal class QoiEncoderCore
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255); Rgba32 previousPixel = new(0, 0, 0, 255);
Rgba32 currentRgba32 = default; 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); if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode))
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
for (int j = 0; j < row.Length && i < pixels.Height; j++)
{ {
// We get the RGBA value from pixels clonedFrame = image.Frames.RootFrame.Clone();
currentRgba32 = rgbaRow[j]; 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 Span<TPixel> row = pixels.DangerousGetRowSpan(i);
// If so, we do a QOI_OP_RUN PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
if (currentRgba32.Equals(previousPixel)) for (int j = 0; j < row.Length && i < pixels.Height; j++)
{ {
/* It looks like this isn't an error, but this makes possible that // We get the RGBA value from pixels
* files start with a QOI_OP_RUN if their first pixel is a fully opaque currentRgba32 = rgbaRow[j];
* black. However, the decoder of this project takes that into consideration
* // First, we check if the current pixel is equal to the previous one
* To further details, see https://github.com/phoboslab/qoi/issues/258, // If so, we do a QOI_OP_RUN
* and we should discuss what to do about this approach and if (currentRgba32.Equals(previousPixel))
* if it's correct
*/
int repetitions = 0;
do
{ {
repetitions++; /* It looks like this isn't an error, but this makes possible that
j++; * files start with a QOI_OP_RUN if their first pixel is a fully opaque
if (j == row.Length) * 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; repetitions++;
i++; j++;
if (i == pixels.Height) 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); currentRgba32 = rgbaRow[j];
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
} }
while (currentRgba32.Equals(previousPixel) && repetitions < 62);
currentRgba32 = rgbaRow[j]; j--;
} stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
while (currentRgba32.Equals(previousPixel) && repetitions < 62);
j--;
stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
/* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since /* 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 * it will be taken and compared on the next iteration
*/ */
continue; continue;
} }
// else, we check if it exists in the previously seen pixels // else, we check if it exists in the previously seen pixels
// If so, we do a QOI_OP_INDEX // If so, we do a QOI_OP_INDEX
int pixelArrayPosition = GetArrayPosition(currentRgba32); int pixelArrayPosition = GetArrayPosition(currentRgba32);
if (previouslySeenPixels[pixelArrayPosition].Equals(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)
{ {
// Bottom limit is -2, so we add 2 to make it equal to 0 stream.WriteByte((byte)pixelArrayPosition);
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
{ {
// else, we check if the green difference is less than -32..31 and the rest -8..7 // else, we check if the difference is less than -2..1
// If so, we do a QOI_OP_LUMA // Since it wasn't found on the previously seen pixels, we save it
int diffRedGreen = diffRed - diffGreen; previouslySeenPixels[pixelArrayPosition] = currentRgba32;
int diffBlueGreen = diffBlue - diffGreen;
if (diffGreen is >= -32 and <= 31 && int diffRed = currentRgba32.R - previousPixel.R;
diffRedGreen is >= -8 and <= 7 && int diffGreen = currentRgba32.G - previousPixel.G;
diffBlueGreen is >= -8 and <= 7 && 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) currentRgba32.A == previousPixel.A)
{ {
int dr_dg = diffRedGreen + 8; // Bottom limit is -2, so we add 2 to make it equal to 0
int db_dg = diffBlueGreen + 8; int dr = diffRed + 2;
byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32)); int dg = diffGreen + 2;
byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg); int db = diffBlue + 2;
stream.WriteByte(byteToWrite1); byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
stream.WriteByte(byteToWrite2); stream.WriteByte(valueToWrite);
} }
else else
{ {
// else, we check if the alpha is equal to the previous pixel // else, we check if the green difference is less than -32..31 and the rest -8..7
// If so, we do a QOI_OP_RGB // If so, we do a QOI_OP_LUMA
if (currentRgba32.A == previousPixel.A) 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); int dr_dg = diffRedGreen + 8;
stream.WriteByte(currentRgba32.R); int db_dg = diffBlueGreen + 8;
stream.WriteByte(currentRgba32.G); byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
stream.WriteByte(currentRgba32.B); byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
stream.WriteByte(byteToWrite1);
stream.WriteByte(byteToWrite2);
} }
else else
{ {
// else, we do a QOI_OP_RGBA // else, we check if the alpha is equal to the previous pixel
stream.WriteByte((byte)QoiChunk.QoiOpRgba); // If so, we do a QOI_OP_RGB
stream.WriteByte(currentRgba32.R); if (currentRgba32.A == previousPixel.A)
stream.WriteByte(currentRgba32.G); {
stream.WriteByte(currentRgba32.B); stream.WriteByte((byte)QoiChunk.QoiOpRgb);
stream.WriteByte(currentRgba32.A); 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) private static void WriteEndOfStream(Stream stream)

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

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

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

@ -29,6 +29,8 @@ internal sealed class TgaEncoderCore
/// </summary> /// </summary>
private readonly TgaCompression compression; private readonly TgaCompression compression;
private readonly TransparentColorMode transparentColorMode;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TgaEncoderCore"/> class. /// Initializes a new instance of the <see cref="TgaEncoderCore"/> class.
/// </summary> /// </summary>
@ -39,6 +41,7 @@ internal sealed class TgaEncoderCore
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel; this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = encoder.Compression; this.compression = encoder.Compression;
this.transparentColorMode = encoder.TransparentColorMode;
} }
/// <summary> /// <summary>
@ -103,16 +106,33 @@ internal sealed class TgaEncoderCore
fileHeader.WriteTo(buffer); fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, TgaFileHeader.Size); 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> /// <summary>

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

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

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

@ -47,7 +47,7 @@ internal static unsafe class PredictorEncoder
int[][] bestHisto, int[][] bestHisto,
bool nearLossless, bool nearLossless,
int nearLosslessQuality, int nearLosslessQuality,
WebpTransparentColorMode transparentColorMode, TransparentColorMode transparentColorMode,
bool usedSubtractGreen, bool usedSubtractGreen,
bool lowEffort) bool lowEffort)
{ {
@ -202,7 +202,7 @@ internal static unsafe class PredictorEncoder
int[][] histoArgb, int[][] histoArgb,
int[][] bestHisto, int[][] bestHisto,
int maxQuantization, int maxQuantization,
WebpTransparentColorMode transparentColorMode, TransparentColorMode transparentColorMode,
bool usedSubtractGreen, bool usedSubtractGreen,
bool nearLossless, bool nearLossless,
Span<uint> modes, Span<uint> modes,
@ -340,19 +340,20 @@ internal static unsafe class PredictorEncoder
int xEnd, int xEnd,
int y, int y,
int maxQuantization, int maxQuantization,
WebpTransparentColorMode transparentColorMode, TransparentColorMode transparentColorMode,
bool usedSubtractGreen, bool usedSubtractGreen,
bool nearLossless, bool nearLossless,
Span<uint> output, Span<uint> output,
Span<short> scratch) Span<short> scratch)
{ {
if (transparentColorMode == WebpTransparentColorMode.Preserve) if (transparentColorMode == TransparentColorMode.Preserve)
{ {
PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch); PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch);
} }
else else
{ {
#pragma warning disable SA1503 // Braces should not be omitted #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* currentRow = currentRowSpan)
fixed (uint* upperRow = upperRowSpan) 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 #pragma warning restore SA1503 // Braces should not be omitted
/// <summary> /// <summary>
@ -577,7 +579,7 @@ internal static unsafe class PredictorEncoder
Span<uint> argbScratch, Span<uint> argbScratch,
Span<uint> argb, Span<uint> argb,
int maxQuantization, int maxQuantization,
WebpTransparentColorMode transparentColorMode, TransparentColorMode transparentColorMode,
bool usedSubtractGreen, bool usedSubtractGreen,
bool nearLossless, bool nearLossless,
bool lowEffort) 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 /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression. /// RGB information for better compression.
/// </summary> /// </summary>
private readonly WebpTransparentColorMode transparentColorMode; private readonly TransparentColorMode transparentColorMode;
/// <summary> /// <summary>
/// Whether to skip metadata during encoding. /// Whether to skip metadata during encoding.
@ -114,7 +114,7 @@ internal class Vp8LEncoder : IDisposable
uint quality, uint quality,
bool skipMetadata, bool skipMetadata,
WebpEncodingMethod method, WebpEncodingMethod method,
WebpTransparentColorMode transparentColorMode, TransparentColorMode transparentColorMode,
bool nearLossless, bool nearLossless,
int nearLosslessQuality) int nearLosslessQuality)
{ {

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

@ -343,7 +343,7 @@ internal class WebpAnimationDecoder : IDisposable
return; 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); Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>(); TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
pixelRegion.Fill(backgroundPixel); pixelRegion.Fill(backgroundPixel);

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

@ -58,13 +58,6 @@ public sealed class WebpEncoder : AnimatedImageEncoder
/// </summary> /// </summary>
public int FilterStrength { get; init; } = 60; 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> /// <summary>
/// Gets a value indicating whether near lossless mode should be used. /// 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. /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.

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

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. /// Gets the bounds of the frame.
/// </summary> /// </summary>
/// <returns>The <see cref="Rectangle"/></returns> /// <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 /> /// <inheritdoc />
public void Dispose() public void Dispose()

2
src/ImageSharp/ImageFrame{TPixel}.cs

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

62
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -25,27 +25,65 @@ public static class Buffer2DExtensions
return buffer.FastMemoryGroup.View; 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> /// <summary>
/// TODO: Does not work with multi-buffer groups, should be specific to Resize. /// TODO: Does not work with multi-buffer groups, should be specific to Resize.
/// Copy <paramref name="columnCount"/> columns of <paramref name="buffer"/> inplace, /// Copy <paramref name="columnCount"/> columns of <paramref name="buffer"/> in-place,
/// from positions starting at <paramref name="sourceIndex"/> to positions at <paramref name="destIndex"/>. /// from positions starting at <paramref name="sourceIndex"/> to positions at <paramref name="destinationIndex"/>.
/// </summary> /// </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>( internal static unsafe void DangerousCopyColumns<T>(
this Buffer2D<T> buffer, this Buffer2D<T> buffer,
int sourceIndex, int sourceIndex,
int destIndex, int destinationIndex,
int columnCount) int columnCount)
where T : struct where T : struct
{ {
DebugGuard.NotNull(buffer, nameof(buffer)); DebugGuard.NotNull(buffer, nameof(buffer));
DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex)); DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex));
DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex)); DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex));
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount); CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount);
int elementSize = Unsafe.SizeOf<T>(); int elementSize = Unsafe.SizeOf<T>();
int width = buffer.Width * elementSize; int width = buffer.Width * elementSize;
int sOffset = sourceIndex * elementSize; int sOffset = sourceIndex * elementSize;
int dOffset = destIndex * elementSize; int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize; long count = columnCount * elementSize;
Span<byte> span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span); Span<byte> span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span);
@ -73,9 +111,7 @@ public static class Buffer2DExtensions
/// <returns>The <see cref="Rectangle"/></returns> /// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle FullRectangle<T>(this Buffer2D<T> buffer) internal static Rectangle FullRectangle<T>(this Buffer2D<T> buffer)
where T : struct where T : struct
{ => new(0, 0, buffer.Width, buffer.Height);
return new Rectangle(0, 0, buffer.Width, buffer.Height);
}
/// <summary> /// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the subregion represented by 'rectangle' /// 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> /// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, Rectangle rectangle) internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, Rectangle rectangle)
where T : unmanaged => 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) internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : unmanaged => where T : unmanaged =>
new Buffer2DRegion<T>(buffer, new Rectangle(x, y, width, height)); new(buffer, new Rectangle(x, y, width, height));
/// <summary> /// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the whole area of 'buffer' /// 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> /// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer) internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer)
where T : unmanaged => where T : unmanaged =>
new Buffer2DRegion<T>(buffer); new(buffer);
/// <summary> /// <summary>
/// Returns the size of the buffer. /// Returns the size of the buffer.
@ -115,6 +151,8 @@ public static class Buffer2DExtensions
/// <summary> /// <summary>
/// Gets the bounds of the buffer. /// Gets the bounds of the buffer.
/// </summary> /// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns> /// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle Bounds<T>(this Buffer2D<T> buffer) internal static Rectangle Bounds<T>(this Buffer2D<T> buffer)
where T : struct => where T : struct =>

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

@ -107,7 +107,7 @@ public readonly struct Buffer2DRegion<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion<T> GetSubRegion(int x, int y, int width, int height) 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); return this.GetSubRegion(rectangle);
} }

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

@ -36,8 +36,13 @@ internal static class MemoryGroupExtensions
/// <summary> /// <summary>
/// Returns a slice that is expected to be within the bounds of a single buffer. /// Returns a slice that is expected to be within the bounds of a single buffer.
/// Otherwise <see cref="ArgumentOutOfRangeException"/> is thrown.
/// </summary> /// </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) internal static Memory<T> GetBoundedMemorySlice<T>(this IMemoryGroup<T> group, long start, int length)
where T : struct where T : struct
{ {

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

@ -61,9 +61,7 @@ internal class DefaultImageProcessorContext<TPixel> : IInternalImageProcessingCo
/// <inheritdoc/> /// <inheritdoc/>
public IImageProcessingContext ApplyProcessor(IImageProcessor processor) public IImageProcessingContext ApplyProcessor(IImageProcessor processor)
{ => this.ApplyProcessor(processor, this.GetCurrentBounds());
return this.ApplyProcessor(processor, this.GetCurrentBounds());
}
/// <inheritdoc/> /// <inheritdoc/>
public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle) 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. // interim clone if the first processor in the pipeline is a cloning processor.
if (processor is ICloningImageProcessor cloningImageProcessor) if (processor is ICloningImageProcessor cloningImageProcessor)
{ {
using (ICloningImageProcessor<TPixel> pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle)) using ICloningImageProcessor<TPixel> pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle);
{ this.destination = pixelProcessor.CloneAndExecute();
this.destination = pixelProcessor.CloneAndExecute(); return this;
return this;
}
} }
// Not a cloning processor? We need to create a clone to operate on. // 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. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -42,7 +41,7 @@ public static partial class ProcessingExtensions
/// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns> /// <returns>The <see cref="Buffer2D{T}"/> containing all the sums.</returns>
public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source) public static Buffer2D<ulong> CalculateIntegralImage<TPixel>(this ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> source.CalculateIntegralImage(source.Bounds()); => source.CalculateIntegralImage(source.Bounds);
/// <summary> /// <summary>
/// Apply an image integral. <See href="https://en.wikipedia.org/wiki/Summed-area_table"/> /// 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; Configuration configuration = source.Configuration;
var interest = Rectangle.Intersect(bounds, source.Bounds()); Rectangle interest = Rectangle.Intersect(bounds, source.Bounds);
int startY = interest.Y; int startY = interest.Y;
int startX = interest.X; int startX = interest.X;
int endY = interest.Height; int endY = interest.Height;

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

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization;
/// <summary> /// <summary>
/// Performs Bradley Adaptive Threshold filter against an image. /// Performs Bradley Adaptive Threshold filter against an image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel> internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -30,7 +31,7 @@ internal class AdaptiveThresholdProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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; Configuration configuration = this.Configuration;
TPixel upper = this.definition.Upper.ToPixel<TPixel>(); 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); Span<TPixel> rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
PixelOperations<TPixel>.Instance.ToL8(this.configuration, rowSpan, span); PixelOperations<TPixel>.Instance.ToL8(this.configuration, rowSpan, span);
int startY = this.startY;
int maxX = this.bounds.Width - 1; int maxX = this.bounds.Width - 1;
int maxY = this.bounds.Height - 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++) for (int x = 0; x < rowSpan.Length; x++)
{ {
int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX); int x1 = Math.Clamp(x - clusterSize + 1, 0, maxX);
int x2 = Math.Min(x + this.clusterSize + 1, maxX); int x2 = Math.Min(x + clusterSize + 1, maxX);
int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY); int y1 = Math.Clamp(y - startY - clusterSize + 1, 0, maxY);
int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY); int y2 = Math.Min(y - startY + clusterSize + 1, maxY);
uint count = (uint)((x2 - x1) * (y2 - y1)); 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; 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; Rectangle sourceRectangle = this.SourceRectangle;
Configuration configuration = this.Configuration; Configuration configuration = this.Configuration;
var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds);
var operation = new RowOperation( RowOperation operation = new(
interest.X, interest.X,
source.PixelBuffer, source.PixelBuffer,
upper, upper,
@ -169,10 +169,8 @@ internal class BinaryThresholdProcessor<TPixel> : ImageProcessor<TPixel>
{ {
return chroma / (max + min); return chroma / (max + min);
} }
else
{ return chroma / (2F - max - min);
return chroma / (2F - max - min);
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

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

@ -75,12 +75,12 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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 // Preliminary gamma highlight pass
if (this.gamma == 3F) 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>( ParallelRowIterator.IterateRows<ApplyGamma3ExposureRowOperation, Vector4>(
this.Configuration, this.Configuration,
sourceRectangle, sourceRectangle,
@ -88,7 +88,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
} }
else 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>( ParallelRowIterator.IterateRows<ApplyGammaExposureRowOperation, Vector4>(
this.Configuration, this.Configuration,
sourceRectangle, sourceRectangle,
@ -104,7 +104,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
// Apply the inverse gamma exposure pass, and write the final pixel data // Apply the inverse gamma exposure pass, and write the final pixel data
if (this.gamma == 3F) 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( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
sourceRectangle, sourceRectangle,
@ -112,7 +112,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
} }
else 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( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
sourceRectangle, 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 // 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 // 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. // 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); 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); Vector4 parameters = Unsafe.Add(ref paramsRef, (uint)i);
// Horizontal convolution // Horizontal convolution
var horizontalOperation = new FirstPassConvolutionRowOperation( FirstPassConvolutionRowOperation horizontalOperation = new(
sourceRectangle, sourceRectangle,
firstPassBuffer, firstPassBuffer,
source.PixelBuffer, source.PixelBuffer,
@ -175,7 +175,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
in horizontalOperation); in horizontalOperation);
// Vertical 1D convolutions to accumulate the partial results on the target buffer // Vertical 1D convolutions to accumulate the partial results on the target buffer
var verticalOperation = new BokehBlurProcessor.SecondPassConvolutionRowOperation( BokehBlurProcessor.SecondPassConvolutionRowOperation verticalOperation = new(
sourceRectangle, sourceRectangle,
processingBuffer, processingBuffer,
firstPassBuffer, firstPassBuffer,
@ -342,9 +342,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public int GetRequiredBufferLength(Rectangle bounds) public int GetRequiredBufferLength(Rectangle bounds)
{ => bounds.Width;
return bounds.Width;
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
@ -391,7 +389,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
public void Invoke(int y) public void Invoke(int y)
{ {
Vector4 low = Vector4.Zero; 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<TPixel> targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y)[this.bounds.X..];
Span<Vector4> sourceRowSpan = this.sourceValues.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); source.CopyTo(targetPixels);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (KernelSamplingMap map = new(allocator)) 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); 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 // 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. // 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); source.CopyTo(targetPixels);
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (KernelSamplingMap map = new(allocator)) 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 /> /// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source) 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 // We need a clean copy for each pass to start from
using ImageFrame<TPixel> cleanCopy = source.Clone(); 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); 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); using KernelSamplingMap map = new(this.Configuration.MemoryAllocator);
map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); 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/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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); 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 /> /// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle) public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
return new PixelRowDelegateProcessor<TPixel, PixelRowDelegate>(
new PixelRowDelegate(this.PixelRowOperation), new PixelRowDelegate(this.PixelRowOperation),
configuration, configuration,
this.Modifiers, this.Modifiers,
source, source,
sourceRectangle); sourceRectangle);
}
/// <summary> /// <summary>
/// A <see langword="struct"/> implementing the row processing logic for <see cref="PixelRowDelegateProcessor"/>. /// 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)] [MethodImpl(InliningOptions.ShortMethod)]
public PixelRowDelegate(PixelRowOperation pixelRowOperation) public PixelRowDelegate(PixelRowOperation pixelRowOperation)
{ => this.pixelRowOperation = pixelRowOperation;
this.pixelRowOperation = pixelRowOperation;
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [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/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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 RowOperation(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate); RowOperation operation = new(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate);
ParallelRowIterator.IterateRows<RowOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,

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

@ -32,7 +32,7 @@ internal class PixelateProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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; int size = this.Size;
Guard.MustBeBetweenOrEqualTo(size, 0, interest.Width, nameof(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> /// <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) public FilterProcessor(Configuration configuration, FilterProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ => this.definition = definition;
this.definition = definition;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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 RowOperation(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration); RowOperation operation = new(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration);
ParallelRowIterator.IterateRows<RowOperation, Vector4>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, 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) 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); 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>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</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="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="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="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( public AutoLevelProcessor(
Configuration configuration, Configuration configuration,
int luminanceLevels, int luminanceLevels,
@ -40,9 +40,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
Image<TPixel> source, Image<TPixel> source,
Rectangle sourceRectangle) Rectangle sourceRectangle)
: base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle)
{ => this.SyncChannels = syncChannels;
this.SyncChannels = syncChannels;
}
/// <summary> /// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel. /// 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; MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height; 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); using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels. // 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>( ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -83,7 +81,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
if (this.SyncChannels) 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>( ParallelRowIterator.IterateRows<SynchronizedChannelsRowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -91,7 +89,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
} }
else 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>( ParallelRowIterator.IterateRows<SeperateChannelsRowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -136,10 +134,10 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span) 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 Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); 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; int levels = this.luminanceLevels;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
@ -148,12 +146,11 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
for (int x = 0; x < this.bounds.Width; x++) 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); int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; float scaledLuminance = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin;
float scalingFactor = scaledLuminance * levels / luminance; 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) = new(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W);
Unsafe.Add(ref vectorRef, (uint)x) = scaledVector;
} }
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow); PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, vectorBuffer, pixelRow);
@ -197,10 +194,10 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span) 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 Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); 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; int levelsMinusOne = this.luminanceLevels - 1;
float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin;
@ -209,7 +206,7 @@ internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixe
for (int x = 0; x < this.bounds.Width; x++) 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); uint originalX = (uint)MathF.Round(vector.X);
float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; 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; MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height; 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); using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels. // 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>( ParallelRowIterator.IterateRows<GrayscaleLevelsRowOperation<TPixel>, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -74,7 +74,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
// Apply the cdf to each pixel of the image // 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>( ParallelRowIterator.IterateRows<CdfApplicationRowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -118,7 +118,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span) 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 Vector4 vectorRef = ref MemoryMarshal.GetReference(vectorBuffer);
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
int levels = this.luminanceLevels; int levels = this.luminanceLevels;
@ -129,7 +129,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
for (int x = 0; x < this.bounds.Width; x++) 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); int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels);
float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin; float luminanceEqualized = Unsafe.Add(ref cdfBase, (uint)luminance) / noOfPixelsMinusCdfMin;
Unsafe.Add(ref vectorRef, (uint)x) = new Vector4(luminanceEqualized, luminanceEqualized, luminanceEqualized, vector.W); 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>(); TPixel color = this.definition.Color.ToPixel<TPixel>();
GraphicsOptions graphicsOptions = this.definition.GraphicsOptions; 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; Configuration configuration = this.Configuration;
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
@ -48,7 +48,7 @@ internal class BackgroundColorProcessor<TPixel> : ImageProcessor<TPixel>
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions); 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( ParallelRowIterator.IterateRows(
configuration, configuration,
interest, 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>(); TPixel glowColor = this.definition.GlowColor.ToPixel<TPixel>();
float blendPercent = this.definition.GraphicsOptions.BlendPercentage; 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); Vector2 center = Rectangle.Center(interest);
float finalRadius = this.definition.Radius.Calculate(interest.Size); 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); using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(glowColor); 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>( ParallelRowIterator.IterateRows<RowOperation, float>(
configuration, configuration,
interest, 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>(); TPixel vignetteColor = this.definition.VignetteColor.ToPixel<TPixel>();
float blendPercent = this.definition.GraphicsOptions.BlendPercentage; 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); Vector2 center = Rectangle.Center(interest);
float finalRadiusX = this.definition.RadiusX.Calculate(interest.Size); 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); using IMemoryOwner<TPixel> rowColors = allocator.Allocate<TPixel>(interest.Width);
rowColors.GetSpan().Fill(vignetteColor); 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>( ParallelRowIterator.IterateRows<RowOperation, float>(
configuration, configuration,
interest, interest,

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

@ -32,7 +32,7 @@ internal class QuantizeProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc /> /// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source) 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; Configuration configuration = this.Configuration;
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(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. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -50,7 +51,7 @@ public static class QuantizerUtilities
Guard.NotNull(quantizer, nameof(quantizer)); Guard.NotNull(quantizer, nameof(quantizer));
Guard.NotNull(source, nameof(source)); 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); Buffer2DRegion<TPixel> region = source.PixelBuffer.GetRegion(interest);
// Collect the palette. Required before the second pass runs. // Collect the palette. Required before the second pass runs.
@ -77,7 +78,7 @@ public static class QuantizerUtilities
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Guard.NotNull(source, nameof(source)); Guard.NotNull(source, nameof(source));
Rectangle interest = Rectangle.Intersect(source.Bounds(), bounds); Rectangle interest = Rectangle.Intersect(source.Bounds, bounds);
IndexedImageFrame<TPixel> destination = new( IndexedImageFrame<TPixel> destination = new(
quantizer.Configuration, quantizer.Configuration,
@ -111,10 +112,39 @@ public static class QuantizerUtilities
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
Image<TPixel> source) Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel> 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, IPixelSamplingStrategy pixelSamplingStrategy,
ImageFrame<TPixel> source) ImageFrame<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel> 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)) if (matrix.Equals(Matrix3x2.Identity))
{ {
// The clone will be blank here copy all the pixel data over // 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> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++) for (int y = 0; y < sourceBuffer.Height; y++)
@ -79,13 +79,13 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
{ {
NNAffineOperation nnOperation = new( NNAffineOperation nnOperation = new(
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer, destination.PixelBuffer,
matrix); matrix);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
configuration, configuration,
destination.Bounds(), destination.Bounds,
in nnOperation); in nnOperation);
return; return;
@ -94,14 +94,14 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
AffineOperation<TResampler> operation = new( AffineOperation<TResampler> operation = new(
configuration, configuration,
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer, destination.PixelBuffer,
in sampler, in sampler,
matrix); matrix);
ParallelRowIterator.IterateRowIntervals<AffineOperation<TResampler>, Vector4>( ParallelRowIterator.IterateRowIntervals<AffineOperation<TResampler>, Vector4>(
configuration, configuration,
destination.Bounds(), destination.Bounds,
in operation); 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> /// <param name="configuration">The configuration.</param>
private static void FlipY(ImageFrame<TPixel> source, Configuration configuration) private static void FlipY(ImageFrame<TPixel> source, Configuration configuration)
{ {
var operation = new RowOperation(source.PixelBuffer); RowOperation operation = new(source.PixelBuffer);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds,
in operation); 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)) if (matrix.Equals(Matrix4x4.Identity))
{ {
// The clone will be blank here copy all the pixel data over // 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> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest); Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++) for (int y = 0; y < sourceBuffer.Height; y++)
@ -79,13 +79,13 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
{ {
NNProjectiveOperation nnOperation = new( NNProjectiveOperation nnOperation = new(
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer, destination.PixelBuffer,
matrix); matrix);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
configuration, configuration,
destination.Bounds(), destination.Bounds,
in nnOperation); in nnOperation);
return; return;
@ -94,14 +94,14 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
ProjectiveOperation<TResampler> operation = new( ProjectiveOperation<TResampler> operation = new(
configuration, configuration,
source.PixelBuffer, source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()), Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer, destination.PixelBuffer,
in sampler, in sampler,
matrix); matrix);
ParallelRowIterator.IterateRowIntervals<ProjectiveOperation<TResampler>, Vector4>( ParallelRowIterator.IterateRowIntervals<ProjectiveOperation<TResampler>, Vector4>(
configuration, configuration,
destination.Bounds(), destination.Bounds,
in operation); 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> /// <param name="configuration">The configuration.</param>
private static void Rotate180(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) 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( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds,
in operation); in operation);
} }
/// <summary> /// <summary>
/// Rotates the image 270 degrees clockwise at the centre point. /// Rotates the image 270 degrees clockwise at the center point.
/// </summary> /// </summary>
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param> /// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private static void Rotate270(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) 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( ParallelRowIterator.IterateRowIntervals(
configuration, configuration,
source.Bounds(), source.Bounds,
in operation); in operation);
} }
/// <summary> /// <summary>
/// Rotates the image 90 degrees clockwise at the centre point. /// Rotates the image 90 degrees clockwise at the center point.
/// </summary> /// </summary>
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="destination">The destination image.</param> /// <param name="destination">The destination image.</param>
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
private static void Rotate90(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Configuration configuration) 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( ParallelRowIterator.IterateRows(
configuration, configuration,
source.Bounds(), source.Bounds,
in operation); in operation);
} }

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

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

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

@ -346,7 +346,7 @@ public partial class PngEncoderTests
Image<Rgba32> image = new(50, 50); Image<Rgba32> image = new(50, 50);
PngEncoder encoder = new() PngEncoder encoder = new()
{ {
TransparentColorMode = PngTransparentColorMode.Clear, TransparentColorMode = TransparentColorMode.Clear,
ColorType = colorType ColorType = colorType
}; };
Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>(); Rgba32 rgba32 = Color.Blue.ToPixel<Rgba32>();

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

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

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

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

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

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

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

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

Loading…
Cancel
Save