Browse Source

Merge branch 'main' into js/resize-map-optimizations

pull/2793/head
James Jackson-South 1 year ago
committed by GitHub
parent
commit
639ce69996
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      .github/workflows/build-and-test.yml
  2. 3
      src/ImageSharp.ruleset
  3. 15
      src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
  4. 227
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  5. 47
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  6. 97
      src/ImageSharp/Formats/EncodingUtilities.cs
  7. 2
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  8. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  9. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  10. 223
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  11. 43
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  12. 50
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  13. 47
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  14. 8
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  15. 2
      src/ImageSharp/Formats/ImageEncoder.cs
  16. 3
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  17. 75
      src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
  18. 22
      src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
  19. 68
      src/ImageSharp/Formats/Pbm/PlainEncoder.cs
  20. 9
      src/ImageSharp/Formats/Png/PngEncoder.cs
  21. 189
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  22. 21
      src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
  23. 2
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  24. 219
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  25. 22
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  26. 2
      src/ImageSharp/Formats/Tga/TgaEncoder.cs
  27. 32
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  28. 50
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  29. 22
      src/ImageSharp/Formats/TransparentColorMode.cs
  30. 7
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  31. 2
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  32. 11
      src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
  33. 12
      src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
  34. 12
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  35. 7
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  36. 12
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  37. 20
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs
  38. 16
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  39. 6
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  40. 17
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  41. 54
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  42. 22
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  43. 20
      src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs
  44. 626
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  45. 11
      src/ImageSharp/Image.Decode.cs
  46. 2
      src/ImageSharp/ImageFrame.cs
  47. 2
      src/ImageSharp/ImageFrame{TPixel}.cs
  48. 5
      src/ImageSharp/ImageSharp.csproj
  49. 4
      src/ImageSharp/Image{TPixel}.cs
  50. 62
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  51. 2
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  52. 7
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  53. 2
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  54. 2
      src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
  55. 6
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  56. 19
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  57. 12
      src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
  58. 8
      src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs
  59. 14
      src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs
  60. 89
      src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs
  61. 124
      src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs
  62. 21
      src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs
  63. 28
      src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs
  64. 17
      src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs
  65. 5
      src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
  66. 19
      src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
  67. 10
      src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
  68. 22
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  69. 6
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  70. 54
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  71. 79
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs
  72. 44
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  73. 8
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  74. 2
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs
  75. 20
      src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs
  76. 18
      src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs
  77. 2
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
  78. 6
      src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs
  79. 2
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  80. 8
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor.cs
  81. 4
      src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs
  82. 2
      src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs
  83. 8
      src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs
  84. 4
      src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs
  85. 29
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
  86. 10
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  87. 4
      src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs
  88. 4
      src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs
  89. 4
      src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs
  90. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  91. 71
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  92. 37
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  93. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  94. 86
      src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs
  95. 33
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  96. 16
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  97. 159
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  98. 40
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  99. 21
      tests/Directory.Build.targets
  100. 64
      tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs

27
.github/workflows/build-and-test.yml

@ -19,6 +19,31 @@ jobs:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options:
- os: ubuntu-latest
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net9.0
sdk: 9.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: net8.0
sdk: 8.0.x
@ -100,7 +125,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}

3
src/ImageSharp.ruleset

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="ImageSharp" ToolsVersion="17.0">
<Include Path="..\shared-infrastructure\sixlabors.ruleset" Action="Default" />
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.NetAnalyzers">
<Rule Id="CA2022" Action="Info" />
</Rules>
</RuleSet>

15
src/ImageSharp/Formats/AlphaAwareImageEncoder.cs

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

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

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

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

@ -48,13 +48,13 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
@ -80,20 +80,6 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};
byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
@ -116,8 +102,8 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
@ -138,8 +124,8 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
@ -156,7 +142,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
this.HotspotY = entry.BitCount;
}
internal IconDirEntry ToIconDirEntry()
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
@ -164,8 +150,8 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
return new()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = this.HotspotX,
BitCount = this.HotspotY,
ColorCount = colorCount
@ -233,13 +219,22 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
private static byte Scale(byte? value, int destination, float ratio)
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
return ClampEncodingDimension(destination);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
// Encoding dimensions can be between 0-256 where 0 means 256 or greater.
> 255 => 0,
<= 255 and >= 1 => (byte)dimension,
_ => 0
};
}

97
src/ImageSharp/Formats/EncodingUtilities.cs

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

2
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -45,7 +45,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.

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

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

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public sealed class GifEncoder : QuantizingImageEncoder
public sealed class GifEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Gets the color table mode: Global or local.

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

@ -54,6 +54,21 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
@ -68,6 +83,9 @@ internal sealed class GifEncoderCore
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.transparentColorMode = encoder.TransparentColorMode;
}
/// <summary>
@ -116,18 +134,40 @@ internal sealed class GifEncoderCore
}
}
// Quantize the first frame. Checking to see whether we can clear the transparent pixels
// to allow for a smaller color palette and encoded result.
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{
ImageFrame<TPixel>? clonedFrame = null;
Configuration configuration = this.configuration;
TransparentColorMode mode = this.transparentColorMode;
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(mode))
{
clonedFrame = image.Frames.RootFrame.Clone();
GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
if (useGlobalTable)
{
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(configuration, mode, strategy, image);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
else
{
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
clonedFrame?.Dispose();
}
// Write the header.
@ -141,9 +181,12 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}
byte backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -161,19 +204,32 @@ internal sealed class GifEncoderCore
// Write application extensions.
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile);
}
this.EncodeFirstFrame(stream, frameMetadata, quantized);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
// If the token is cancelled during encoding of frames we must ensure the
// quantized frame is disposed.
try
{
this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken);
this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
stream.WriteByte(GifConstants.EndIntroducer);
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
}
finally
{
stream.WriteByte(GifConstants.EndIntroducer);
quantized?.Dispose();
quantized?.Dispose();
}
}
private static GifFrameMetadata GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
@ -194,7 +250,8 @@ internal sealed class GifEncoderCore
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode)
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -211,51 +268,61 @@ internal sealed class GifEncoderCore
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++)
try
{
// Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
for (int i = 1; i < image.Frames.Count; i++)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
cancellationToken.ThrowIfCancellationRequested();
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
// 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);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
if (hasPaletteQuantizer)
this.EncodeAdditionalFrame(
stream,
previousFrame,
currentFrame,
nextFrame,
encodingFrame,
useLocal,
gifMetadata,
paletteQuantizer,
previousDisposalMode);
previousFrame = currentFrame;
previousDisposalMode = gifMetadata.DisposalMode;
}
}
finally
{
paletteQuantizer.Dispose();
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
}
}
private void EncodeFirstFrame<TPixel>(
Stream stream,
GifFrameMetadata metadata,
IndexedImageFrame<TPixel> quantized)
IndexedImageFrame<TPixel> quantized,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
cancellationToken.ThrowIfCancellationRequested();
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
@ -289,7 +356,13 @@ internal sealed class GifEncoderCore
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground
? null :
previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
@ -299,9 +372,14 @@ internal sealed class GifEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
true);
if (EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.transparentColorMode))
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,
bounds,
@ -428,14 +506,12 @@ internal sealed class GifEncoderCore
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// Returns the index of the transparent color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
/// <returns>The <see cref="int"/>.</returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -463,6 +539,47 @@ internal sealed class GifEncoderCore
return index;
}
/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="background">The background color to match.</param>
/// <param name="index">The index in the palette of the background color.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="bool"/>.</returns>
private static bool TryGetBackgroundIndex<TPixel>(
IndexedImageFrame<TPixel>? quantized,
Color? background,
out byte index)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null && background.HasValue)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}
match = i;
break;
}
}
if (match >= 0)
{
index = (byte)Numerics.Clamp(match, 0, 255);
return true;
}
index = 0;
return false;
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>

43
src/ImageSharp/Formats/IAnimatedImageEncoder.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Defines the contract for all image encoders that allow encoding animation sequences.
/// </summary>
public interface IAnimatedImageEncoder
{
/// <summary>
/// Gets the default background color of the canvas when animating in supported encoders.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
Color? BackgroundColor { get; }
/// <summary>
/// Gets the number of times any animation is repeated in supported encoders.
/// </summary>
ushort? RepeatCount { get; }
/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
/// </summary>
bool? AnimateRootFrame { get; }
}
/// <summary>
/// Acts as a base class for all image encoders that allow encoding animation sequences.
/// </summary>
public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }
/// <inheritdoc/>
public ushort? RepeatCount { get; init; }
/// <inheritdoc/>
public bool? AnimateRootFrame { get; init; } = true;
}

50
src/ImageSharp/Formats/IQuantizingImageEncoder.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Defines the contract for all image encoders that allow color palette generation via quantization.
/// </summary>
public interface IQuantizingImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
}
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder
{
/// <inheritdoc/>
public IQuantizer? Quantizer { get; init; }
/// <inheritdoc/>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization when
/// encoding animation sequences.
/// </summary>
public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; }
/// <inheritdoc/>
public ushort? RepeatCount { get; }
/// <inheritdoc/>
public bool? AnimateRootFrame { get; }
}

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

@ -41,13 +41,13 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
@ -73,20 +73,6 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
}
byte encodingWidth = metadata.EncodingWidth switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingWidth,
_ => 0
};
byte encodingHeight = metadata.EncodingHeight switch
{
> 255 => 0,
<= 255 and >= 1 => (byte)metadata.EncodingHeight,
_ => 0
};
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
@ -109,8 +95,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = encodingWidth,
EncodingHeight = encodingHeight,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
@ -131,8 +117,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
/// <inheritdoc/>
@ -147,7 +133,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
this.EncodingHeight = entry.Height;
}
internal IconDirEntry ToIconDirEntry()
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
@ -155,8 +141,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
return new()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = 1,
ColorCount = colorCount,
BitCount = this.Compression switch
@ -228,13 +214,22 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
}
private static byte Scale(byte? value, int destination, float ratio)
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
return ClampEncodingDimension(destination);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
// Encoding dimensions can be between 0-256 where 0 means 256 or greater.
> 255 => 0,
<= 255 and >= 1 => (byte)dimension,
_ => 0
};
}

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

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

2
src/ImageSharp/Formats/ImageEncoder.cs

@ -51,7 +51,7 @@ public abstract class ImageEncoder : IImageEncoder
else
{
using ChunkedMemoryStream ms = new(configuration.MemoryAllocator);
this.Encode(image, stream, cancellationToken);
this.Encode(image, ms, cancellationToken);
ms.Position = 0;
ms.CopyTo(stream, configuration.StreamProcessingBufferSize);
}

3
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -16,7 +16,6 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg;
@ -1473,7 +1472,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.Frame.ComponentOrder[i / 2] = (byte)componentIndex;
IJpegComponent component = this.Frame.Components[componentIndex];
JpegComponent component = this.Frame.Components[componentIndex];
// 1 byte: Huffman table selectors.
// 4 bits - dc

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

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

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

@ -68,8 +68,7 @@ internal sealed class PbmEncoderCore
byte signature = this.DeduceSignature();
this.WriteHeader(stream, signature, image.Size);
this.WritePixels(stream, image.Frames.RootFrame);
this.WritePixels(stream, image.Frames.RootFrame, cancellationToken);
stream.Flush();
}
@ -167,16 +166,29 @@ internal sealed class PbmEncoderCore
/// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
private void WritePixels<TPixel>(Stream stream, ImageFrame<TPixel> image)
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WritePixels<TPixel>(Stream stream, ImageFrame<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.encoding == PbmEncoding.Plain)
{
PlainEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType);
PlainEncoder.WritePixels(
this.configuration,
stream,
image,
this.colorType,
this.componentType,
cancellationToken);
}
else
{
BinaryEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType);
BinaryEncoder.WritePixels(
this.configuration,
stream,
image,
this.colorType,
this.componentType,
cancellationToken);
}
}
}

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

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

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

@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public class PngEncoder : QuantizingImageEncoder
public class PngEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
@ -69,12 +68,6 @@ public class PngEncoder : QuantizingImageEncoder
/// </summary>
public PngChunkFilter? ChunkFilter { get; init; }
/// <summary>
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
/// should be converted to transparent black, which can yield in better compression in some cases.
/// </summary>
public PngTransparentColorMode TransparentColorMode { get; init; }
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{

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

@ -7,6 +7,7 @@ using System.IO.Hashing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -123,6 +124,24 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary>
private int derivedTransparencyIndex = -1;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// Whether the root frame is shown as part of the animated sequence.
/// </summary>
private readonly bool? animateRootFrame;
/// <summary>
/// A reusable Crc32 hashing instance.
/// </summary>
@ -139,6 +158,9 @@ internal sealed class PngEncoderCore : IDisposable
this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder;
this.quantizer = encoder.Quantizer;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.animateRootFrame = encoder.AnimateRootFrame;
}
/// <summary>
@ -167,18 +189,18 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel> currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0;
bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels<TPixel>(this.encoder.TransparentColorMode);
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame);
EncodingUtilities.ClearTransparentPixels(currentFrame, Color.Transparent);
}
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame<TPixel>? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
currentFrame,
currentFrame.Bounds(),
currentFrame.Bounds,
null);
this.WriteHeaderChunk(stream);
@ -194,89 +216,104 @@ internal sealed class PngEncoderCore : IDisposable
if (image.Frames.Count > 1)
{
this.WriteAnimationControlChunk(stream, (uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)), pngMetadata.RepeatCount);
this.WriteAnimationControlChunk(
stream,
(uint)(image.Frames.Count - (pngMetadata.AnimateRootFrame ? 0 : 1)),
this.repeatCount ?? pngMetadata.RepeatCount);
}
// If the first frame isn't animated, write it as usual and skip it when writing animated frames
if (!pngMetadata.AnimateRootFrame || image.Frames.Count == 1)
bool userAnimateRootFrame = this.animateRootFrame == true;
if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
cancellationToken.ThrowIfCancellationRequested();
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
currentFrameIndex++;
}
if (image.Frames.Count > 1)
try
{
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
else
if (image.Frames.Count > 1)
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
}
// Write the first animated frame.
currentFrame = image.Frames[currentFrameIndex];
PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
uint sequenceNumber = 1;
if (pngMetadata.AnimateRootFrame)
{
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
else
{
sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
}
currentFrameIndex++;
currentFrameIndex++;
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory<TPixel>? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// Write following frames.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
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;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
blend);
if (clearTransparency)
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
ClearTransparentPixels(encodingFrame);
}
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
image.Configuration,
prev,
currentFrame,
nextFrame,
encodingFrame,
background,
blend);
if (clearTransparency)
{
EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
}
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Each frame control sequence number must be incremented by the number of frame data chunks that follow.
frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode;
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMode;
}
}
}
this.WriteEndChunk(stream);
this.WriteEndChunk(stream);
stream.Flush();
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
stream.Flush();
}
finally
{
// Dispose of allocations from final frame.
clonedFrame?.Dispose();
quantized?.Dispose();
}
}
/// <inheritdoc />
@ -286,32 +323,6 @@ internal sealed class PngEncoderCore : IDisposable
this.currentScanline?.Dispose();
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone)
where TPixel : unmanaged, IPixel<TPixel>
=> clone.ProcessPixelRows(accessor =>
{
// TODO: We should be able to speed this up with SIMD and masking.
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
Span<TPixel> span = accessor.GetRowSpan(y);
for (int x = 0; x < accessor.Width; x++)
{
ref TPixel pixel = ref span[x];
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)
{
pixel = TPixel.FromRgba32(transparent);
}
}
}
});
/// <summary>
/// Creates the quantized image and calculates and sets the bit depth.
/// </summary>

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

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

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

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

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

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

22
src/ImageSharp/Formats/QuantizingImageEncoder.cs

@ -1,22 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
public IQuantizer? Quantizer { get; init; }
/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

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

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

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

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

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

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

22
src/ImageSharp/Formats/TransparentColorMode.cs

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

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

@ -183,7 +183,7 @@ internal class AlphaDecoder : IDisposable
else
{
this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span);
this.ExtractAlphaRows(this.Vp8LDec);
this.ExtractAlphaRows(this.Vp8LDec, this.Width);
}
}
@ -257,14 +257,15 @@ internal class AlphaDecoder : IDisposable
/// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet.
/// </summary>
/// <param name="dec">The VP8L decoder.</param>
private void ExtractAlphaRows(Vp8LDecoder dec)
/// <param name="width">The image width.</param>
private void ExtractAlphaRows(Vp8LDecoder dec, int width)
{
int numRowsToProcess = dec.Height;
int width = dec.Width;
Span<uint> input = dec.Pixels.Memory.Span;
Span<byte> output = this.Alpha.Memory.Span;
// Extract alpha (which is stored in the green plane).
// the final width (!= dec->width_)
int pixelCount = width * numRowsToProcess;
WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator);
ExtractGreen(input, output, pixelCount);

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

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

11
src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs

@ -269,7 +269,11 @@ internal static unsafe class LosslessUtils
/// </summary>
/// <param name="transform">The transform data contains color table size and the entries in the color table.</param>
/// <param name="pixelData">The pixel data to apply the reverse transform on.</param>
public static void ColorIndexInverseTransform(Vp8LTransform transform, Span<uint> pixelData)
/// <param name="outputSpan">The resulting pixel data with the reversed transformation data.</param>
public static void ColorIndexInverseTransform(
Vp8LTransform transform,
Span<uint> pixelData,
Span<uint> outputSpan)
{
int bitsPerPixel = 8 >> transform.Bits;
int width = transform.XSize;
@ -282,7 +286,6 @@ internal static unsafe class LosslessUtils
int countMask = pixelsPerByte - 1;
int bitMask = (1 << bitsPerPixel) - 1;
uint[] decodedPixelData = new uint[width * height];
int pixelDataPos = 0;
for (int y = 0; y < height; y++)
{
@ -298,12 +301,12 @@ internal static unsafe class LosslessUtils
packedPixels = GetArgbIndex(pixelData[pixelDataPos++]);
}
decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)];
outputSpan[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)];
packedPixels >>= bitsPerPixel;
}
}
decodedPixelData.AsSpan().CopyTo(pixelData);
outputSpan.CopyTo(pixelData);
}
else
{

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

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

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

@ -69,7 +69,7 @@ internal class Vp8LEncoder : IDisposable
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
@ -114,7 +114,7 @@ internal class Vp8LEncoder : IDisposable
uint quality,
bool skipMetadata,
WebpEncodingMethod method,
WebpTransparentColorMode transparentColorMode,
TransparentColorMode transparentColorMode,
bool nearLossless,
int nearLosslessQuality)
{
@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public Vp8LHashChain HashChain { get; }
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
public WebpVp8X EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation, ushort? repeatCount)
where TPixel : unmanaged, IPixel<TPixel>
{
// Write bytes from the bit-writer buffer to the stream.
@ -258,7 +258,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount);
}
return vp8x;
@ -315,8 +315,8 @@ internal class Vp8LEncoder : IDisposable
(uint)bounds.Width,
(uint)bounds.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
frameMetadata.BlendMode,
frameMetadata.DisposalMode)
.WriteHeaderTo(stream);
}

7
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -684,6 +684,7 @@ internal sealed class WebpLosslessDecoder
List<Vp8LTransform> transforms = decoder.Transforms;
for (int i = transforms.Count - 1; i >= 0; i--)
{
// TODO: Review these 1D allocations. They could conceivably exceed limits.
Vp8LTransform transform = transforms[i];
switch (transform.TransformType)
{
@ -701,7 +702,11 @@ internal sealed class WebpLosslessDecoder
LosslessUtils.ColorSpaceInverseTransform(transform, pixelData);
break;
case Vp8LTransformType.ColorIndexingTransform:
LosslessUtils.ColorIndexInverseTransform(transform, pixelData);
using (IMemoryOwner<uint> output = memoryAllocator.Allocate<uint>(transform.XSize * transform.YSize, AllocationOptions.Clean))
{
LosslessUtils.ColorIndexInverseTransform(transform, pixelData, output.GetSpan());
}
break;
}
}

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

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

20
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs

@ -667,12 +667,12 @@ internal static unsafe class Vp8Encoding
// V block.
dst = dst[8..];
if (top != default)
if (!top.IsEmpty)
{
top = top[8..];
}
if (left != default)
if (!left.IsEmpty)
{
left = left[16..];
}
@ -701,7 +701,7 @@ internal static unsafe class Vp8Encoding
private static void VerticalPred(Span<byte> dst, Span<byte> top, int size)
{
if (top != default)
if (!top.IsEmpty)
{
for (int j = 0; j < size; j++)
{
@ -716,7 +716,7 @@ internal static unsafe class Vp8Encoding
public static void HorizontalPred(Span<byte> dst, Span<byte> left, int size)
{
if (left != default)
if (!left.IsEmpty)
{
left = left[1..]; // in the reference implementation, left starts at - 1.
for (int j = 0; j < size; j++)
@ -732,9 +732,9 @@ internal static unsafe class Vp8Encoding
public static void TrueMotion(Span<byte> dst, Span<byte> left, Span<byte> top, int size)
{
if (left != default)
if (!left.IsEmpty)
{
if (top != default)
if (!top.IsEmpty)
{
Span<byte> clip = Clip1.AsSpan(255 - left[0]); // left [0] instead of left[-1], original left starts at -1
for (int y = 0; y < size; y++)
@ -759,7 +759,7 @@ internal static unsafe class Vp8Encoding
// is equivalent to VE prediction where you just copy the top samples.
// Note that if top samples are not available, the default value is
// then 129, and not 127 as in the VerticalPred case.
if (top != default)
if (!top.IsEmpty)
{
VerticalPred(dst, top, size);
}
@ -774,14 +774,14 @@ internal static unsafe class Vp8Encoding
{
int dc = 0;
int j;
if (top != default)
if (!top.IsEmpty)
{
for (j = 0; j < size; j++)
{
dc += top[j];
}
if (left != default)
if (!left.IsEmpty)
{
// top and left present.
left = left[1..]; // in the reference implementation, left starts at -1.
@ -798,7 +798,7 @@ internal static unsafe class Vp8Encoding
dc = (dc + round) >> shift;
}
else if (left != default)
else if (!left.IsEmpty)
{
// left but no top.
left = left[1..]; // in the reference implementation, left starts at -1.

16
src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs

@ -48,7 +48,7 @@ internal static class YuvConversion
uint uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2;
YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2;
YuvToBgr(bottomY[0], (int)uv0 & 0xff, (int)(uv0 >> 16), bottomDst);
@ -69,7 +69,7 @@ internal static class YuvConversion
YuvToBgr(topY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((xMul2 - 1) * xStep)..]);
YuvToBgr(topY[xMul2 - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst[((xMul2 - 0) * xStep)..]);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
uv0 = (diag03 + luv) >> 1;
uv1 = (diag12 + uv) >> 1;
@ -85,7 +85,7 @@ internal static class YuvConversion
{
uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2;
YuvToBgr(topY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((len - 1) * xStep)..]);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2;
YuvToBgr(bottomY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst[((len - 1) * xStep)..]);
@ -120,7 +120,7 @@ internal static class YuvConversion
int u0t = (topU[0] + uDiag) >> 1;
int v0t = (topV[0] + vDiag) >> 1;
YuvToBgr(topY[0], u0t, v0t, topDst);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
int u0b = (curU[0] + uDiag) >> 1;
int v0b = (curV[0] + vDiag) >> 1;
@ -134,7 +134,7 @@ internal static class YuvConversion
ref byte topVRef = ref MemoryMarshal.GetReference(topV);
ref byte curURef = ref MemoryMarshal.GetReference(curU);
ref byte curVRef = ref MemoryMarshal.GetReference(curV);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
for (pos = 1, uvPos = 0; pos + 32 + 1 <= len; pos += 32, uvPos += 16)
{
@ -160,12 +160,12 @@ internal static class YuvConversion
Span<byte> tmpTopDst = ru[(4 * 32)..];
Span<byte> tmpBottomDst = tmpTopDst[(4 * 32)..];
Span<byte> tmpTop = tmpBottomDst[(4 * 32)..];
Span<byte> tmpBottom = (bottomY == default) ? null : tmpTop[32..];
Span<byte> tmpBottom = bottomY.IsEmpty ? null : tmpTop[32..];
UpSampleLastBlock(topU[uvPos..], curU[uvPos..], leftOver, ru);
UpSampleLastBlock(topV[uvPos..], curV[uvPos..], leftOver, rv);
topY[pos..len].CopyTo(tmpTop);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
bottomY[pos..len].CopyTo(tmpBottom);
ConvertYuvToBgrWithBottomYSse41(tmpTop, tmpBottom, tmpTopDst, tmpBottomDst, ru, rv, 0, xStep);
@ -176,7 +176,7 @@ internal static class YuvConversion
}
tmpTopDst[..((len - pos) * xStep)].CopyTo(topDst[(pos * xStep)..]);
if (bottomY != default)
if (!bottomY.IsEmpty)
{
tmpBottomDst[..((len - pos) * xStep)].CopyTo(bottomDst[(pos * xStep)..]);
}

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

@ -220,8 +220,8 @@ internal class WebpAnimationDecoder : IDisposable
{
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod;
frameMetadata.BlendMode = frameData.BlendingMethod;
frameMetadata.DisposalMode = frameData.DisposalMethod;
}
/// <summary>
@ -343,7 +343,7 @@ internal class WebpAnimationDecoder : IDisposable
return;
}
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
pixelRegion.Fill(backgroundPixel);

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

@ -6,8 +6,16 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary>
/// Image encoder for writing an image to a stream in the Webp format.
/// </summary>
public sealed class WebpEncoder : ImageEncoder
public sealed class WebpEncoder : AnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="WebpEncoder"/> class.
/// </summary>
public WebpEncoder()
// Match the default behavior of the native reference encoder.
=> this.TransparentColorMode = TransparentColorMode.Clear;
/// <summary>
/// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.
@ -58,13 +66,6 @@ public sealed class WebpEncoder : ImageEncoder
/// </summary>
public int FilterStrength { get; init; } = 60;
/// <summary>
/// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// The default value is Clear.
/// </summary>
public WebpTransparentColorMode TransparentColorMode { get; init; } = WebpTransparentColorMode.Clear;
/// <summary>
/// Gets a value indicating whether near lossless mode should be used.
/// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.

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

@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -55,7 +54,7 @@ internal sealed class WebpEncoderCore
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
/// </summary>
private readonly WebpTransparentColorMode transparentColorMode;
private readonly TransparentColorMode transparentColorMode;
/// <summary>
/// Whether to skip metadata during encoding.
@ -78,6 +77,19 @@ internal sealed class WebpEncoderCore
/// </summary>
private readonly WebpFileFormatType? fileFormat;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;
/// <summary>
/// The global configuration.
/// </summary>
@ -103,6 +115,8 @@ internal sealed class WebpEncoderCore
this.skipMetadata = encoder.SkipMetadata;
this.nearLossless = encoder.NearLossless;
this.nearLosslessQuality = encoder.NearLosslessQuality;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
}
/// <summary>
@ -147,16 +161,19 @@ internal sealed class WebpEncoderCore
long initialPosition = stream.Position;
bool hasAlpha = false;
WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation);
WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount);
// Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
cancellationToken.ThrowIfCancellationRequested();
hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation);
if (hasAnimation)
{
FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod;
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@ -164,12 +181,17 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata();
bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over;
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -178,7 +200,7 @@ internal sealed class WebpEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
blend,
ClampingMode.Even);
@ -197,7 +219,7 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
previousDisposal = frameMetadata.DisposalMode;
}
}
@ -229,9 +251,9 @@ internal sealed class WebpEncoderCore
// Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod;
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds, frameMetadata);
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@ -239,12 +261,17 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata();
bool blend = frameMetadata.BlendMethod == FrameBlendMode.Over;
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
@ -253,7 +280,7 @@ internal sealed class WebpEncoderCore
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
blend,
ClampingMode.Even);
@ -273,13 +300,14 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod;
previousDisposal = frameMetadata.DisposalMode;
}
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}
else
{
cancellationToken.ThrowIfCancellationRequested();
encoder.EncodeStatic(stream, image);
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);
}

22
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -24,19 +24,21 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
private WebpFrameMetadata(WebpFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
this.DisposalMode = other.DisposalMode;
this.BlendMode = other.BlendMode;
}
/// <summary>
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels
/// of the previous canvas.
/// </summary>
public FrameBlendMode BlendMethod { get; set; }
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// Gets or sets how the current frame is to be treated after it has been displayed
/// (before rendering the next frame) on the canvas.
/// </summary>
public FrameDisposalMode DisposalMethod { get; set; }
public FrameDisposalMode DisposalMode { get; set; }
/// <summary>
/// Gets or sets the frame duration. The time to wait before displaying the next frame,
@ -49,8 +51,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
=> new()
{
FrameDelay = (uint)metadata.Duration.TotalMilliseconds,
BlendMethod = metadata.BlendMode,
DisposalMethod = GetMode(metadata.DisposalMode)
BlendMode = metadata.BlendMode,
DisposalMode = GetMode(metadata.DisposalMode)
};
/// <inheritdoc/>
@ -59,8 +61,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay),
DisposalMode = this.DisposalMethod,
BlendMode = this.BlendMethod,
DisposalMode = this.DisposalMode,
BlendMode = this.BlendMode,
};
/// <inheritdoc/>

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

626
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -3,6 +3,7 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.IO;
@ -14,42 +15,19 @@ namespace SixLabors.ImageSharp.IO;
/// </summary>
internal sealed class ChunkedMemoryStream : Stream
{
// The memory allocator.
private readonly MemoryAllocator allocator;
// Data
private MemoryChunk? memoryChunk;
// The total number of allocated chunks
private int chunkCount;
// The length of the largest contiguous buffer that can be handled by the allocator.
private readonly int allocatorCapacity;
// Has the stream been disposed.
private readonly MemoryChunkBuffer memoryChunkBuffer;
private long length;
private long position;
private int bufferIndex;
private int chunkIndex;
private bool isDisposed;
// Current chunk to write to
private MemoryChunk? writeChunk;
// Offset into chunk to write to
private int writeOffset;
// Current chunk to read from
private MemoryChunk? readChunk;
// Offset into chunk to read from
private int readOffset;
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(MemoryAllocator allocator)
{
this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator;
}
=> this.memoryChunkBuffer = new(allocator);
/// <inheritdoc/>
public override bool CanRead => !this.isDisposed;
@ -66,25 +44,7 @@ internal sealed class ChunkedMemoryStream : Stream
get
{
this.EnsureNotDisposed();
int length = 0;
MemoryChunk? chunk = this.memoryChunk;
while (chunk != null)
{
MemoryChunk? next = chunk.Next;
if (next != null)
{
length += chunk.Length;
}
else
{
length += this.writeOffset;
}
chunk = next;
}
return length;
return this.length;
}
}
@ -94,93 +54,35 @@ internal sealed class ChunkedMemoryStream : Stream
get
{
this.EnsureNotDisposed();
if (this.readChunk is null)
{
return 0;
}
int pos = 0;
MemoryChunk? chunk = this.memoryChunk;
while (chunk != this.readChunk && chunk is not null)
{
pos += chunk.Length;
chunk = chunk.Next;
}
pos += this.readOffset;
return pos;
return this.position;
}
set
{
this.EnsureNotDisposed();
if (value < 0)
{
ThrowArgumentOutOfRange(nameof(value));
}
// Back up current position in case new position is out of range
MemoryChunk? backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset;
this.readChunk = null;
this.readOffset = 0;
int leftUntilAtPos = (int)value;
MemoryChunk? chunk = this.memoryChunk;
while (chunk != null)
{
if ((leftUntilAtPos < chunk.Length)
|| ((leftUntilAtPos == chunk.Length)
&& (chunk.Next is null)))
{
// The desired position is in this chunk
this.readChunk = chunk;
this.readOffset = leftUntilAtPos;
break;
}
leftUntilAtPos -= chunk.Length;
chunk = chunk.Next;
}
if (this.readChunk is null)
{
// Position is out of range
this.readChunk = backupReadChunk;
this.readOffset = backupReadOffset;
}
this.SetPosition(value);
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Flush()
{
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
this.EnsureNotDisposed();
switch (origin)
this.Position = origin switch
{
case SeekOrigin.Begin:
this.Position = offset;
break;
case SeekOrigin.Current:
this.Position += offset;
break;
case SeekOrigin.End:
this.Position = this.Length + offset;
break;
default:
ThrowInvalidSeek();
break;
}
SeekOrigin.Begin => (int)offset,
SeekOrigin.Current => (int)(this.Position + offset),
SeekOrigin.End => (int)(this.Length + offset),
_ => throw new ArgumentOutOfRangeException(nameof(offset)),
};
return this.Position;
return this.position;
}
/// <inheritdoc/>
@ -188,39 +90,13 @@ internal sealed class ChunkedMemoryStream : Stream
=> throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
try
{
this.isDisposed = true;
if (disposing)
{
ReleaseMemoryChunks(this.memoryChunk);
}
this.memoryChunk = null;
this.writeChunk = null;
this.readChunk = null;
this.chunkCount = 0;
}
finally
{
base.Dispose(disposing);
}
}
/// <inheritdoc/>
public override void Flush()
public override int ReadByte()
{
Unsafe.SkipInit(out byte b);
return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
@ -230,111 +106,70 @@ internal sealed class ChunkedMemoryStream : Stream
const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
return this.ReadImpl(buffer.AsSpan(offset, count));
return this.Read(buffer.AsSpan(offset, count));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(Span<byte> buffer) => this.ReadImpl(buffer);
private int ReadImpl(Span<byte> buffer)
public override int Read(Span<byte> buffer)
{
this.EnsureNotDisposed();
if (this.readChunk is null)
{
if (this.memoryChunk is null)
{
return 0;
}
int offset = 0;
int count = buffer.Length;
this.readChunk = this.memoryChunk;
this.readOffset = 0;
long remaining = this.length - this.position;
if (remaining <= 0)
{
// Already at the end of the stream, nothing to read
return 0;
}
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
if (remaining > count)
{
chunkSize = this.writeOffset;
remaining = count;
}
// 'remaining' can be less than the provided buffer length.
int bytesToRead = (int)remaining;
int bytesRead = 0;
int offset = 0;
int count = buffer.Length;
while (count > 0)
while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
{
if (this.readOffset == chunkSize)
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
int n = bytesToRead;
int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
if (n >= remainingBytesInCurrentChunk)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
{
break;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.readChunk.Buffer;
chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
n = remainingBytesInCurrentChunk;
moveToNextChunk = true;
}
int readCount = Math.Min(count, chunkSize - this.readOffset);
chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]);
offset += readCount;
count -= readCount;
this.readOffset += readCount;
bytesRead += readCount;
}
return bytesRead;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int ReadByte()
{
this.EnsureNotDisposed();
// Read n bytes from the current chunk
chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n));
bytesToRead -= n;
offset += n;
bytesRead += n;
if (this.readChunk is null)
{
if (this.memoryChunk is null)
if (moveToNextChunk)
{
return 0;
this.chunkIndex = 0;
this.bufferIndex++;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
}
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
if (this.readOffset == chunkSize)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
else
{
return -1;
this.chunkIndex += n;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.readChunk.Buffer;
}
return chunkBuffer.GetSpan()[this.readOffset++];
this.position += bytesRead;
return bytesRead;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void WriteByte(byte value)
=> this.Write(MemoryMarshal.CreateSpan(ref value, 1));
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
@ -344,157 +179,198 @@ internal sealed class ChunkedMemoryStream : Stream
const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
this.WriteImpl(buffer.AsSpan(offset, count));
this.Write(buffer.AsSpan(offset, count));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Write(ReadOnlySpan<byte> buffer) => this.WriteImpl(buffer);
private void WriteImpl(ReadOnlySpan<byte> buffer)
public override void Write(ReadOnlySpan<byte> buffer)
{
this.EnsureNotDisposed();
if (this.memoryChunk is null)
int offset = 0;
int count = buffer.Length;
long remaining = this.memoryChunkBuffer.Length - this.position;
// Ensure we have enough capacity to write the data.
while (remaining < count)
{
this.memoryChunk = this.AllocateMemoryChunk();
this.writeChunk = this.memoryChunk;
this.writeOffset = 0;
this.memoryChunkBuffer.Expand();
remaining = this.memoryChunkBuffer.Length - this.position;
}
Guard.NotNull(this.writeChunk);
Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan();
int chunkSize = this.writeChunk.Length;
int count = buffer.Length;
int offset = 0;
while (count > 0)
int bytesToWrite = count;
int bytesWritten = 0;
while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
{
if (this.writeOffset == chunkSize)
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
int n = bytesToWrite;
int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
if (n >= remainingBytesInCurrentChunk)
{
// Allocate a new chunk if the current one is full
this.writeChunk.Next = this.AllocateMemoryChunk();
this.writeChunk = this.writeChunk.Next;
this.writeOffset = 0;
chunkBuffer = this.writeChunk.Buffer.GetSpan();
chunkSize = this.writeChunk.Length;
n = remainingBytesInCurrentChunk;
moveToNextChunk = true;
}
int copyCount = Math.Min(count, chunkSize - this.writeOffset);
buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]);
// Write n bytes to the current chunk
buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n));
bytesToWrite -= n;
offset += n;
bytesWritten += n;
offset += copyCount;
count -= copyCount;
this.writeOffset += copyCount;
if (moveToNextChunk)
{
this.chunkIndex = 0;
this.bufferIndex++;
}
else
{
this.chunkIndex += n;
}
}
this.position += bytesWritten;
this.length += bytesWritten;
}
/// <inheritdoc/>
public override void WriteByte(byte value)
/// <summary>
/// Writes the entire contents of this memory stream to another stream.
/// </summary>
/// <param name="stream">The stream to write this memory stream to.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is <see langword="null"/>.</exception>
/// <exception cref="ObjectDisposedException">The current or target stream is closed.</exception>
public void WriteTo(Stream stream)
{
Guard.NotNull(stream, nameof(stream));
this.EnsureNotDisposed();
if (this.memoryChunk is null)
this.Position = 0;
long remaining = this.length - this.position;
if (remaining <= 0)
{
this.memoryChunk = this.AllocateMemoryChunk();
this.writeChunk = this.memoryChunk;
this.writeOffset = 0;
// Already at the end of the stream, nothing to read
return;
}
Guard.NotNull(this.writeChunk);
int bytesToRead = (int)remaining;
int bytesRead = 0;
while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
{
bool moveToNextChunk = false;
MemoryChunk chunk = this.memoryChunkBuffer[this.bufferIndex];
int n = bytesToRead;
int remainingBytesInCurrentChunk = chunk.Length - this.chunkIndex;
if (n >= remainingBytesInCurrentChunk)
{
n = remainingBytesInCurrentChunk;
moveToNextChunk = true;
}
IMemoryOwner<byte> chunkBuffer = this.writeChunk.Buffer;
int chunkSize = this.writeChunk.Length;
// Read n bytes from the current chunk
stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n));
bytesToRead -= n;
bytesRead += n;
if (this.writeOffset == chunkSize)
{
// Allocate a new chunk if the current one is full
this.writeChunk.Next = this.AllocateMemoryChunk();
this.writeChunk = this.writeChunk.Next;
this.writeOffset = 0;
chunkBuffer = this.writeChunk.Buffer;
if (moveToNextChunk)
{
this.chunkIndex = 0;
this.bufferIndex++;
}
else
{
this.chunkIndex += n;
}
}
chunkBuffer.GetSpan()[this.writeOffset++] = value;
this.position += bytesRead;
}
/// <summary>
/// Copy entire buffer into an array.
/// Writes the stream contents to a byte array, regardless of the <see cref="Position"/> property.
/// </summary>
/// <returns>The <see cref="T:byte[]"/>.</returns>
/// <returns>A new <see cref="T:byte[]"/>.</returns>
public byte[] ToArray()
{
int length = (int)this.Length; // This will throw if stream is closed
byte[] copy = new byte[this.Length];
MemoryChunk? backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset;
this.readChunk = this.memoryChunk;
this.readOffset = 0;
this.Read(copy, 0, length);
this.readChunk = backupReadChunk;
this.readOffset = backupReadOffset;
this.EnsureNotDisposed();
long position = this.position;
byte[] copy = new byte[this.length];
this.Position = 0;
_ = this.Read(copy, 0, copy.Length);
this.Position = position;
return copy;
}
/// <summary>
/// Write remainder of this stream to another stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public void WriteTo(Stream stream)
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
this.EnsureNotDisposed();
Guard.NotNull(stream, nameof(stream));
if (this.isDisposed)
{
return;
}
if (this.readChunk is null)
try
{
if (this.memoryChunk is null)
this.isDisposed = true;
if (disposing)
{
return;
this.memoryChunkBuffer.Dispose();
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
this.bufferIndex = 0;
this.chunkIndex = 0;
this.position = 0;
this.length = 0;
}
finally
{
base.Dispose(disposing);
}
}
private void SetPosition(long value)
{
long newPosition = value;
if (newPosition < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
this.position = newPosition;
// Find the current chunk & current chunk index
int currentChunkIndex = 0;
long offset = newPosition;
// If the new position is greater than the length of the stream, set the position to the end of the stream
if (offset > 0 && offset >= this.memoryChunkBuffer.Length)
{
chunkSize = this.writeOffset;
this.bufferIndex = this.memoryChunkBuffer.ChunkCount - 1;
this.chunkIndex = this.memoryChunkBuffer[this.bufferIndex].Length - 1;
return;
}
// Following code mirrors Read() logic (readChunk/readOffset should
// point just past last byte of last chunk when done)
// loop until end of chunks is found
while (true)
// Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk
// from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk.
while (offset != 0)
{
if (this.readOffset == chunkSize)
int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length;
if (offset < chunkLength)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
{
break;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.readChunk.Buffer;
chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
// Found the correct chunk and the corresponding index
break;
}
int writeCount = chunkSize - this.readOffset;
stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount);
this.readOffset = chunkSize;
offset -= chunkLength;
currentChunkIndex++;
}
this.bufferIndex = currentChunkIndex;
// Safe to cast here as we know the offset is less than the chunk length.
this.chunkIndex = (int)offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -507,48 +383,66 @@ internal sealed class ChunkedMemoryStream : Stream
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed.");
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(ChunkedMemoryStream), "The stream is closed.");
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value);
private sealed class MemoryChunkBuffer : IDisposable
{
private readonly List<MemoryChunk> memoryChunks = new();
private readonly MemoryAllocator allocator;
private readonly int allocatorCapacity;
private bool isDisposed;
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin.");
public MemoryChunkBuffer(MemoryAllocator allocator)
{
this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private MemoryChunk AllocateMemoryChunk()
{
// Tweak our buffer sizes to take the minimum of the provided buffer sizes
// or the allocator buffer capacity which provides us with the largest
// available contiguous buffer size.
IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++)));
public int ChunkCount => this.memoryChunks.Count;
return new MemoryChunk(buffer)
public long Length { get; private set; }
public MemoryChunk this[int index] => this.memoryChunks[index];
public void Expand()
{
Next = null,
Length = buffer.Length()
};
}
IMemoryOwner<byte> buffer =
this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount)));
private static void ReleaseMemoryChunks(MemoryChunk? chunk)
{
while (chunk != null)
MemoryChunk chunk = new(buffer)
{
Length = buffer.Length()
};
this.memoryChunks.Add(chunk);
this.Length += chunk.Length;
}
public void Dispose()
{
chunk.Dispose();
chunk = chunk.Next;
if (!this.isDisposed)
{
foreach (MemoryChunk chunk in this.memoryChunks)
{
chunk.Dispose();
}
this.memoryChunks.Clear();
this.Length = 0;
this.isDisposed = true;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetChunkSize(int i)
{
// Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator.
// https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
#pragma warning disable IDE1006 // Naming Styles
const int _128K = 1 << 17;
const int _4M = 1 << 22;
return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M;
#pragma warning restore IDE1006 // Naming Styles
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetChunkSize(int i)
{
// Increment chunks sizes with moderate speed, but without using too many buffers from the
// same ArrayPool bucket of the default MemoryAllocator.
// https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
const int b128K = 1 << 17;
const int b4M = 1 << 22;
return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M;
}
}
private sealed class MemoryChunk : IDisposable
@ -559,27 +453,15 @@ internal sealed class ChunkedMemoryStream : Stream
public IMemoryOwner<byte> Buffer { get; }
public MemoryChunk? Next { get; set; }
public int Length { get; init; }
private void Dispose(bool disposing)
public void Dispose()
{
if (!this.isDisposed)
{
if (disposing)
{
this.Buffer.Dispose();
}
this.Buffer.Dispose();
this.isDisposed = true;
}
}
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

11
src/ImageSharp/Image.Decode.cs

@ -128,21 +128,18 @@ public abstract partial class Image
// Does the given stream contain enough data to fit in the header for the format
// and does that data match the format specification?
// Individual formats should still check since they are public.
IImageFormat? format = null;
foreach (IImageFormatDetector formatDetector in configuration.ImageFormatsManager.FormatDetectors)
{
if (formatDetector.HeaderSize <= headersBuffer.Length && formatDetector.TryDetectFormat(headersBuffer, out IImageFormat? attemptFormat))
{
format = attemptFormat;
return attemptFormat;
}
}
if (format is null)
{
ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager);
}
ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager);
return format;
// Need to write this otherwise compiler is not happy
return null;
}
/// <summary>

2
src/ImageSharp/ImageFrame.cs

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

2
src/ImageSharp/ImageFrame{TPixel}.cs

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

5
src/ImageSharp/ImageSharp.csproj

@ -13,6 +13,7 @@
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore</PackageTags>
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
<Configurations>Debug;Release</Configurations>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<!-- This enables the nullable analysis and treats all nullable warnings as error-->
@ -29,14 +30,12 @@
<Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</Otherwise>
</Choose>

4
src/ImageSharp/Image{TPixel}.cs

@ -160,7 +160,7 @@ public sealed class Image<TPixel> : Image
/// <summary>
/// Gets the root frame.
/// </summary>
private IPixelSource<TPixel> PixelSourceUnsafe => this.frames.RootFrameUnsafe;
private ImageFrame<TPixel> PixelSourceUnsafe => this.frames.RootFrameUnsafe;
/// <summary>
/// Gets or sets the pixel at the specified position.
@ -324,7 +324,7 @@ public sealed class Image<TPixel> : Image
}
/// <summary>
/// Clones the current image
/// Clones the current image.
/// </summary>
/// <returns>Returns a new image with all the same metadata as the original.</returns>
public Image<TPixel> Clone() => this.Clone(this.Configuration);

62
src/ImageSharp/Memory/Buffer2DExtensions.cs

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

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

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

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

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

2
src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs

@ -241,7 +241,7 @@ internal sealed class ExifWriter
return true;
}
private static uint GetLength(IList<IExifValue> values)
private static uint GetLength(List<IExifValue> values)
{
if (values.Count == 0)
{

2
src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs

@ -144,7 +144,7 @@ internal sealed partial class IccDataReader
ushort channelCount = this.ReadUInt16();
var colorant = (IccColorantEncoding)this.ReadUInt16();
if (Enum.IsDefined(typeof(IccColorantEncoding), colorant) && colorant != IccColorantEncoding.Unknown)
if (Enum.IsDefined(colorant) && colorant != IccColorantEncoding.Unknown)
{
// The type is known and so are the values (they are constant)
// channelCount should always be 3 but it doesn't really matter if it's not

6
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -155,9 +155,9 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
}
return arrayValid &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
Enum.IsDefined(this.Header.DataColorSpace) &&
Enum.IsDefined(this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(this.Header.RenderingIntent) &&
this.Header.Size is >= minSize and < maxSize;
}

19
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary>
public class AffineTransformBuilder
{
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = [];
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
@ -301,7 +301,8 @@ public class AffineTransformBuilder
/// </summary>
/// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
public Matrix3x2 BuildMatrix(Size sourceSize)
=> this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
/// <summary>
/// Returns the combined transform matrix for a given source rectangle.
@ -345,18 +346,8 @@ public class AffineTransformBuilder
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
{
Size size = sourceRectangle.Size;
// Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Func<Size, Matrix3x2> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace);
Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
}
private static void CheckDegenerate(Matrix3x2 matrix)

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

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

8
src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs

@ -44,13 +44,13 @@ public static class BokehBlurExtensions
/// Applies a bokeh blur to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
/// <param name="components">The 'components' value representing the number of kernels to use to approximate the bokeh effect.</param>
/// <param name="gamma">The gamma highlight factor to use to emphasize bright spots in the source image</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, int radius, int components, float gamma, Rectangle rectangle)
public static IImageProcessingContext BokehBlur(this IImageProcessingContext source, Rectangle rectangle, int radius, int components, float gamma)
=> source.ApplyProcessor(new BokehBlurProcessor(radius, components, gamma), rectangle);
}

14
src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs

@ -44,10 +44,10 @@ public static class BoxBlurExtensions
/// Applies a box blur to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
/// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param>
@ -55,9 +55,11 @@ public static class BoxBlurExtensions
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY)
{
var processor = new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY);
return source.ApplyProcessor(processor, rectangle);
}
public static IImageProcessingContext BoxBlur(
this IImageProcessingContext source,
Rectangle rectangle,
int radius,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
=> source.ApplyProcessor(new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY), rectangle);
}

89
src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Processing.Processors.Convolution;
namespace SixLabors.ImageSharp.Processing.Extensions.Convolution;
/// <summary>
/// Defines general convolution extensions to apply on an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class ConvolutionExtensions
{
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix<float> kernelXY)
=> Convolve(source, kernelXY, false);
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(this IImageProcessingContext source, DenseMatrix<float> kernelXY, bool preserveAlpha)
=> Convolve(source, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</param>
/// <param name="borderWrapModeX">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.</param>
/// <param name="borderWrapModeY">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(
this IImageProcessingContext source,
DenseMatrix<float> kernelXY,
bool preserveAlpha,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
=> source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY));
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image object to alter.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix<float> kernelXY)
=> Convolve(source, rectangle, kernelXY, false);
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image object to alter.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(this IImageProcessingContext source, Rectangle rectangle, DenseMatrix<float> kernelXY, bool preserveAlpha)
=> Convolve(source, rectangle, kernelXY, preserveAlpha, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);
/// <summary>
/// Applies a convolution filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image object to alter.</param>
/// <param name="kernelXY">The convolution kernel to apply.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</param>
/// <param name="borderWrapModeX">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.</param>
/// <param name="borderWrapModeY">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext Convolve(
this IImageProcessingContext source,
Rectangle rectangle,
DenseMatrix<float> kernelXY,
bool preserveAlpha,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
=> source.ApplyProcessor(new ConvolutionProcessor(kernelXY, preserveAlpha, borderWrapModeX, borderWrapModeY), rectangle);
}

124
src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs

@ -16,8 +16,8 @@ public static class DetectEdgesExtensions
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) =>
DetectEdges(source, KnownEdgeDetectorKernels.Sobel);
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source)
=> DetectEdges(source, KnownEdgeDetectorKernels.Sobel);
/// <summary>
/// Detects any edges within the image.
@ -28,10 +28,8 @@ public static class DetectEdgesExtensions
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
Rectangle rectangle) =>
DetectEdges(source, KnownEdgeDetectorKernels.Sobel, rectangle);
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, Rectangle rectangle)
=> DetectEdges(source, rectangle, KnownEdgeDetectorKernels.Sobel);
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
@ -39,10 +37,8 @@ public static class DetectEdgesExtensions
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetector2DKernel kernel) =>
DetectEdges(source, kernel, true);
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetector2DKernel kernel)
=> DetectEdges(source, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>.
@ -57,49 +53,41 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source,
EdgeDetector2DKernel kernel,
bool grayscale)
{
var processor = new EdgeDetector2DProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
=> source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale));
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetector2DKernel kernel,
Rectangle rectangle) =>
DetectEdges(source, kernel, true, rectangle);
Rectangle rectangle,
EdgeDetector2DKernel kernel)
=> DetectEdges(source, rectangle, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection.
/// </param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetector2DKernel kernel,
bool grayscale,
Rectangle rectangle)
{
var processor = new EdgeDetector2DProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
bool grayscale)
=> source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale), rectangle);
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
@ -107,10 +95,8 @@ public static class DetectEdgesExtensions
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">The edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetectorKernel kernel) =>
DetectEdges(source, kernel, true);
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorKernel kernel)
=> DetectEdges(source, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>.
@ -125,66 +111,56 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source,
EdgeDetectorKernel kernel,
bool grayscale)
{
var processor = new EdgeDetectorProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
=> source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale));
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">The edge detector kernel.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetectorKernel kernel,
Rectangle rectangle) =>
DetectEdges(source, kernel, true, rectangle);
Rectangle rectangle,
EdgeDetectorKernel kernel)
=> DetectEdges(source, rectangle, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The edge detector kernel.</param>
/// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection.
/// </param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetectorKernel kernel,
bool grayscale,
Rectangle rectangle)
{
var processor = new EdgeDetectorProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
bool grayscale)
=> source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale), rectangle);
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">Thecompass edge detector kernel.</param>
/// <param name="kernel">The compass edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetectorCompassKernel kernel) =>
DetectEdges(source, kernel, true);
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorCompassKernel kernel)
=> DetectEdges(source, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">Thecompass edge detector kernel.</param>
/// <param name="kernel">The compass edge detector kernel.</param>
/// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection.
/// </param>
@ -193,47 +169,39 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source,
EdgeDetectorCompassKernel kernel,
bool grayscale)
{
var processor = new EdgeDetectorCompassProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
=> source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale));
/// <summary>
/// Detects any edges within the image operating in grayscale mode.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">Thecompass edge detector kernel.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The compass edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
EdgeDetectorCompassKernel kernel,
Rectangle rectangle) =>
DetectEdges(source, kernel, true, rectangle);
Rectangle rectangle,
EdgeDetectorCompassKernel kernel)
=> DetectEdges(source, rectangle, kernel, true);
/// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="kernel">Thecompass edge detector kernel.</param>
/// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection.
/// </param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="kernel">The compass edge detector kernel.</param>
/// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetectorCompassKernel kernel,
bool grayscale,
Rectangle rectangle)
{
var processor = new EdgeDetectorCompassProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
bool grayscale)
=> source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale), rectangle);
}

21
src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs

@ -32,22 +32,25 @@ public static class GaussianBlurExtensions
/// Applies a Gaussian blur to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle)
public static IImageProcessingContext GaussianBlur(
this IImageProcessingContext source,
Rectangle rectangle,
float sigma)
=> source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle);
/// <summary>
/// Applies a Gaussian blur to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param>
@ -55,9 +58,11 @@ public static class GaussianBlurExtensions
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY)
{
var processor = new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY);
return source.ApplyProcessor(processor, rectangle);
}
public static IImageProcessingContext GaussianBlur(
this IImageProcessingContext source,
Rectangle rectangle,
float sigma,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
=> source.ApplyProcessor(new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle);
}

28
src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs

@ -16,8 +16,8 @@ public static class GaussianSharpenExtensions
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source) =>
source.ApplyProcessor(new GaussianSharpenProcessor());
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source)
=> source.ApplyProcessor(new GaussianSharpenProcessor());
/// <summary>
/// Applies a Gaussian sharpening filter to the image.
@ -25,32 +25,32 @@ public static class GaussianSharpenExtensions
/// <param name="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma) =>
source.ApplyProcessor(new GaussianSharpenProcessor(sigma));
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma)
=> source.ApplyProcessor(new GaussianSharpenProcessor(sigma));
/// <summary>
/// Applies a Gaussian sharpening filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(
this IImageProcessingContext source,
float sigma,
Rectangle rectangle) =>
Rectangle rectangle,
float sigma) =>
source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle);
/// <summary>
/// Applies a Gaussian sharpening filter to the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param>
@ -58,9 +58,11 @@ public static class GaussianSharpenExtensions
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY)
{
var processor = new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY);
return source.ApplyProcessor(processor, rectangle);
}
public static IImageProcessingContext GaussianSharpen(
this IImageProcessingContext source,
Rectangle rectangle,
float sigma,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
=> source.ApplyProcessor(new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY), rectangle);
}

17
src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs

@ -20,21 +20,28 @@ public static class MedianBlurExtensions
/// Whether the filter is applied to alpha as well as the color channels.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha)
public static IImageProcessingContext MedianBlur(
this IImageProcessingContext source,
int radius,
bool preserveAlpha)
=> source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha));
/// <summary>
/// Applies a median blur on the image.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <param name="radius">The radius of the area to find the median for.</param>
/// <param name="preserveAlpha">
/// Whether the filter is applied to alpha as well as the color channels.
/// </param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha, Rectangle rectangle)
public static IImageProcessingContext MedianBlur(
this IImageProcessingContext source,
Rectangle rectangle,
int radius,
bool preserveAlpha)
=> source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle);
}

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

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

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

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

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

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

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

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

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

@ -62,14 +62,14 @@ internal class Convolution2DProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (var map = new KernelSamplingMap(allocator))
using (KernelSamplingMap map = new(allocator))
{
// Since the kernel sizes are identical we can use a single map.
map.BuildSamplingOffsetMap(this.KernelY, interest);
var operation = new Convolution2DRowOperation<TPixel>(
Convolution2DRowOperation<TPixel> operation = new(
interest,
targetPixels,
source.PixelBuffer,

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

@ -35,18 +35,48 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
Rectangle sourceRectangle,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
: this(configuration, kernel, kernel, preserveAlpha, source, sourceRectangle, borderWrapModeX, borderWrapModeY)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Convolution2PassProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="kernelX">The 1D convolution kernel. X Direction</param>
/// <param name="kernelY">The 1D convolution kernel. Y Direction</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</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="borderWrapModeX">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.</param>
/// <param name="borderWrapModeY">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.</param>
public Convolution2PassProcessor(
Configuration configuration,
float[] kernelX,
float[] kernelY,
bool preserveAlpha,
Image<TPixel> source,
Rectangle sourceRectangle,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
: base(configuration, source, sourceRectangle)
{
this.Kernel = kernel;
this.KernelX = kernelX;
this.KernelY = kernelY;
this.PreserveAlpha = preserveAlpha;
this.BorderWrapModeX = borderWrapModeX;
this.BorderWrapModeY = borderWrapModeY;
}
/// <summary>
/// Gets the convolution kernel.
/// Gets the convolution kernel. X direction.
/// </summary>
public float[] KernelX { get; }
/// <summary>
/// Gets the convolution kernel. Y direction.
/// </summary>
public float[] Kernel { get; }
public float[] KernelY { get; }
/// <summary>
/// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels.
@ -68,21 +98,21 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
{
using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// We can create a single sampling map with the size as if we were using the non separated 2D kernel
// the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur.
using var mapXY = new KernelSamplingMap(this.Configuration.MemoryAllocator);
using KernelSamplingMap mapXY = new(this.Configuration.MemoryAllocator);
mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY);
mapXY.BuildSamplingOffsetMap(this.KernelX.Length, this.KernelX.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY);
// Horizontal convolution
var horizontalOperation = new HorizontalConvolutionRowOperation(
HorizontalConvolutionRowOperation horizontalOperation = new(
interest,
firstPassPixels,
source.PixelBuffer,
mapXY,
this.Kernel,
this.KernelX,
this.Configuration,
this.PreserveAlpha);
@ -92,12 +122,12 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
in horizontalOperation);
// Vertical convolution
var verticalOperation = new VerticalConvolutionRowOperation(
VerticalConvolutionRowOperation verticalOperation = new(
interest,
source.PixelBuffer,
firstPassPixels,
mapXY,
this.Kernel,
this.KernelY,
this.Configuration,
this.PreserveAlpha);
@ -140,7 +170,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width;
@ -306,7 +336,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width;

79
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs

@ -0,0 +1,79 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution;
/// <summary>
/// Defines a processor that uses a 2 dimensional matrix to perform convolution against an image.
/// </summary>
public class ConvolutionProcessor : IImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="ConvolutionProcessor"/> class.
/// </summary>
/// <param name="kernelXY">The 2d gradient operator.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</param>
/// <param name="borderWrapModeX">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.</param>
/// <param name="borderWrapModeY">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.</param>
public ConvolutionProcessor(
in DenseMatrix<float> kernelXY,
bool preserveAlpha,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
{
this.KernelXY = kernelXY;
this.PreserveAlpha = preserveAlpha;
this.BorderWrapModeX = borderWrapModeX;
this.BorderWrapModeY = borderWrapModeY;
}
/// <summary>
/// Gets the 2d convolution kernel.
/// </summary>
public DenseMatrix<float> KernelXY { get; }
/// <summary>
/// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels.
/// </summary>
public bool PreserveAlpha { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </summary>
public BorderWrappingMode BorderWrapModeX { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </summary>
public BorderWrappingMode BorderWrapModeY { get; }
/// <inheritdoc/>
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged,
IPixel<TPixel>
{
if (this.KernelXY.TryGetLinearlySeparableComponents(out float[]? kernelX, out float[]? kernelY))
{
return new Convolution2PassProcessor<TPixel>(
configuration,
kernelX,
kernelY,
this.PreserveAlpha,
source,
sourceRectangle,
this.BorderWrapModeX,
this.BorderWrapModeY);
}
return new ConvolutionProcessor<TPixel>(
configuration,
this.KernelXY,
this.PreserveAlpha,
source,
sourceRectangle,
this.BorderWrapModeX,
this.BorderWrapModeY);
}
}

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

@ -31,10 +31,34 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
bool preserveAlpha,
Image<TPixel> source,
Rectangle sourceRectangle)
: this(configuration, kernelXY, preserveAlpha, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConvolutionProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="kernelXY">The 2d gradient operator.</param>
/// <param name="preserveAlpha">Whether the convolution filter is applied to alpha as well as the color channels.</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="borderWrapModeX">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.</param>
/// <param name="borderWrapModeY">The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.</param>
public ConvolutionProcessor(
Configuration configuration,
in DenseMatrix<float> kernelXY,
bool preserveAlpha,
Image<TPixel> source,
Rectangle sourceRectangle,
BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY)
: base(configuration, source, sourceRectangle)
{
this.KernelXY = kernelXY;
this.PreserveAlpha = preserveAlpha;
this.BorderWrapModeX = borderWrapModeX;
this.BorderWrapModeY = borderWrapModeY;
}
/// <summary>
@ -47,6 +71,16 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
/// </summary>
public bool PreserveAlpha { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </summary>
public BorderWrappingMode BorderWrapModeX { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </summary>
public BorderWrappingMode BorderWrapModeY { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
@ -55,13 +89,13 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
using (var map = new KernelSamplingMap(allocator))
using (KernelSamplingMap map = new(allocator))
{
map.BuildSamplingOffsetMap(this.KernelXY, interest);
map.BuildSamplingOffsetMap(this.KernelXY.Rows, this.KernelXY.Columns, interest, this.BorderWrapModeX, this.BorderWrapModeY);
var operation = new RowOperation(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha);
RowOperation operation = new(interest, targetPixels, source.PixelBuffer, map, this.KernelXY, this.Configuration, this.PreserveAlpha);
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration,
interest,
@ -121,7 +155,7 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span);
Span<TPixel> targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth);
var state = new ConvolutionState(in this.kernel, this.map);
ConvolutionState state = new(in this.kernel, this.map);
int row = y - this.bounds.Y;
ref int sampleRowBase = ref state.GetSampleRow((uint)row);

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

@ -58,12 +58,12 @@ internal class EdgeDetectorCompassProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// We need a clean copy for each pass to start from
using ImageFrame<TPixel> cleanCopy = source.Clone();
using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, in this.kernels[0], true, this.Source, interest))
using (ConvolutionProcessor<TPixel> processor = new(this.Configuration, in this.kernels[0], true, this.Source, interest))
{
processor.Apply(source);
}
@ -78,12 +78,12 @@ internal class EdgeDetectorCompassProcessor<TPixel> : ImageProcessor<TPixel>
{
using ImageFrame<TPixel> pass = cleanCopy.Clone();
using (var processor = new ConvolutionProcessor<TPixel>(this.Configuration, in this.kernels[i], true, this.Source, interest))
using (ConvolutionProcessor<TPixel> processor = new(this.Configuration, in this.kernels[i], true, this.Source, interest))
{
processor.Apply(pass);
}
var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest);
RowOperation operation = new(source.PixelBuffer, pass.PixelBuffer, interest);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,

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

@ -53,7 +53,7 @@ internal class EdgeDetectorProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
using var processor = new ConvolutionProcessor<TPixel>(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle);
using ConvolutionProcessor<TPixel> processor = new(this.Configuration, in this.kernelXY, true, this.Source, this.SourceRectangle);
processor.Apply(source);
}
}

20
src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs

@ -12,24 +12,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution;
internal class GaussianBlurProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="GaussianBlurProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="GaussianBlurProcessor"/> defining the processor parameters.</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>
public GaussianBlurProcessor(
Configuration configuration,
GaussianBlurProcessor definition,
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
int kernelSize = (definition.Radius * 2) + 1;
this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma);
}
/// <summary>
/// Initializes a new instance of the <see cref="GaussianBlurProcessor{TPixel}"/> class.
/// </summary>
@ -72,7 +54,7 @@ internal class GaussianBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
using var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY);
using Convolution2PassProcessor<TPixel> processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY);
processor.Apply(source);
}

18
src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs

@ -12,22 +12,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution;
internal class GaussianSharpenProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="GaussianSharpenProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="GaussianSharpenProcessor"/> defining the processor parameters.</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>
public GaussianSharpenProcessor(
Configuration configuration,
GaussianSharpenProcessor definition,
Image<TPixel> source,
Rectangle sourceRectangle)
: this(configuration, definition, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GaussianSharpenProcessor{TPixel}"/> class.
/// </summary>
@ -70,7 +54,7 @@ internal class GaussianSharpenProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
using var processor = new Convolution2PassProcessor<TPixel>(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY);
using Convolution2PassProcessor<TPixel> processor = new(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY);
processor.Apply(source);
}

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

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

6
src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs

@ -21,12 +21,12 @@ internal static class BokehBlurKernelDataProvider
/// <summary>
/// Gets the kernel scales to adjust the component values in each kernel
/// </summary>
private static IReadOnlyList<float> KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f };
private static float[] KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f };
/// <summary>
/// Gets the available bokeh blur kernel parameters
/// </summary>
private static IReadOnlyList<Vector4[]> KernelComponents { get; } = new[]
private static Vector4[][] KernelComponents { get; } = new[]
{
// 1 component
new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) },
@ -112,7 +112,7 @@ internal static class BokehBlurKernelDataProvider
private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount)
{
// Prepare the kernel components
int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Count));
int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Length));
return (KernelComponents[index], KernelScales[index]);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -61,12 +61,12 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
if (matrix.Equals(Matrix3x2.Identity))
{
// The clone will be blank here copy all the pixel data over
var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds);
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++)
{
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y));
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y));
}
return;
@ -77,31 +77,31 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
if (sampler is NearestNeighborResampler)
{
var nnOperation = new NNAffineOperation(
NNAffineOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
matrix);
ParallelRowIterator.IterateRows(
configuration,
destination.Bounds(),
destination.Bounds,
in nnOperation);
return;
}
var operation = new AffineOperation<TResampler>(
AffineOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
in sampler,
matrix);
ParallelRowIterator.IterateRowIntervals<AffineOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
destination.Bounds,
in operation);
}
@ -128,17 +128,17 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destRow.Length; x++)
for (int x = 0; x < destinationRowSpan.Length; x++)
{
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
Vector2 point = Vector2.Transform(new Vector2(x, y), this.matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source.GetElementUnsafe(px, py);
destinationRowSpan[x] = this.source.GetElementUnsafe(px, py);
}
}
}
@ -195,16 +195,16 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
destinationRowSpan,
span,
PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++)
{
var point = Vector2.Transform(new Vector2(x, y), matrix);
Vector2 point = Vector2.Transform(new Vector2(x, y), matrix);
float pY = point.Y;
float pX = point.X;
@ -221,13 +221,14 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
Span<TPixel> sourceRowSpan = this.source.DangerousGetRowSpan(yK);
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4();
Vector4 current = sourceRowSpan[xK].ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
@ -240,7 +241,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}

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

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

86
src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs

@ -0,0 +1,86 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;
/// <summary>
/// Represents a solver for systems of linear equations using the Gaussian Elimination method.
/// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector.
/// This implementation is based on: <see href="https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html"/>
/// </summary>
internal static class GaussianEliminationSolver
{
/// <summary>
/// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination.
/// </summary>
/// <param name="matrix">The square matrix representing the coefficients of the linear equations.</param>
/// <param name="result">The vector representing the constants on the right-hand side of the linear equations.</param>
/// <exception cref="Exception">Thrown if the matrix is singular and cannot be solved.</exception>
/// <remarks>
/// The matrix passed to this method must be a square matrix.
/// If the matrix is singular (i.e., has no unique solution), an <see cref="NotSupportedException"/> will be thrown.
/// </remarks>
public static void Solve(double[][] matrix, double[] result)
{
TransformToRowEchelonForm(matrix, result);
ApplyBackSubstitution(matrix, result);
}
private static void TransformToRowEchelonForm(double[][] matrix, double[] result)
{
int colCount = matrix.Length;
int rowCount = matrix[0].Length;
int pivotRow = 0;
for (int pivotCol = 0; pivotCol < colCount; pivotCol++)
{
double maxValue = double.Abs(matrix[pivotRow][pivotCol]);
int maxIndex = pivotRow;
for (int r = pivotRow + 1; r < rowCount; r++)
{
double value = double.Abs(matrix[r][pivotCol]);
if (value > maxValue)
{
maxIndex = r;
maxValue = value;
}
}
if (matrix[maxIndex][pivotCol] == 0)
{
throw new NotSupportedException("Matrix is singular and cannot be solve");
}
(matrix[pivotRow], matrix[maxIndex]) = (matrix[maxIndex], matrix[pivotRow]);
(result[pivotRow], result[maxIndex]) = (result[maxIndex], result[pivotRow]);
for (int r = pivotRow + 1; r < rowCount; r++)
{
double fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol];
for (int c = pivotCol + 1; c < colCount; c++)
{
matrix[r][c] -= matrix[pivotRow][c] * fraction;
}
result[r] -= result[pivotRow] * fraction;
matrix[r][pivotCol] = 0;
}
pivotRow++;
}
}
private static void ApplyBackSubstitution(double[][] matrix, double[] result)
{
int rowCount = matrix[0].Length;
for (int row = rowCount - 1; row >= 0; row--)
{
result[row] /= matrix[row][row];
for (int r = 0; r < row; r++)
{
result[r] -= result[row] * matrix[r][row];
}
}
}
}

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

@ -61,12 +61,12 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (matrix.Equals(Matrix4x4.Identity))
{
// The clone will be blank here copy all the pixel data over
var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds());
Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds);
Buffer2DRegion<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destinationBuffer = destination.PixelBuffer.GetRegion(interest);
for (int y = 0; y < sourceBuffer.Height; y++)
{
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y));
sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y));
}
return;
@ -77,31 +77,31 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (sampler is NearestNeighborResampler)
{
var nnOperation = new NNProjectiveOperation(
NNProjectiveOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
matrix);
ParallelRowIterator.IterateRows(
configuration,
destination.Bounds(),
destination.Bounds,
in nnOperation);
return;
}
var operation = new ProjectiveOperation<TResampler>(
ProjectiveOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Rectangle.Intersect(this.SourceRectangle, source.Bounds),
destination.PixelBuffer,
in sampler,
matrix);
ParallelRowIterator.IterateRowIntervals<ProjectiveOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
destination.Bounds,
in operation);
}
@ -128,9 +128,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destRow.Length; x++)
for (int x = 0; x < destinationRowSpan.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X);
@ -138,7 +138,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source.GetElementUnsafe(px, py);
destinationRowSpan[x] = this.source.GetElementUnsafe(px, py);
}
}
}
@ -195,10 +195,10 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
destinationRowSpan,
span,
PixelConversionModifiers.Scale);
@ -221,13 +221,14 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
Span<TPixel> sourceRowSpan = this.source.DangerousGetRowSpan(yK);
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4();
Vector4 current = sourceRowSpan[xK].ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
@ -240,7 +241,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}

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

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

159
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -3,6 +3,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -278,6 +279,91 @@ internal static class TransformUtils
return matrix;
}
/// <summary>
/// Computes the projection matrix for a quad distortion transformation.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="topLeft">The top-left point of the distorted quad.</param>
/// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the matrix.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks>
/// This method is based on the algorithm described in the following article:
/// <see href="https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/"/>
/// </remarks>
public static Matrix4x4 CreateQuadDistortionMatrix(
Rectangle rectangle,
PointF topLeft,
PointF topRight,
PointF bottomRight,
PointF bottomLeft,
TransformSpace transformSpace)
{
PointF p1 = new(rectangle.X, rectangle.Y);
PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y);
PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height);
PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height);
PointF q1 = topLeft;
PointF q2 = topRight;
PointF q3 = bottomRight;
PointF q4 = bottomLeft;
double[][] matrixData =
[
[p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X],
[0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y],
[p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X],
[0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y],
[p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X],
[0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y],
[p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X],
[0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y],
];
double[] b =
[
q1.X,
q1.Y,
q2.X,
q2.Y,
q3.X,
q3.Y,
q4.X,
q4.Y,
];
GaussianEliminationSolver.Solve(matrixData, b);
#pragma warning disable SA1117
Matrix4x4 projectionMatrix = new(
(float)b[0], (float)b[3], 0, (float)b[6],
(float)b[1], (float)b[4], 0, (float)b[7],
0, 0, 1, 0,
(float)b[2], (float)b[5], 0, 1);
#pragma warning restore SA1117
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
if (transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(projectionMatrix))
{
if (projectionMatrix.M41 != 0)
{
projectionMatrix.M41--;
}
if (projectionMatrix.M42 != 0)
{
projectionMatrix.M42--;
}
}
return projectionMatrix;
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
@ -293,15 +379,16 @@ internal static class TransformUtils
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Matrix4x4 matrix, Size size)
public static Size GetTransformedSize(Matrix4x4 matrix, Size size, TransformSpace transformSpace)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
if (matrix.IsIdentity || matrix.Equals(default))
{
return size;
}
@ -309,27 +396,7 @@ internal static class TransformUtils
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
// The conditions are as follows:
bool usePixelSpace =
// 1. Ensure there's no perspective distortion:
// M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0.
(matrix.M34 == 0) &&
// 2. Ensure standard affine transformation without any unusual depth or perspective scaling:
// M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth
// scaling or perspective, which suggests a more complex transformation.
(matrix.M44 == 1) &&
// 3. Ensure no unusual translation in the x-direction:
// M14 represents translation in the x-direction that might be part of a more complex transformation.
// For standard affine transformations, M14 should be 0.
(matrix.M14 == 0) &&
// 4. Ensure no unusual translation in the y-direction:
// M24 represents translation in the y-direction that might be part of a more complex transformation.
// For standard affine transformations, M24 should be 0.
(matrix.M24 == 0);
bool usePixelSpace = transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(matrix);
// Define an offset size to translate between pixel space and coordinate space.
// When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
@ -376,7 +443,7 @@ internal static class TransformUtils
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity))
if (matrix.IsIdentity || matrix.Equals(default))
{
return size;
}
@ -412,7 +479,7 @@ internal static class TransformUtils
/// </returns>
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
{
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
if (matrix.IsIdentity || rectangle.Equals(default))
{
bounds = default;
return false;
@ -439,7 +506,7 @@ internal static class TransformUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
{
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
if (matrix.IsIdentity || rectangle.Equals(default))
{
bounds = default;
return false;
@ -492,4 +559,44 @@ internal static class TransformUtils
(int)Math.Ceiling(right),
(int)Math.Ceiling(bottom));
}
private static bool IsAffineRotationOrSkew(Matrix4x4 matrix)
{
const float epsilon = 1e-6f;
// Check if the matrix is affine (last column should be [0, 0, 0, 1])
if (Math.Abs(matrix.M14) > epsilon ||
Math.Abs(matrix.M24) > epsilon ||
Math.Abs(matrix.M34) > epsilon ||
Math.Abs(matrix.M44 - 1f) > epsilon)
{
return false;
}
// Translation component (M41, m42) are allowed, others are not.
if (Math.Abs(matrix.M43) > epsilon)
{
return false;
}
// Extract the linear (rotation and skew) part of the matrix
// Upper-left 3x3 matrix
float m11 = matrix.M11, m12 = matrix.M12, m13 = matrix.M13;
float m21 = matrix.M21, m22 = matrix.M22, m23 = matrix.M23;
float m31 = matrix.M31, m32 = matrix.M32, m33 = matrix.M33;
// Compute the determinant of the linear part
float determinant = (m11 * ((m22 * m33) - (m23 * m32))) -
(m12 * ((m21 * m33) - (m23 * m31))) +
(m13 * ((m21 * m32) - (m22 * m31)));
// Check if the determinant is approximately ±1 (no scaling)
if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon)
{
return false;
}
// All checks passed; the matrix represents rotation and/or skew (with possible translation)
return true;
}
}

40
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary>
public class ProjectiveTransformBuilder
{
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = [];
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
@ -279,6 +279,30 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder AppendTranslation(Vector2 position)
=> this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0)));
/// <summary>
/// Prepends a quad distortion matrix using the specified corner points.
/// </summary>
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
/// <summary>
/// Appends a quad distortion matrix using the specified corner points.
/// </summary>
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Append(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
/// <summary>
/// Prepends a raw matrix.
/// </summary>
@ -361,18 +385,8 @@ public class ProjectiveTransformBuilder
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
{
Size size = sourceRectangle.Size;
// Translate the origin matrix to cater for source rectangle offsets.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
foreach (Func<Size, Matrix4x4> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(matrix, size);
Matrix4x4 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
}
private static void CheckDegenerate(Matrix4x4 matrix)

21
tests/Directory.Build.targets

@ -18,18 +18,23 @@
<ItemGroup>
<!-- Test Dependencies -->
<PackageReference Update="Colourful" Version="3.1.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.4.0" />
<PackageReference Update="Colourful" Version="3.2.0" />
<!--
Do not update to 14+ yet. There's differnce in how the BMP decoder handles rounding in 16 bit images.
See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
-->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.10.0" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.70" />
<PackageReference Update="NetVips" Version="2.4.0" />
<PackageReference Update="NetVips.Native" Version="8.15.0" />
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.14.0" />
<PackageReference Update="Pfim" Version="0.11.2" />
<PackageReference Update="Moq" Version="4.20.72" />
<PackageReference Update="NetVips" Version="3.0.0" />
<PackageReference Update="NetVips.Native" Version="8.16.0" />
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.14.2" />
<PackageReference Update="Pfim" Version="0.11.3" />
<PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="6.0.5.128" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.4.2" />
<PackageReference Update="SkiaSharp" Version="2.88.6" />
<PackageReference Update="SkiaSharp" Version="2.88.9" />
<PackageReference Update="System.Drawing.Common" Version="6.0.0" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" />
</ItemGroup>

64
tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.cs

@ -47,37 +47,37 @@ public class Pad3Shuffle4Channel
//
// | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------------------------- |------------------- |-------------------------------------------------- |------ |------------:|----------:|----------:|------------:|------:|--------:|------:|------:|------:|----------:|
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 120.64 ns | 7.190 ns | 21.200 ns | 114.26 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 120.64 ns | 7.190 ns | 21.200 ns | 114.26 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. AVX | Empty | 96 | 23.63 ns | 0.175 ns | 0.155 ns | 23.65 ns | 0.15 | 0.01 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 96 | 25.25 ns | 0.356 ns | 0.298 ns | 25.27 ns | 0.17 | 0.01 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 96 | 25.25 ns | 0.356 ns | 0.298 ns | 25.27 ns | 0.17 | 0.01 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 14.80 ns | 0.358 ns | 1.032 ns | 14.64 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 14.80 ns | 0.358 ns | 1.032 ns | 14.64 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. AVX | Empty | 96 | 24.84 ns | 0.376 ns | 0.333 ns | 24.74 ns | 1.57 | 0.06 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 96 | 24.58 ns | 0.471 ns | 0.704 ns | 24.38 ns | 1.60 | 0.09 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 96 | 24.58 ns | 0.471 ns | 0.704 ns | 24.38 ns | 1.60 | 0.09 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 258.92 ns | 4.873 ns | 4.069 ns | 257.95 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 258.92 ns | 4.873 ns | 4.069 ns | 257.95 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. AVX | Empty | 384 | 41.41 ns | 0.859 ns | 1.204 ns | 41.33 ns | 0.16 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 384 | 40.74 ns | 0.848 ns | 0.793 ns | 40.48 ns | 0.16 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 384 | 40.74 ns | 0.848 ns | 0.793 ns | 40.48 ns | 0.16 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 74.50 ns | 0.490 ns | 0.383 ns | 74.49 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 74.50 ns | 0.490 ns | 0.383 ns | 74.49 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. AVX | Empty | 384 | 40.74 ns | 0.624 ns | 0.584 ns | 40.72 ns | 0.55 | 0.01 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 384 | 38.28 ns | 0.534 ns | 0.417 ns | 38.22 ns | 0.51 | 0.01 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 384 | 38.28 ns | 0.534 ns | 0.417 ns | 38.22 ns | 0.51 | 0.01 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 503.91 ns | 6.466 ns | 6.048 ns | 501.58 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 503.91 ns | 6.466 ns | 6.048 ns | 501.58 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. AVX | Empty | 768 | 62.86 ns | 0.332 ns | 0.277 ns | 62.80 ns | 0.12 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 768 | 64.59 ns | 0.469 ns | 0.415 ns | 64.62 ns | 0.13 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 768 | 64.59 ns | 0.469 ns | 0.415 ns | 64.62 ns | 0.13 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 110.51 ns | 0.592 ns | 0.554 ns | 110.33 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 110.51 ns | 0.592 ns | 0.554 ns | 110.33 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. AVX | Empty | 768 | 64.72 ns | 1.306 ns | 1.090 ns | 64.51 ns | 0.59 | 0.01 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 768 | 62.11 ns | 0.816 ns | 0.682 ns | 61.98 ns | 0.56 | 0.01 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 768 | 62.11 ns | 0.816 ns | 0.682 ns | 61.98 ns | 0.56 | 0.01 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 1,005.84 ns | 13.176 ns | 12.325 ns | 1,004.70 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 1,005.84 ns | 13.176 ns | 12.325 ns | 1,004.70 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. AVX | Empty | 1536 | 110.05 ns | 0.256 ns | 0.214 ns | 110.04 ns | 0.11 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | COMPlus_EnableAVX=0 | 1536 | 110.23 ns | 0.545 ns | 0.483 ns | 110.09 ns | 0.11 | 0.00 | - | - | - | - |
// | Pad3Shuffle4 | 3. SSE | DOTNET_EnableAVX=0 | 1536 | 110.23 ns | 0.545 ns | 0.483 ns | 110.09 ns | 0.11 | 0.00 | - | - | - | - |
// | | | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 220.37 ns | 1.601 ns | 1.419 ns | 220.13 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 220.37 ns | 1.601 ns | 1.419 ns | 220.13 ns | 1.00 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. AVX | Empty | 1536 | 111.54 ns | 2.173 ns | 2.901 ns | 111.27 ns | 0.51 | 0.01 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | COMPlus_EnableAVX=0 | 1536 | 110.23 ns | 0.456 ns | 0.427 ns | 110.25 ns | 0.50 | 0.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. SSE | DOTNET_EnableAVX=0 | 1536 | 110.23 ns | 0.456 ns | 0.427 ns | 110.25 ns | 0.50 | 0.00 | - | - | - | - |
// 2023-02-21
// ##########
@ -94,34 +94,34 @@ public class Pad3Shuffle4Channel
// | Method | Job | EnvironmentVariables | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------------------------- |------------------- |-------------------------------------------------- |------ |----------:|---------:|---------:|------:|------:|------:|------:|----------:|
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 57.45 ns | 0.126 ns | 0.118 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 96 | 14.70 ns | 0.105 ns | 0.098 ns | 0.26 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 57.45 ns | 0.126 ns | 0.118 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 96 | 14.70 ns | 0.105 ns | 0.098 ns | 0.26 | - | - | - | - |
// | Pad3Shuffle4 | 3. AVX | Empty | 96 | 14.63 ns | 0.070 ns | 0.062 ns | 0.25 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 96 | 12.08 ns | 0.028 ns | 0.025 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 96 | 14.04 ns | 0.050 ns | 0.044 ns | 1.16 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 96 | 12.08 ns | 0.028 ns | 0.025 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 96 | 14.04 ns | 0.050 ns | 0.044 ns | 1.16 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. AVX | Empty | 96 | 13.90 ns | 0.086 ns | 0.080 ns | 1.15 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 202.67 ns | 2.010 ns | 1.678 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 384 | 25.54 ns | 0.060 ns | 0.053 ns | 0.13 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 202.67 ns | 2.010 ns | 1.678 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 384 | 25.54 ns | 0.060 ns | 0.053 ns | 0.13 | - | - | - | - |
// | Pad3Shuffle4 | 3. AVX | Empty | 384 | 25.72 ns | 0.139 ns | 0.130 ns | 0.13 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 384 | 60.35 ns | 0.080 ns | 0.071 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 384 | 25.18 ns | 0.388 ns | 0.324 ns | 0.42 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 384 | 60.35 ns | 0.080 ns | 0.071 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 384 | 25.18 ns | 0.388 ns | 0.324 ns | 0.42 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. AVX | Empty | 384 | 26.21 ns | 0.067 ns | 0.059 ns | 0.43 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 393.88 ns | 1.353 ns | 1.199 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 768 | 39.44 ns | 0.230 ns | 0.204 ns | 0.10 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 393.88 ns | 1.353 ns | 1.199 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 768 | 39.44 ns | 0.230 ns | 0.204 ns | 0.10 | - | - | - | - |
// | Pad3Shuffle4 | 3. AVX | Empty | 768 | 39.51 ns | 0.108 ns | 0.101 ns | 0.10 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 768 | 112.02 ns | 0.140 ns | 0.131 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 768 | 38.60 ns | 0.091 ns | 0.080 ns | 0.34 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 768 | 112.02 ns | 0.140 ns | 0.131 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 768 | 38.60 ns | 0.091 ns | 0.080 ns | 0.34 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. AVX | Empty | 768 | 38.18 ns | 0.100 ns | 0.084 ns | 0.34 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4 | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 777.95 ns | 1.719 ns | 1.342 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | COMPlus_EnableAVX=0 | 1536 | 73.11 ns | 0.090 ns | 0.075 ns | 0.09 | - | - | - | - |
// | Pad3Shuffle4 | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 777.95 ns | 1.719 ns | 1.342 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4 | 2. SSE | DOTNET_EnableAVX=0 | 1536 | 73.11 ns | 0.090 ns | 0.075 ns | 0.09 | - | - | - | - |
// | Pad3Shuffle4 | 3. AVX | Empty | 1536 | 73.41 ns | 0.125 ns | 0.117 ns | 0.09 | - | - | - | - |
// | | | | | | | | | | | | |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | COMPlus_EnableHWIntrinsic=0,COMPlus_FeatureSIMD=0 | 1536 | 218.14 ns | 0.377 ns | 0.334 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | COMPlus_EnableAVX=0 | 1536 | 72.55 ns | 1.418 ns | 1.184 ns | 0.33 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 1. No HwIntrinsics | DOTNET_EnableHWIntrinsic=0,DOTNET_FeatureSIMD=0 | 1536 | 218.14 ns | 0.377 ns | 0.334 ns | 1.00 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 2. SSE | DOTNET_EnableAVX=0 | 1536 | 72.55 ns | 1.418 ns | 1.184 ns | 0.33 | - | - | - | - |
// | Pad3Shuffle4FastFallback | 3. AVX | Empty | 1536 | 73.15 ns | 0.330 ns | 0.292 ns | 0.34 | - | - | - | - |

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save