Browse Source

Merge branch 'main' into icc-color-conversion

pull/1567/head
James Jackson-South 2 years ago
parent
commit
6e2c29c041
  1. 27
      .github/workflows/build-and-test.yml
  2. 3
      src/ImageSharp.ruleset
  3. 32
      src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
  4. 33
      src/ImageSharp/Formats/AnimatedImageMetadata.cs
  5. 12
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  6. 6
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  7. 4
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  8. 20
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  9. 32
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  10. 2
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  11. 2
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  12. 102
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  13. 37
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  14. 6
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  15. 43
      src/ImageSharp/Formats/IAnimatedImageEncoder.cs
  16. 11
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  17. 8
      src/ImageSharp/Formats/IFormatMetadata.cs
  18. 50
      src/ImageSharp/Formats/IQuantizingImageEncoder.cs
  19. 2
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  20. 20
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  21. 20
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  22. 2
      src/ImageSharp/Formats/ImageEncoder.cs
  23. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  24. 201
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  25. 3
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  26. 60
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  27. 96
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  28. 6
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  29. 6
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  30. 3
      src/ImageSharp/Formats/Png/PngEncoder.cs
  31. 50
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  32. 7
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  33. 6
      src/ImageSharp/Formats/Png/PngMetadata.cs
  34. 6
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  35. 22
      src/ImageSharp/Formats/QuantizingImageEncoder.cs
  36. 6
      src/ImageSharp/Formats/Tga/TgaMetadata.cs
  37. 168
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  38. 18
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  39. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  40. 155
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  41. 6
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  42. 27
      src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
  43. 19
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  44. 28
      src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
  45. 21
      src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
  46. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
  47. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs
  48. 7
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  49. 12
      src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs
  50. 7
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  51. 11
      src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
  52. 8
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  53. 7
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  54. 4
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  55. 20
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs
  56. 16
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  57. 4
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  58. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  59. 54
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  60. 30
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  61. 6
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  62. 626
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  63. 11
      src/ImageSharp/Image.Decode.cs
  64. 10
      src/ImageSharp/Image.cs
  65. 32
      src/ImageSharp/ImageFrame.cs
  66. 2
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  67. 24
      src/ImageSharp/ImageFrame{TPixel}.cs
  68. 5
      src/ImageSharp/ImageSharp.csproj
  69. 40
      src/ImageSharp/Image{TPixel}.cs
  70. 33
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  71. 16
      src/ImageSharp/Metadata/ImageMetadata.cs
  72. 14
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  73. 2
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  74. 2
      src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
  75. 6
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  76. 75
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  77. 8
      src/ImageSharp/Processing/Extensions/Convolution/BokehBlurExtensions.cs
  78. 14
      src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs
  79. 89
      src/ImageSharp/Processing/Extensions/Convolution/ConvolutionExtensions.cs
  80. 124
      src/ImageSharp/Processing/Extensions/Convolution/DetectEdgesExtensions.cs
  81. 21
      src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs
  82. 28
      src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs
  83. 17
      src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs
  84. 6
      src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
  85. 4
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  86. 6
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor{TPixel}.cs
  87. 56
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs
  88. 79
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs
  89. 46
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs
  90. 8
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs
  91. 2
      src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorProcessor{TPixel}.cs
  92. 20
      src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs
  93. 18
      src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs
  94. 6
      src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs
  95. 2
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  96. 29
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  97. 86
      src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs
  98. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
  99. 25
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  100. 7
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

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

@ -19,6 +19,31 @@ jobs:
isARM: isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options: 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 - os: ubuntu-latest
framework: net8.0 framework: net8.0
sdk: 8.0.x sdk: 8.0.x
@ -100,7 +125,7 @@ jobs:
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: | dotnet-version: |
8.0.x 9.0.x
- name: DotNet Build - name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }} if: ${{ matrix.options.sdk-preview != true }}

3
src/ImageSharp.ruleset

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="ImageSharp" ToolsVersion="17.0"> <RuleSet Name="ImageSharp" ToolsVersion="17.0">
<Include Path="..\shared-infrastructure\sixlabors.ruleset" Action="Default" /> <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> </RuleSet>

32
src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageFrameMetadata
{
/// <summary>
/// Gets or sets the frame color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the duration of the frame.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets the frame alpha blending mode.
/// </summary>
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the frame disposal mode.
/// </summary>
public FrameDisposalMode DisposalMode { get; set; }
}

33
src/ImageSharp/Formats/AnimatedImageMetadata.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats;
internal class AnimatedImageMetadata
{
/// <summary>
/// Gets or sets the shared color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the shared color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets 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"/>.
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
/// </remarks>
/// </summary>
public ushort RepeatCount { get; set; }
}

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

@ -575,7 +575,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 16 MaxColors = 16,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@ -623,7 +625,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 4 MaxColors = 4,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
@ -680,7 +684,9 @@ internal sealed class BmpEncoderCore
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 2 MaxColors = 2,
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale
}); });
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);

6
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -154,4 +154,10 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
/// <inheritdoc/> /// <inheritdoc/>
public BmpMetadata DeepClone() => new(this); public BmpMetadata DeepClone() => new(this);
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
} }

4
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -35,10 +35,6 @@ internal sealed class CurDecoderCore : IconDecoderCore
curMetadata.Compression = compression; curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable; curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
curMetadata.HotspotX = curFrameMetadata.HotspotX;
curMetadata.HotspotY = curFrameMetadata.HotspotY;
} }
} }
} }

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

@ -132,6 +132,16 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
EncodingHeight = this.EncodingHeight EncodingHeight = this.EncodingHeight
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -222,4 +232,14 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
ColorType = color ColorType = color
}; };
} }
private static byte Scale(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
}
} }

32
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -22,10 +22,6 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
private CurMetadata(CurMetadata other) private CurMetadata(CurMetadata other)
{ {
this.Compression = other.Compression; this.Compression = other.Compression;
this.HotspotX = other.HotspotX;
this.HotspotY = other.HotspotY;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel; this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0) if (other.ColorTable?.Length > 0)
@ -39,28 +35,6 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
/// </summary> /// </summary>
public IconFrameCompression Compression { get; set; } public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
/// </summary>
public ushort HotspotY { get; set; }
/// <summary>
/// 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. Derived from the root frame.
/// </summary>
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. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
@ -175,6 +149,12 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
ColorTable = this.ColorTable ColorTable = this.ColorTable
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

2
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -45,7 +45,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating. /// 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, /// 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. /// 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> /// </summary>
/// <remarks> /// <remarks>
/// Defaults to <see cref="Color.Transparent"/>. /// Defaults to <see cref="Color.Transparent"/>.

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

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

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

@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
/// </summary> /// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy; 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;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
@ -68,6 +81,8 @@ internal sealed class GifEncoderCore
this.hasQuantizer = encoder.Quantizer is not null; this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode; this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
} }
/// <summary> /// <summary>
@ -141,9 +156,12 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
} }
byte backgroundIndex = derivedTransparencyIndex >= 0 if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
? frameMetadata.TransparencyIndex {
: gifMetadata.BackgroundColorIndex; backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}
// Get the number of bits. // Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -161,7 +179,7 @@ internal sealed class GifEncoderCore
// Write application extensions. // Write application extensions.
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; 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); this.EncodeFirstFrame(stream, frameMetadata, quantized);
@ -169,7 +187,13 @@ internal sealed class GifEncoderCore
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode); this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
stream.WriteByte(GifConstants.EndIntroducer); stream.WriteByte(GifConstants.EndIntroducer);
@ -194,7 +218,8 @@ internal sealed class GifEncoderCore
Image<TPixel> image, Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette, ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex, int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode) FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (image.Frames.Count == 1) if (image.Frames.Count == 1)
@ -209,10 +234,20 @@ internal sealed class GifEncoderCore
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {
if (cancellationToken.IsCancellationRequested)
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}
return;
}
// Gather the metadata for this frame. // Gather the metadata for this frame.
ImageFrame<TPixel> currentFrame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
@ -291,6 +326,10 @@ internal sealed class GifEncoderCore
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. // Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) = (bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels( AnimationUtilities.DeDuplicatePixels(
@ -299,7 +338,7 @@ internal sealed class GifEncoderCore
currentFrame, currentFrame,
nextFrame, nextFrame,
encodingFrame, encodingFrame,
Color.Transparent, background,
true); true);
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
@ -428,14 +467,12 @@ internal sealed class GifEncoderCore
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue); private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
/// <summary> /// <summary>
/// Returns the index of the most transparent color in the palette. /// Returns the index of the transparent color in the palette.
/// </summary> /// </summary>
/// <param name="quantized">The current quantized frame.</param> /// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param> /// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns> /// <returns>The <see cref="int"/>.</returns>
/// The <see cref="int"/>.
/// </returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata) private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -463,6 +500,47 @@ internal sealed class GifEncoderCore
return index; 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> /// <summary>
/// Writes the file header signature and version to the stream. /// Writes the file header signature and version to the stream.
/// </summary> /// </summary>

37
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -126,40 +126,15 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/> /// <inheritdoc/>
public GifFrameMetadata DeepClone() => new(this); public GifFrameMetadata DeepClone() => new(this);
internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
{
// TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
int index = -1;
const float background = 1f;
if (metadata.ColorTable.HasValue)
{
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
for (int i = 0; i < colorTable.Length; i++)
{
Vector4 vector = colorTable[i].ToScaledVector4();
if (vector.W < background)
{
index = i;
}
}
}
bool hasTransparency = index >= 0;
return new()
{
LocalColorTable = metadata.ColorTable,
ColorTableMode = metadata.ColorTableMode,
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
DisposalMode = metadata.DisposalMode,
HasTransparency = hasTransparency,
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
};
}
} }

6
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -130,6 +130,12 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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 : ImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }
/// <inheritdoc/>
public ushort? RepeatCount { get; init; }
/// <inheritdoc/>
public bool? AnimateRootFrame { get; init; } = true;
}

11
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
@ -13,6 +15,15 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns> /// <returns>The <see cref="FormatConnectingFrameMetadata"/>.</returns>
FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata(); FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata();
/// <summary>
/// This method is called after a process has been applied to the image frame.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
} }
/// <summary> /// <summary>

8
src/ImageSharp/Formats/IFormatMetadata.cs

@ -21,6 +21,14 @@ public interface IFormatMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <returns>The <see cref="FormatConnectingMetadata"/>.</returns> /// <returns>The <see cref="FormatConnectingMetadata"/>.</returns>
FormatConnectingMetadata ToFormatConnectingMetadata(); FormatConnectingMetadata ToFormatConnectingMetadata();
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param>
void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>;
} }
/// <summary> /// <summary>

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 : ImageEncoder, 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; }
}

2
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -35,8 +35,6 @@ internal sealed class IcoDecoderCore : IconDecoderCore
curMetadata.Compression = compression; curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel; curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable; curMetadata.ColorTable = colorTable;
curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
} }
} }
} }

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

@ -125,6 +125,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
EncodingHeight = this.EncodingHeight EncodingHeight = this.EncodingHeight
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -217,4 +227,14 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
ColorType = color ColorType = color
}; };
} }
private static byte Scale(byte? value, int destination, float ratio)
{
if (value is null)
{
return (byte)Math.Clamp(destination, 0, 255);
}
return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
}
} }

20
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -22,8 +22,6 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
private IcoMetadata(IcoMetadata other) private IcoMetadata(IcoMetadata other)
{ {
this.Compression = other.Compression; this.Compression = other.Compression;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel; this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0) if (other.ColorTable?.Length > 0)
@ -37,18 +35,6 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
/// </summary> /// </summary>
public IconFrameCompression Compression { get; set; } public IconFrameCompression Compression { get; set; }
/// <summary>
/// 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. Derived from the root frame.
/// </summary>
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. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
@ -163,6 +149,12 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
ColorTable = this.ColorTable ColorTable = this.ColorTable
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

2
src/ImageSharp/Formats/ImageEncoder.cs

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

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -119,6 +119,8 @@ internal class HuffmanScanDecoder : IJpegScanDecoder
this.frame.AllocateComponents(); this.frame.AllocateComponents();
this.todo = this.restartInterval;
if (!this.frame.Progressive) if (!this.frame.Progressive)
{ {
this.ParseBaselineData(); this.ParseBaselineData();

201
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
/// </remarks> /// </remarks>
private readonly byte[] streamWriteBuffer; private readonly byte[] streamWriteBuffer;
private readonly int restartInterval;
/// <summary> /// <summary>
/// Number of jagged bits stored in <see cref="accumulatedBits"/> /// Number of jagged bits stored in <see cref="accumulatedBits"/>
/// </summary> /// </summary>
@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class. /// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class.
/// </summary> /// </summary>
/// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param> /// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="outputStream">Output stream for saving encoded data.</param> /// <param name="outputStream">Output stream for saving encoded data.</param>
public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream) public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
{ {
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit; int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)]; this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
this.emitWriteIndex = this.emitBuffer.Length; this.emitWriteIndex = this.emitBuffer.Length;
this.restartInterval = restartInterval;
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier]; this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
this.target = outputStream; this.target = outputStream;
@ -211,6 +216,9 @@ internal class HuffmanScanEncoder
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++) for (int i = 0; i < h; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -221,6 +229,13 @@ internal class HuffmanScanEncoder
for (nuint k = 0; k < (uint)w; k++) for (nuint k = 0; k < (uint)w; k++)
{ {
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteBlock( this.WriteBlock(
component, component,
ref Unsafe.Add(ref blockRef, k), ref Unsafe.Add(ref blockRef, k),
@ -231,6 +246,133 @@ internal class HuffmanScanEncoder
{ {
this.FlushToStream(); this.FlushToStream();
} }
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the DC coefficients for a given component's blocks in a scan.
/// </summary>
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeDcScan(Component component, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteDc(
component,
ref Unsafe.Add(ref blockRef, k),
ref dcHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the AC coefficients for a specified range of blocks in a component's scan.
/// </summary>
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
int restarts = 0;
int restartsToGo = this.restartInterval;
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
}
this.WriteAcBlock(
ref Unsafe.Add(ref blockRef, k),
start,
end,
ref acHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
} }
} }
@ -250,6 +392,9 @@ internal class HuffmanScanEncoder
int mcusPerColumn = frame.McusPerColumn; int mcusPerColumn = frame.McusPerColumn;
int mcusPerLine = frame.McusPerLine; int mcusPerLine = frame.McusPerLine;
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int j = 0; j < mcusPerColumn; j++) for (int j = 0; j < mcusPerColumn; j++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -260,6 +405,16 @@ internal class HuffmanScanEncoder
// Encode spectral to binary // Encode spectral to binary
for (int i = 0; i < mcusPerLine; i++) for (int i = 0; i < mcusPerLine; i++)
{ {
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
foreach (var component in frame.Components)
{
component.DcPredictor = 0;
}
}
// Scan an interleaved mcu... process components in order // Scan an interleaved mcu... process components in order
int mcuCol = mcu % mcusPerLine; int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < frame.Components.Length; k++) for (int k = 0; k < frame.Components.Length; k++)
@ -300,6 +455,17 @@ internal class HuffmanScanEncoder
{ {
this.FlushToStream(); this.FlushToStream();
} }
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
} }
} }
@ -371,25 +537,29 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes(); this.FlushRemainingBytes();
} }
private void WriteBlock( private void WriteDc(
Component component, Component component,
ref Block8x8 block, ref Block8x8 block,
ref HuffmanLut dcTable, ref HuffmanLut dcTable)
ref HuffmanLut acTable)
{ {
// Emit the DC delta. // Emit the DC delta.
int dc = block[0]; int dc = block[0];
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor); this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc; component.DcPredictor = dc;
}
private void WriteAcBlock(
ref Block8x8 block,
nint start,
nint end,
ref HuffmanLut acTable)
{
// Emit the AC components. // Emit the AC components.
int[] acHuffTable = acTable.Values; int[] acHuffTable = acTable.Values;
nint lastValuableIndex = block.GetLastNonZeroIndex();
int runLength = 0; int runLength = 0;
ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block); ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block);
for (nint zig = 1; zig <= lastValuableIndex; zig++) for (nint zig = start; zig < end; zig++)
{ {
const int zeroRun1 = 1 << 4; const int zeroRun1 = 1 << 4;
const int zeroRun16 = 16 << 4; const int zeroRun16 = 16 << 4;
@ -413,14 +583,25 @@ internal class HuffmanScanEncoder
} }
// if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over
// this can be done for any number of trailing zeros, even when all 63 ac values are zero if (runLength > 0)
// (Block8x8F.Size - 1) == 63 - last index of the mcu elements
if (lastValuableIndex != Block8x8F.Size - 1)
{ {
this.EmitHuff(acHuffTable, 0x00); this.EmitHuff(acHuffTable, 0x00);
} }
} }
private void WriteBlock(
Component component,
ref Block8x8 block,
ref HuffmanLut dcTable,
ref HuffmanLut acTable)
{
this.WriteDc(component, ref block, ref dcTable);
this.WriteAcBlock(ref block, 1, 64, ref acTable);
}
private void WriteRestart(int restart) =>
this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
/// <summary> /// <summary>
/// Emits the most significant count of bits to the buffer. /// Emits the most significant count of bits to the buffer.
/// </summary> /// </summary>

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

60
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
/// </summary> /// </summary>
private int? quality; private int? quality;
/// <summary>
/// Backing field for <see cref="ProgressiveScans"/>
/// </summary>
private int progressiveScans = 4;
/// <summary>
/// Backing field for <see cref="RestartInterval"/>
/// </summary>
private int restartInterval;
/// <summary> /// <summary>
/// Gets the quality, that will be used to encode the image. Quality /// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min). /// index must be between 1 and 100 (compression from max to min).
@ -33,6 +43,56 @@ public sealed class JpegEncoder : ImageEncoder
} }
} }
/// <summary>
/// Gets a value indicating whether progressive encoding is used.
/// </summary>
public bool Progressive { get; init; }
/// <summary>
/// Gets number of scans per component for progressive encoding.
/// Defaults to <value>4</value>.
/// </summary>
/// <remarks>
/// Number of scans must be between 2 and 64.
/// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
/// </remarks>
/// <exception cref="ArgumentException">Progressive scans must be in [2..64] range.</exception>
public int ProgressiveScans
{
get => this.progressiveScans;
init
{
if (value is < 2 or > 64)
{
throw new ArgumentException("Progressive scans must be in [2..64] range.");
}
this.progressiveScans = value;
}
}
/// <summary>
/// Gets numbers of MCUs between restart markers.
/// Defaults to <value>0</value>.
/// </summary>
/// <remarks>
/// Currently supported in progressive encoding only.
/// </remarks>
/// <exception cref="ArgumentException">Restart interval must be in [0..65535] range.</exception>
public int RestartInterval
{
get => this.restartInterval;
init
{
if (value is < 0 or > 65535)
{
throw new ArgumentException("Restart interval must be in [0..65535] range.");
}
this.restartInterval = value;
}
}
/// <summary> /// <summary>
/// Gets the component encoding mode. /// Gets the component encoding mode.
/// </summary> /// </summary>

96
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -100,12 +100,15 @@ internal sealed unsafe partial class JpegEncoderCore
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer); this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables. // Write the Huffman tables.
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream); HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer); this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables. // Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer); this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer);
// Write define restart interval
this.WriteDri(this.encoder.RestartInterval, buffer);
// Write scans with actual pixel data // Write scans with actual pixel data
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables); using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken); this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
@ -426,6 +429,25 @@ internal sealed unsafe partial class JpegEncoderCore
} }
} }
/// <summary>
/// Writes the DRI marker
/// </summary>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteDri(int restartInterval, Span<byte> buffer)
{
if (restartInterval <= 0)
{
return;
}
this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);
buffer[1] = (byte)(restartInterval & 0xff);
buffer[0] = (byte)(restartInterval >> 8);
this.outputStream.Write(buffer, 0, 2);
}
/// <summary> /// <summary>
/// Writes the App1 header. /// Writes the App1 header.
/// </summary> /// </summary>
@ -563,7 +585,8 @@ internal sealed unsafe partial class JpegEncoderCore
// Length (high byte, low byte), 8 + components * 3. // Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length); int markerlen = 8 + (3 * components.Length);
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer); byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
this.WriteMarkerHeader(marker, markerlen, buffer);
buffer[5] = (byte)components.Length; buffer[5] = (byte)components.Length;
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
buffer[1] = (byte)(height >> 8); buffer[1] = (byte)(height >> 8);
@ -597,7 +620,17 @@ internal sealed unsafe partial class JpegEncoderCore
/// </summary> /// </summary>
/// <param name="components">The collecction of component configuration items.</param> /// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param> /// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) =>
this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="spectralStart">Start of spectral selection</param>
/// <param name="spectralEnd">End of spectral selection</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer, byte spectralStart, byte spectralEnd)
{ {
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes: // Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - the marker length "\x00\x0c",
@ -630,8 +663,8 @@ internal sealed unsafe partial class JpegEncoderCore
buffer[i2 + 6] = (byte)tableSelectors; buffer[i2 + 6] = (byte)tableSelectors;
} }
buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection. buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
buffer[sosSize] = 0x3f; // Se - End of spectral selection. buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low) buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(buffer, 0, sosSize + 2); this.outputStream.Write(buffer, 0, sosSize + 2);
} }
@ -666,7 +699,14 @@ internal sealed unsafe partial class JpegEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (frame.Components.Length == 1) if (this.encoder.Progressive)
{
frame.AllocateComponents(fullScan: true);
spectralConverter.ConvertFull();
this.WriteProgressiveScans<TPixel>(frame, frameConfig, encoder, buffer, cancellationToken);
}
else if (frame.Components.Length == 1)
{ {
frame.AllocateComponents(fullScan: false); frame.AllocateComponents(fullScan: false);
@ -694,6 +734,50 @@ internal sealed unsafe partial class JpegEncoderCore
} }
} }
/// <summary>
/// Writes the progressive scans
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="frame">The current frame.</param>
/// <param name="frameConfig">The frame configuration.</param>
/// <param name="encoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void WriteProgressiveScans<TPixel>(
JpegFrame frame,
JpegFrameConfig frameConfig,
HuffmanScanEncoder encoder,
Span<byte> buffer,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Span<JpegComponentConfig> components = frameConfig.Components;
// Phase 1: DC scan
for (int i = 0; i < frame.Components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
encoder.EncodeDcScan(frame.Components[i], cancellationToken);
}
// Phase 2: AC scans
int acScans = this.encoder.ProgressiveScans - 1;
int valuesPerScan = 64 / acScans;
for (int scan = 0; scan < acScans; scan++)
{
int start = Math.Max(1, scan * valuesPerScan);
int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
for (int i = 0; i < components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
}
}
}
/// <summary> /// <summary>
/// Writes the header for a marker with the given length. /// Writes the header for a marker with the given length.
/// </summary> /// </summary>

6
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -199,6 +199,12 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
Quality = this.Quality, Quality = this.Quality,
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Pbm/PbmMetadata.cs

@ -129,6 +129,12 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
PixelTypeInfo = this.GetPixelTypeInfo(), PixelTypeInfo = this.GetPixelTypeInfo(),
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary> /// <summary>
/// Image encoder for writing image data to a stream in png format. /// Image encoder for writing image data to a stream in png format.
/// </summary> /// </summary>
public class PngEncoder : QuantizingImageEncoder public class PngEncoder : QuantizingAnimatedImageEncoder
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class. /// Initializes a new instance of the <see cref="PngEncoder"/> class.

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

@ -123,6 +123,24 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary> /// </summary>
private int derivedTransparencyIndex = -1; 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> /// <summary>
/// A reusable Crc32 hashing instance. /// A reusable Crc32 hashing instance.
/// </summary> /// </summary>
@ -139,6 +157,9 @@ internal sealed class PngEncoderCore : IDisposable
this.memoryAllocator = configuration.MemoryAllocator; this.memoryAllocator = configuration.MemoryAllocator;
this.encoder = encoder; this.encoder = encoder;
this.quantizer = encoder.Quantizer; this.quantizer = encoder.Quantizer;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.animateRootFrame = encoder.AnimateRootFrame;
} }
/// <summary> /// <summary>
@ -171,7 +192,7 @@ internal sealed class PngEncoderCore : IDisposable
if (clearTransparency) if (clearTransparency)
{ {
currentFrame = clonedFrame = currentFrame.Clone(); currentFrame = clonedFrame = currentFrame.Clone();
ClearTransparentPixels(currentFrame); ClearTransparentPixels(currentFrame, Color.Transparent);
} }
// Do not move this. We require an accurate bit depth for the header chunk. // Do not move this. We require an accurate bit depth for the header chunk.
@ -194,11 +215,15 @@ internal sealed class PngEncoderCore : IDisposable
if (image.Frames.Count > 1) 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 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)
{ {
FrameControl frameControl = new((uint)this.width, (uint)this.height); FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false); this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
@ -231,16 +256,24 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++) for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{ {
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[currentFrameIndex]; currentFrame = image.Frames[currentFrameIndex];
ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null; ImageFrame<TPixel>? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
frameMetadata = currentFrame.Metadata.GetPngMetadata(); frameMetadata = currentFrame.Metadata.GetPngMetadata();
bool blend = frameMetadata.BlendMode == FrameBlendMode.Over; bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
(bool difference, Rectangle bounds) = (bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels( AnimationUtilities.DeDuplicatePixels(
@ -249,12 +282,12 @@ internal sealed class PngEncoderCore : IDisposable
currentFrame, currentFrame,
nextFrame, nextFrame,
encodingFrame, encodingFrame,
Color.Transparent, background,
blend); blend);
if (clearTransparency) if (clearTransparency)
{ {
ClearTransparentPixels(encodingFrame); ClearTransparentPixels(encodingFrame, background);
} }
// Each frame control sequence number must be incremented by the number of frame data chunks that follow. // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
@ -291,12 +324,13 @@ internal sealed class PngEncoderCore : IDisposable
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param> /// <param name="clone">The cloned image frame where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone) /// <param name="color">The color to replace transparent pixels with.</param>
private static void ClearTransparentPixels<TPixel>(ImageFrame<TPixel> clone, Color color)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> clone.ProcessPixelRows(accessor => => clone.ProcessPixelRows(accessor =>
{ {
// TODO: We should be able to speed this up with SIMD and masking. // TODO: We should be able to speed this up with SIMD and masking.
Rgba32 transparent = Color.Transparent.ToPixel<Rgba32>(); Rgba32 transparent = color.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++) for (int y = 0; y < accessor.Height; y++)
{ {
Span<TPixel> span = accessor.GetRowSpan(y); Span<TPixel> span = accessor.GetRowSpan(y);

7
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png; namespace SixLabors.ImageSharp.Formats.Png;
@ -84,6 +85,12 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
}; };
} }
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -247,6 +247,12 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue), RepeatCount = (ushort)Numerics.Clamp(this.RepeatCount, 0, ushort.MaxValue),
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

6
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -88,6 +88,12 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

6
src/ImageSharp/Formats/Tga/TgaMetadata.cs

@ -94,6 +94,12 @@ public class TgaMetadata : IFormatMetadata<TgaMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

168
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -167,11 +167,18 @@ internal class TiffDecoderCore : ImageDecoderCore
this.byteOrder = reader.ByteOrder; this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff; this.isBigTiff = reader.IsBigTiff;
Size? size = null;
uint frameCount = 0; uint frameCount = 0;
foreach (ExifProfile ifd in directories) foreach (ExifProfile ifd in directories)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, cancellationToken); ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd, size, cancellationToken);
if (!size.HasValue)
{
size = frame.Size;
}
frames.Add(frame); frames.Add(frame);
framesMetadata.Add(frame.Metadata); framesMetadata.Add(frame.Metadata);
@ -181,19 +188,8 @@ internal class TiffDecoderCore : ImageDecoderCore
} }
} }
this.Dimensions = frames[0].Size;
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
// TODO: Tiff frames can have different sizes.
ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames)
{
if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
}
return new Image<TPixel>(this.configuration, metadata, frames); return new Image<TPixel>(this.configuration, metadata, frames);
} }
catch catch
@ -215,17 +211,21 @@ internal class TiffDecoderCore : ImageDecoderCore
IList<ExifProfile> directories = reader.Read(); IList<ExifProfile> directories = reader.Read();
List<ImageFrameMetadata> framesMetadata = []; List<ImageFrameMetadata> framesMetadata = [];
foreach (ExifProfile dir in directories) int width = 0;
int height = 0;
for (int i = 0; i < directories.Count; i++)
{ {
framesMetadata.Add(this.CreateFrameMetadata(dir)); (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) meta
} = this.CreateFrameMetadata(directories[i]);
ExifProfile rootFrameExifProfile = directories[0]; framesMetadata.Add(meta.FrameMetadata);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); width = Math.Max(width, meta.TiffMetadata.EncodingWidth);
height = Math.Max(height, meta.TiffMetadata.EncodingHeight);
}
int width = GetImageWidth(rootFrameExifProfile); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
int height = GetImageHeight(rootFrameExifProfile);
return new ImageInfo(new(width, height), metadata, framesMetadata); return new ImageInfo(new(width, height), metadata, framesMetadata);
} }
@ -235,31 +235,46 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="size">The previously determined root frame size if decoded.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
/// <returns>The tiff frame.</returns> /// <returns>The tiff frame.</returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken) private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, Size? size, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrameMetadata imageFrameMetaData = this.CreateFrameMetadata(tags); (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffFrameMetadata) metadata = this.CreateFrameMetadata(tags);
bool isTiled = this.VerifyAndParse(tags, imageFrameMetaData.GetTiffMetadata()); bool isTiled = this.VerifyAndParse(tags, metadata.TiffFrameMetadata);
int width = metadata.TiffFrameMetadata.EncodingWidth;
int height = metadata.TiffFrameMetadata.EncodingHeight;
// If size has a value and the width/height off the tiff is smaller we much capture the delta.
if (size.HasValue)
{
if (size.Value.Width < width || size.Value.Height < height)
{
TiffThrowHelper.ThrowNotSupported("Images with frames of size greater than the root frame are not supported.");
}
}
else
{
size = new Size(width, height);
}
int width = GetImageWidth(tags); ImageFrame<TPixel> frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
int height = GetImageHeight(tags);
ImageFrame<TPixel> frame = new(this.configuration, width, height, imageFrameMetaData);
if (isTiled) if (isTiled)
{ {
this.DecodeImageWithTiles(tags, frame, cancellationToken); this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
} }
else else
{ {
this.DecodeImageWithStrips(tags, frame, cancellationToken); this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
} }
return frame; return frame;
} }
private ImageFrameMetadata CreateFrameMetadata(ExifProfile tags) private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
{ {
ImageFrameMetadata imageFrameMetaData = new(); ImageFrameMetadata imageFrameMetaData = new();
if (!this.skipMetadata) if (!this.skipMetadata)
@ -267,9 +282,10 @@ internal class TiffDecoderCore : ImageDecoderCore
imageFrameMetaData.ExifProfile = tags; imageFrameMetaData.ExifProfile = tags;
} }
TiffFrameMetadata.Parse(imageFrameMetaData.GetTiffMetadata(), tags); TiffFrameMetadata tiffMetadata = TiffFrameMetadata.Parse(tags);
imageFrameMetaData.SetFormatMetadata(TiffFormat.Instance, tiffMetadata);
return imageFrameMetaData; return (imageFrameMetaData, tiffMetadata);
} }
/// <summary> /// <summary>
@ -278,8 +294,10 @@ internal class TiffDecoderCore : ImageDecoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param> /// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken) private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int rowsPerStrip; int rowsPerStrip;
@ -302,6 +320,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
this.DecodeStripsPlanar( this.DecodeStripsPlanar(
frame, frame,
width,
height,
rowsPerStrip, rowsPerStrip,
stripOffsets, stripOffsets,
stripByteCounts, stripByteCounts,
@ -311,6 +331,8 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
this.DecodeStripsChunky( this.DecodeStripsChunky(
frame, frame,
width,
height,
rowsPerStrip, rowsPerStrip,
stripOffsets, stripOffsets,
stripByteCounts, stripByteCounts,
@ -324,13 +346,13 @@ internal class TiffDecoderCore : ImageDecoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param> /// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param> /// <param name="frame">The image frame to decode into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken) private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
int width = pixels.Width;
int height = pixels.Height;
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueWidth)) if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueWidth))
{ {
@ -384,11 +406,20 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param> /// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The number of rows per strip of data.</param> /// <param name="rowsPerStrip">The number of rows per strip of data.</param>
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param> /// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param> /// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsPlanar<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int stripsPerPixel = this.BitsPerSample.Channels; int stripsPerPixel = this.BitsPerSample.Channels;
@ -403,18 +434,18 @@ internal class TiffDecoderCore : ImageDecoderCore
{ {
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{ {
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize); stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(uncompressedStripSize);
} }
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel); using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>(); TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
for (int i = 0; i < stripsPerPlane; i++) for (int i = 0; i < stripsPerPlane; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip;
int stripIndex = i; int stripIndex = i;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
@ -430,7 +461,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripIndex += stripsPerPlane; stripIndex += stripsPerPlane;
} }
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
} }
} }
finally finally
@ -447,39 +478,48 @@ internal class TiffDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param> /// <param name="frame">The image frame to decode data into.</param>
/// <param name="width">The width in px units of the frame data.</param>
/// <param name="height">The height in px units of the frame data.</param>
/// <param name="rowsPerStrip">The rows per strip.</param> /// <param name="rowsPerStrip">The rows per strip.</param>
/// <param name="stripOffsets">The strip offsets.</param> /// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param> /// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsChunky<TPixel>(
ImageFrame<TPixel> frame,
int width,
int height,
int rowsPerStrip,
Span<ulong> stripOffsets,
Span<ulong> stripByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{ {
rowsPerStrip = frame.Height; rowsPerStrip = height;
} }
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); int uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel; int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean); using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean);
Span<byte> stripBufferSpan = stripBuffer.GetSpan(); Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel); using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(width, bitsPerPixel);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>(); TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip ? rowsPerStrip
: frame.Height % rowsPerStrip; : height % rowsPerStrip;
int top = rowsPerStrip * stripIndex; int top = rowsPerStrip * stripIndex;
if (top + stripHeight > frame.Height) if (top + stripHeight > height)
{ {
// Make sure we ignore any strips that are not needed for the image (if too many are present). // Make sure we ignore any strips that are not needed for the image (if too many are present).
break; break;
@ -493,7 +533,7 @@ internal class TiffDecoderCore : ImageDecoderCore
stripBufferSpan, stripBufferSpan,
cancellationToken); cancellationToken);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
} }
} }
@ -790,38 +830,6 @@ internal class TiffDecoderCore : ImageDecoderCore
return bytesPerRow * height; return bytesPerRow * height;
} }
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number> width))
{
TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
}
DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
return (int)width.Value;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number> height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
} }

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

@ -189,11 +189,22 @@ internal sealed class TiffEncoderCore
long ifdOffset) long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Get the width and height of the frame.
// This can differ from the frame bounds in-memory if the image represents only
// a subregion.
TiffFrameMetadata frameMetaData = frame.Metadata.GetTiffMetadata();
int width = frameMetaData.EncodingWidth > 0 ? frameMetaData.EncodingWidth : frame.Width;
int height = frameMetaData.EncodingHeight > 0 ? frameMetaData.EncodingHeight : frame.Height;
width = Math.Min(width, frame.Width);
height = Math.Min(height, frame.Height);
Size encodingSize = new(width, height);
using TiffBaseCompressor compressor = TiffCompressorFactory.Create( using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
compression, compression,
writer.BaseStream, writer.BaseStream,
this.memoryAllocator, this.memoryAllocator,
frame.Width, width,
(int)bitsPerPixel, (int)bitsPerPixel,
this.compressionLevel, this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
@ -202,6 +213,7 @@ internal sealed class TiffEncoderCore
using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create( using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation, this.PhotometricInterpretation,
frame, frame,
encodingSize,
this.quantizer, this.quantizer,
this.pixelSamplingStrategy, this.pixelSamplingStrategy,
this.memoryAllocator, this.memoryAllocator,
@ -209,7 +221,7 @@ internal sealed class TiffEncoderCore
entriesCollector, entriesCollector,
(int)bitsPerPixel); (int)bitsPerPixel);
int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); int rowsPerStrip = CalcRowsPerStrip(height, colorWriter.BytesPerRow, this.CompressionType);
colorWriter.Write(compressor, rowsPerStrip); colorWriter.Write(compressor, rowsPerStrip);
@ -222,7 +234,7 @@ internal sealed class TiffEncoderCore
// Write the metadata for the frame // Write the metadata for the frame
entriesCollector.ProcessMetadata(frame, this.skipMetadata); entriesCollector.ProcessMetadata(frame, this.skipMetadata);
entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessFrameInfo(frame, encodingSize, imageMetadata);
entriesCollector.ProcessImageFormat(this); entriesCollector.ProcessImageFormat(this);
if (writer.Position % 2 != 0) if (writer.Position % 2 != 0)

10
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -24,8 +24,8 @@ internal class TiffEncoderEntriesCollector
public void ProcessMetadata(ImageFrame frame, bool skipMetadata) public void ProcessMetadata(ImageFrame frame, bool skipMetadata)
=> new MetadataProcessor(this).Process(frame, skipMetadata); => new MetadataProcessor(this).Process(frame, skipMetadata);
public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) public void ProcessFrameInfo(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
=> new FrameInfoProcessor(this).Process(frame, imageMetadata); => new FrameInfoProcessor(this).Process(frame, encodingSize, imageMetadata);
public void ProcessImageFormat(TiffEncoderCore encoder) public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(encoder); => new ImageFormatProcessor(this).Process(encoder);
@ -267,16 +267,16 @@ internal class TiffEncoderEntriesCollector
{ {
} }
public void Process(ImageFrame frame, ImageMetadata imageMetadata) public void Process(ImageFrame frame, Size encodingSize, ImageMetadata imageMetadata)
{ {
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth)
{ {
Value = (uint)frame.Width Value = (uint)encodingSize.Width
}); });
this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength)
{ {
Value = (uint)frame.Height Value = (uint)encodingSize.Height
}); });
this.ProcessResolution(imageMetadata); this.ProcessResolution(imageMetadata);

155
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp.Formats.Tiff;
@ -29,6 +30,8 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
this.PhotometricInterpretation = other.PhotometricInterpretation; this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor; this.Predictor = other.Predictor;
this.InkSet = other.InkSet; this.InkSet = other.InkSet;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
} }
/// <summary> /// <summary>
@ -61,13 +64,59 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
/// </summary> /// </summary>
public TiffInkSet? InkSet { get; set; } public TiffInkSet? InkSet { get; set; }
/// <summary>
/// Gets or sets the encoding width.
/// </summary>
public int EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height.
/// </summary>
public int EncodingHeight { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) public static TiffFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
=> new(); {
TiffFrameMetadata frameMetadata = new();
if (metadata.EncodingWidth.HasValue && metadata.EncodingHeight.HasValue)
{
frameMetadata.EncodingWidth = metadata.EncodingWidth.Value;
frameMetadata.EncodingHeight = metadata.EncodingHeight.Value;
}
return frameMetadata;
}
/// <inheritdoc/> /// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new(); => new()
{
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
// Overwrite the EXIF dimensional metadata with the encoding dimensions of the image.
destination.Metadata.ExifProfile?.SyncDimensions(this.EncodingWidth, this.EncodingHeight);
}
private static int Scale(int value, int destination, float ratio)
{
if (value <= 0)
{
return destination;
}
return Math.Min((int)MathF.Ceiling(value * ratio), destination);
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
@ -93,43 +142,75 @@ public class TiffFrameMetadata : IFormatFrameMetadata<TiffFrameMetadata>
/// </summary> /// </summary>
/// <param name="meta">The tiff frame meta data.</param> /// <param name="meta">The tiff frame meta data.</param>
/// <param name="profile">The Exif profile containing tiff frame directory tags.</param> /// <param name="profile">The Exif profile containing tiff frame directory tags.</param>
internal static void Parse(TiffFrameMetadata meta, ExifProfile profile) private static void Parse(TiffFrameMetadata meta, ExifProfile profile)
{ {
if (profile != null) meta.EncodingWidth = GetImageWidth(profile);
meta.EncodingHeight = GetImageHeight(profile);
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue)
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{ {
if (profile.TryGetValue(ExifTag.BitsPerSample, out IExifValue<ushort[]>? bitsPerSampleValue) meta.Compression = (TiffCompression)compressionValue.Value;
&& TiffBitsPerSample.TryParse(bitsPerSampleValue.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample.BitsPerPixel();
if (profile.TryGetValue(ExifTag.Compression, out IExifValue<ushort>? compressionValue))
{
meta.Compression = (TiffCompression)compressionValue.Value;
}
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? inkSetValue))
{
meta.InkSet = (TiffInkSet)inkSetValue.Value;
}
// TODO: Why do we remove this? Encoding should overwrite.
profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor);
} }
if (profile.TryGetValue(ExifTag.PhotometricInterpretation, out IExifValue<ushort>? photometricInterpretationValue))
{
meta.PhotometricInterpretation = (TiffPhotometricInterpretation)photometricInterpretationValue.Value;
}
if (profile.TryGetValue(ExifTag.Predictor, out IExifValue<ushort>? predictorValue))
{
meta.Predictor = (TiffPredictor)predictorValue.Value;
}
if (profile.TryGetValue(ExifTag.InkSet, out IExifValue<ushort>? inkSetValue))
{
meta.InkSet = (TiffInkSet)inkSetValue.Value;
}
// Remove values, we've explicitly captured them and they could change on encode.
profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor);
}
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageWidth, out IExifValue<Number>? width))
{
TiffThrowHelper.ThrowInvalidImageContentException("The TIFF image frame is missing the ImageWidth");
}
DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth));
return (int)width.Value;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
if (!exifProfile.TryGetValue(ExifTag.ImageLength, out IExifValue<Number>? height))
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
} }
} }

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

@ -180,6 +180,12 @@ public class TiffMetadata : IFormatMetadata<TiffMetadata>
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

27
src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs

@ -13,8 +13,15 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
{ {
private bool isDisposed; private bool isDisposed;
protected TiffBaseColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) protected TiffBaseColorWriter(
ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
{ {
this.Width = encodingSize.Width;
this.Height = encodingSize.Height;
this.Image = image; this.Image = image;
this.MemoryAllocator = memoryAllocator; this.MemoryAllocator = memoryAllocator;
this.Configuration = configuration; this.Configuration = configuration;
@ -26,10 +33,20 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
/// </summary> /// </summary>
public abstract int BitsPerPixel { get; } public abstract int BitsPerPixel { get; }
/// <summary>
/// Gets the width of the portion of the image to be encoded.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the portion of the image to be encoded.
/// </summary>
public int Height { get; }
/// <summary> /// <summary>
/// Gets the bytes per row. /// Gets the bytes per row.
/// </summary> /// </summary>
public int BytesPerRow => (int)(((uint)(this.Image.Width * this.BitsPerPixel) + 7) / 8); public int BytesPerRow => (int)(((uint)(this.Width * this.BitsPerPixel) + 7) / 8);
protected ImageFrame<TPixel> Image { get; } protected ImageFrame<TPixel> Image { get; }
@ -42,18 +59,18 @@ internal abstract class TiffBaseColorWriter<TPixel> : IDisposable
public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip) public virtual void Write(TiffBaseCompressor compressor, int rowsPerStrip)
{ {
DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer"); DebugGuard.IsTrue(this.BytesPerRow == compressor.BytesPerRow, "bytes per row of the compressor does not match tiff color writer");
int stripsCount = (this.Image.Height + rowsPerStrip - 1) / rowsPerStrip; int stripsCount = (this.Height + rowsPerStrip - 1) / rowsPerStrip;
uint[] stripOffsets = new uint[stripsCount]; uint[] stripOffsets = new uint[stripsCount];
uint[] stripByteCounts = new uint[stripsCount]; uint[] stripByteCounts = new uint[stripsCount];
int stripIndex = 0; int stripIndex = 0;
compressor.Initialize(rowsPerStrip); compressor.Initialize(rowsPerStrip);
for (int y = 0; y < this.Image.Height; y += rowsPerStrip) for (int y = 0; y < this.Height; y += rowsPerStrip)
{ {
long offset = compressor.Output.Position; long offset = compressor.Output.Position;
int height = Math.Min(rowsPerStrip, this.Image.Height - y); int height = Math.Min(rowsPerStrip, this.Height - y);
this.EncodeStrip(y, height, compressor); this.EncodeStrip(y, height, compressor);
long endOffset = compressor.Output.Position; long endOffset = compressor.Output.Position;

19
src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs

@ -21,11 +21,16 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
private IMemoryOwner<byte> bitStrip; private IMemoryOwner<byte> bitStrip;
public TiffBiColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffBiColorWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
// Convert image to black and white. // Convert image to black and white.
this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), new[] { image.Clone() }); this.imageBlackWhite = new Image<TPixel>(configuration, new ImageMetadata(), [image.Clone()]);
this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg)); this.imageBlackWhite.Mutate(img => img.BinaryDither(KnownDitherings.FloydSteinberg));
} }
@ -35,9 +40,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{ {
int width = this.Image.Width; int width = this.Width;
if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax) if (compressor.Method is TiffCompression.CcittGroup3Fax or TiffCompression.Ccitt1D or TiffCompression.CcittGroup4Fax)
{ {
// Special case for T4BitCompressor. // Special case for T4BitCompressor.
int stripPixels = width * height; int stripPixels = width * height;
@ -77,9 +82,9 @@ internal sealed class TiffBiColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
int bitIndex = 0; int bitIndex = 0;
int byteIndex = 0; int byteIndex = 0;
Span<byte> outputRow = rows[(outputRowIdx * this.BytesPerRow)..]; Span<byte> outputRow = rows[(outputRowIdx * this.BytesPerRow)..];
Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row); Span<TPixel> pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row)[..width];
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width);
for (int x = 0; x < this.Image.Width; x++) for (int x = 0; x < this.Width; x++)
{ {
int shift = 7 - bitIndex; int shift = 7 - bitIndex;
if (pixelAsGraySpan[x] == 255) if (pixelAsGraySpan[x] == 255)

28
src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs

@ -13,6 +13,7 @@ internal static class TiffColorWriterFactory
public static TiffBaseColorWriter<TPixel> Create<TPixel>( public static TiffBaseColorWriter<TPixel> Create<TPixel>(
TiffPhotometricInterpretation? photometricInterpretation, TiffPhotometricInterpretation? photometricInterpretation,
ImageFrame<TPixel> image, ImageFrame<TPixel> image,
Size encodingSize,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
@ -20,22 +21,15 @@ internal static class TiffColorWriterFactory
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel) int bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ => photometricInterpretation switch
switch (photometricInterpretation)
{ {
case TiffPhotometricInterpretation.PaletteColor: TiffPhotometricInterpretation.PaletteColor => new TiffPaletteWriter<TPixel>(image, encodingSize, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel),
return new TiffPaletteWriter<TPixel>(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel); TiffPhotometricInterpretation.BlackIsZero or TiffPhotometricInterpretation.WhiteIsZero => bitsPerPixel switch
case TiffPhotometricInterpretation.BlackIsZero: {
case TiffPhotometricInterpretation.WhiteIsZero: 1 => new TiffBiColorWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
return bitsPerPixel switch 16 => new TiffGrayL16Writer<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
{ _ => new TiffGrayWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector)
1 => new TiffBiColorWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector), },
16 => new TiffGrayL16Writer<TPixel>(image, memoryAllocator, configuration, entriesCollector), _ => new TiffRgbWriter<TPixel>(image, encodingSize, memoryAllocator, configuration, entriesCollector),
_ => new TiffGrayWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector) };
};
default:
return new TiffRgbWriter<TPixel>(image, memoryAllocator, configuration, entriesCollector);
}
}
} }

21
src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs

@ -12,35 +12,36 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
/// <summary> /// <summary>
/// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr). /// The base class for composite color types: 8-bit gray, 24-bit RGB (4-bit gray, 16-bit (565/555) RGB, 32-bit RGB, CMYK, YCbCr).
/// </summary> /// </summary>
/// <typeparam name="TPixel">The tpe of pixel format.</typeparam>
internal abstract class TiffCompositeColorWriter<TPixel> : TiffBaseColorWriter<TPixel> internal abstract class TiffCompositeColorWriter<TPixel> : TiffBaseColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private IMemoryOwner<byte> rowBuffer; private IMemoryOwner<byte> rowBuffer;
protected TiffCompositeColorWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) protected TiffCompositeColorWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{ {
if (this.rowBuffer == null) (this.rowBuffer ??= this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height)).Clear();
{
this.rowBuffer = this.MemoryAllocator.Allocate<byte>(this.BytesPerRow * height);
}
this.rowBuffer.Clear();
Span<byte> outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)]; Span<byte> outputRowSpan = this.rowBuffer.GetSpan()[..(this.BytesPerRow * height)];
int width = this.Image.Width; int width = this.Width;
using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width); using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width);
Span<TPixel> stripPixels = stripPixelBuffer.GetSpan(); Span<TPixel> stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height; int lastRow = y + height;
int stripPixelsRowIdx = 0; int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++) for (int row = y; row < lastRow; row++)
{ {
Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row); Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row)[..width];
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++; stripPixelsRowIdx++;
} }

12
src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffGrayL16Writer(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffGrayL16Writer(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffGrayL16Writer<TPixel> : TiffCompositeColorWriter<TPixe
public override int BitsPerPixel => 16; public override int BitsPerPixel => 16;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

12
src/ImageSharp/Formats/Tiff/Writers/TiffGrayWriter{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffGrayWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffGrayWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffGrayWriter<TPixel> : TiffCompositeColorWriter<TPixel>
public override int BitsPerPixel => 8; public override int BitsPerPixel => 8;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

7
src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs

@ -23,13 +23,14 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
public TiffPaletteWriter( public TiffPaletteWriter(
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
Size encodingSize,
IQuantizer quantizer, IQuantizer quantizer,
IPixelSamplingStrategy pixelSamplingStrategy, IPixelSamplingStrategy pixelSamplingStrategy,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
TiffEncoderEntriesCollector entriesCollector, TiffEncoderEntriesCollector entriesCollector,
int bitsPerPixel) int bitsPerPixel)
: base(frame, memoryAllocator, configuration, entriesCollector) : base(frame, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
DebugGuard.NotNull(quantizer, nameof(quantizer)); DebugGuard.NotNull(quantizer, nameof(quantizer));
DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy)); DebugGuard.NotNull(quantizer, nameof(pixelSamplingStrategy));
@ -49,7 +50,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
}); });
frameQuantizer.BuildPalette(pixelSamplingStrategy, frame); frameQuantizer.BuildPalette(pixelSamplingStrategy, frame);
this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); this.quantizedFrame = frameQuantizer.QuantizeFrame(frame, new Rectangle(Point.Empty, encodingSize));
this.AddColorMapTag(); this.AddColorMapTag();
} }
@ -60,7 +61,7 @@ internal sealed class TiffPaletteWriter<TPixel> : TiffBaseColorWriter<TPixel>
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor) protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{ {
int width = this.Image.Width; int width = this.quantizedFrame.Width;
if (this.BitsPerPixel == 4) if (this.BitsPerPixel == 4)
{ {

12
src/ImageSharp/Formats/Tiff/Writers/TiffRgbWriter{TPixel}.cs

@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel> internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
public TiffRgbWriter(ImageFrame<TPixel> image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector) public TiffRgbWriter(
: base(image, memoryAllocator, configuration, entriesCollector) ImageFrame<TPixel> image,
Size encodingSize,
MemoryAllocator memoryAllocator,
Configuration configuration,
TiffEncoderEntriesCollector entriesCollector)
: base(image, encodingSize, memoryAllocator, configuration, entriesCollector)
{ {
} }
@ -18,5 +23,6 @@ internal sealed class TiffRgbWriter<TPixel> : TiffCompositeColorWriter<TPixel>
public override int BitsPerPixel => 24; public override int BitsPerPixel => 24;
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer) => PixelOperations<TPixel>.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length); protected override void EncodePixels(Span<TPixel> pixels, Span<byte> buffer)
=> PixelOperations<TPixel>.Instance.ToRgb24Bytes(this.Configuration, pixels, buffer, pixels.Length);
} }

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

@ -183,7 +183,7 @@ internal class AlphaDecoder : IDisposable
else else
{ {
this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); 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. /// 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> /// </summary>
/// <param name="dec">The VP8L decoder.</param> /// <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 numRowsToProcess = dec.Height;
int width = dec.Width;
Span<uint> input = dec.Pixels.Memory.Span; Span<uint> input = dec.Pixels.Memory.Span;
Span<byte> output = this.Alpha.Memory.Span; Span<byte> output = this.Alpha.Memory.Span;
// Extract alpha (which is stored in the green plane). // Extract alpha (which is stored in the green plane).
// the final width (!= dec->width_)
int pixelCount = width * numRowsToProcess; int pixelCount = width * numRowsToProcess;
WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator);
ExtractGreen(input, output, pixelCount); ExtractGreen(input, output, pixelCount);

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

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

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

@ -236,7 +236,7 @@ internal class Vp8LEncoder : IDisposable
/// </summary> /// </summary>
public Vp8LHashChain HashChain { get; } 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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Write bytes from the bit-writer buffer to the stream. // Write bytes from the bit-writer buffer to the stream.
@ -258,7 +258,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation) if (hasAnimation)
{ {
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount); BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, repeatCount ?? webpMetadata.RepeatCount);
} }
return vp8x; return vp8x;
@ -315,8 +315,8 @@ internal class Vp8LEncoder : IDisposable
(uint)bounds.Width, (uint)bounds.Width,
(uint)bounds.Height, (uint)bounds.Height,
frameMetadata.FrameDelay, frameMetadata.FrameDelay,
frameMetadata.BlendMethod, frameMetadata.BlendMode,
frameMetadata.DisposalMethod) frameMetadata.DisposalMode)
.WriteHeaderTo(stream); .WriteHeaderTo(stream);
} }

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

@ -684,6 +684,7 @@ internal sealed class WebpLosslessDecoder
List<Vp8LTransform> transforms = decoder.Transforms; List<Vp8LTransform> transforms = decoder.Transforms;
for (int i = transforms.Count - 1; i >= 0; i--) for (int i = transforms.Count - 1; i >= 0; i--)
{ {
// TODO: Review these 1D allocations. They could conceivably exceed limits.
Vp8LTransform transform = transforms[i]; Vp8LTransform transform = transforms[i];
switch (transform.TransformType) switch (transform.TransformType)
{ {
@ -701,7 +702,11 @@ internal sealed class WebpLosslessDecoder
LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); LosslessUtils.ColorSpaceInverseTransform(transform, pixelData);
break; break;
case Vp8LTransformType.ColorIndexingTransform: 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; break;
} }
} }

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

@ -495,8 +495,8 @@ internal class Vp8Encoder : IDisposable
(uint)bounds.Width, (uint)bounds.Width,
(uint)bounds.Height, (uint)bounds.Height,
frameMetadata.FrameDelay, frameMetadata.FrameDelay,
frameMetadata.BlendMethod, frameMetadata.BlendMode,
frameMetadata.DisposalMethod) frameMetadata.DisposalMode)
.WriteHeaderTo(stream); .WriteHeaderTo(stream);
} }

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

@ -667,12 +667,12 @@ internal static unsafe class Vp8Encoding
// V block. // V block.
dst = dst[8..]; dst = dst[8..];
if (top != default) if (!top.IsEmpty)
{ {
top = top[8..]; top = top[8..];
} }
if (left != default) if (!left.IsEmpty)
{ {
left = left[16..]; left = left[16..];
} }
@ -701,7 +701,7 @@ internal static unsafe class Vp8Encoding
private static void VerticalPred(Span<byte> dst, Span<byte> top, int size) 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++) 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) 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. left = left[1..]; // in the reference implementation, left starts at - 1.
for (int j = 0; j < size; j++) 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) 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 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++) 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. // is equivalent to VE prediction where you just copy the top samples.
// Note that if top samples are not available, the default value is // Note that if top samples are not available, the default value is
// then 129, and not 127 as in the VerticalPred case. // then 129, and not 127 as in the VerticalPred case.
if (top != default) if (!top.IsEmpty)
{ {
VerticalPred(dst, top, size); VerticalPred(dst, top, size);
} }
@ -774,14 +774,14 @@ internal static unsafe class Vp8Encoding
{ {
int dc = 0; int dc = 0;
int j; int j;
if (top != default) if (!top.IsEmpty)
{ {
for (j = 0; j < size; j++) for (j = 0; j < size; j++)
{ {
dc += top[j]; dc += top[j];
} }
if (left != default) if (!left.IsEmpty)
{ {
// top and left present. // top and left present.
left = left[1..]; // in the reference implementation, left starts at -1. left = left[1..]; // in the reference implementation, left starts at -1.
@ -798,7 +798,7 @@ internal static unsafe class Vp8Encoding
dc = (dc + round) >> shift; dc = (dc + round) >> shift;
} }
else if (left != default) else if (!left.IsEmpty)
{ {
// left but no top. // left but no top.
left = left[1..]; // in the reference implementation, left starts at -1. 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; uint uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2;
YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst); YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst);
if (bottomY != default) if (!bottomY.IsEmpty)
{ {
uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2;
YuvToBgr(bottomY[0], (int)uv0 & 0xff, (int)(uv0 >> 16), bottomDst); 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 - 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)..]); YuvToBgr(topY[xMul2 - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst[((xMul2 - 0) * xStep)..]);
if (bottomY != default) if (!bottomY.IsEmpty)
{ {
uv0 = (diag03 + luv) >> 1; uv0 = (diag03 + luv) >> 1;
uv1 = (diag12 + uv) >> 1; uv1 = (diag12 + uv) >> 1;
@ -85,7 +85,7 @@ internal static class YuvConversion
{ {
uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2;
YuvToBgr(topY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst[((len - 1) * xStep)..]); 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; uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2;
YuvToBgr(bottomY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst[((len - 1) * xStep)..]); 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 u0t = (topU[0] + uDiag) >> 1;
int v0t = (topV[0] + vDiag) >> 1; int v0t = (topV[0] + vDiag) >> 1;
YuvToBgr(topY[0], u0t, v0t, topDst); YuvToBgr(topY[0], u0t, v0t, topDst);
if (bottomY != default) if (!bottomY.IsEmpty)
{ {
int u0b = (curU[0] + uDiag) >> 1; int u0b = (curU[0] + uDiag) >> 1;
int v0b = (curV[0] + vDiag) >> 1; int v0b = (curV[0] + vDiag) >> 1;
@ -134,7 +134,7 @@ internal static class YuvConversion
ref byte topVRef = ref MemoryMarshal.GetReference(topV); ref byte topVRef = ref MemoryMarshal.GetReference(topV);
ref byte curURef = ref MemoryMarshal.GetReference(curU); ref byte curURef = ref MemoryMarshal.GetReference(curU);
ref byte curVRef = ref MemoryMarshal.GetReference(curV); 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) 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> tmpTopDst = ru[(4 * 32)..];
Span<byte> tmpBottomDst = tmpTopDst[(4 * 32)..]; Span<byte> tmpBottomDst = tmpTopDst[(4 * 32)..];
Span<byte> tmpTop = tmpBottomDst[(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(topU[uvPos..], curU[uvPos..], leftOver, ru);
UpSampleLastBlock(topV[uvPos..], curV[uvPos..], leftOver, rv); UpSampleLastBlock(topV[uvPos..], curV[uvPos..], leftOver, rv);
topY[pos..len].CopyTo(tmpTop); topY[pos..len].CopyTo(tmpTop);
if (bottomY != default) if (!bottomY.IsEmpty)
{ {
bottomY[pos..len].CopyTo(tmpBottom); bottomY[pos..len].CopyTo(tmpBottom);
ConvertYuvToBgrWithBottomYSse41(tmpTop, tmpBottom, tmpTopDst, tmpBottomDst, ru, rv, 0, xStep); 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)..]); tmpTopDst[..((len - pos) * xStep)].CopyTo(topDst[(pos * xStep)..]);
if (bottomY != default) if (!bottomY.IsEmpty)
{ {
tmpBottomDst[..((len - pos) * xStep)].CopyTo(bottomDst[(pos * xStep)..]); tmpBottomDst[..((len - pos) * xStep)].CopyTo(bottomDst[(pos * xStep)..]);
} }

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

@ -220,8 +220,8 @@ internal class WebpAnimationDecoder : IDisposable
{ {
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDelay = frameData.Duration; frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod; frameMetadata.BlendMode = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod; frameMetadata.DisposalMode = frameData.DisposalMethod;
} }
/// <summary> /// <summary>

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Image encoder for writing an image to a stream in the Webp format. /// Image encoder for writing an image to a stream in the Webp format.
/// </summary> /// </summary>
public sealed class WebpEncoder : ImageEncoder public sealed class WebpEncoder : AnimatedImageEncoder
{ {
/// <summary> /// <summary>
/// Gets the webp file format used. Either lossless or lossy. /// Gets the webp file format used. Either lossless or lossy.

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.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
@ -78,6 +77,19 @@ internal sealed class WebpEncoderCore
/// </summary> /// </summary>
private readonly WebpFileFormatType? fileFormat; 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> /// <summary>
/// The global configuration. /// The global configuration.
/// </summary> /// </summary>
@ -103,6 +115,8 @@ internal sealed class WebpEncoderCore
this.skipMetadata = encoder.SkipMetadata; this.skipMetadata = encoder.SkipMetadata;
this.nearLossless = encoder.NearLossless; this.nearLossless = encoder.NearLossless;
this.nearLosslessQuality = encoder.NearLosslessQuality; this.nearLosslessQuality = encoder.NearLosslessQuality;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
} }
/// <summary> /// <summary>
@ -147,7 +161,7 @@ internal sealed class WebpEncoderCore
long initialPosition = stream.Position; long initialPosition = stream.Position;
bool hasAlpha = false; bool hasAlpha = false;
WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation); WebpVp8X vp8x = encoder.EncodeHeader(image, stream, hasAnimation, this.repeatCount);
// Encode the first frame. // Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
@ -156,20 +170,28 @@ internal sealed class WebpEncoderCore
if (hasAnimation) if (hasAnimation)
{ {
FrameDisposalMode previousDisposal = frameMetadata.DisposalMethod; FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
// Encode additional frames // Encode additional frames
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata(); 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) = (bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels( AnimationUtilities.DeDuplicatePixels(
@ -178,7 +200,7 @@ internal sealed class WebpEncoderCore
currentFrame, currentFrame,
nextFrame, nextFrame,
encodingFrame, encodingFrame,
Color.Transparent, background,
blend, blend,
ClampingMode.Even); ClampingMode.Even);
@ -197,7 +219,7 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation); hasAlpha |= animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
previousFrame = currentFrame; previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod; previousDisposal = frameMetadata.DisposalMode;
} }
} }
@ -229,22 +251,30 @@ internal sealed class WebpEncoderCore
// Encode the first frame. // Encode the first frame.
ImageFrame<TPixel> previousFrame = image.Frames.RootFrame; ImageFrame<TPixel> previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata(); WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
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 // Encode additional frames
// This frame is reused to store de-duplicated pixel buffers. // This frame is reused to store de-duplicated pixel buffers.
using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size()); using ImageFrame<TPixel> encodingFrame = new(image.Configuration, previousFrame.Size);
for (int i = 1; i < image.Frames.Count; i++) for (int i = 1; i < image.Frames.Count; i++)
{ {
if (cancellationToken.IsCancellationRequested)
{
break;
}
ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame; ImageFrame<TPixel>? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame<TPixel> currentFrame = image.Frames[i]; ImageFrame<TPixel> currentFrame = image.Frames[i];
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
frameMetadata = currentFrame.Metadata.GetWebpMetadata(); 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) = (bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels( AnimationUtilities.DeDuplicatePixels(
@ -253,7 +283,7 @@ internal sealed class WebpEncoderCore
currentFrame, currentFrame,
nextFrame, nextFrame,
encodingFrame, encodingFrame,
Color.Transparent, background,
blend, blend,
ClampingMode.Even); ClampingMode.Even);
@ -273,7 +303,7 @@ internal sealed class WebpEncoderCore
hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata); hasAlpha |= animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
previousFrame = currentFrame; previousFrame = currentFrame;
previousDisposal = frameMetadata.DisposalMethod; previousDisposal = frameMetadata.DisposalMode;
} }
encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition); encoder.EncodeFooter(image, in vp8x, hasAlpha, stream, initialPosition);

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

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
@ -22,19 +24,21 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
private WebpFrameMetadata(WebpFrameMetadata other) private WebpFrameMetadata(WebpFrameMetadata other)
{ {
this.FrameDelay = other.FrameDelay; this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod; this.DisposalMode = other.DisposalMode;
this.BlendMethod = other.BlendMethod; this.BlendMode = other.BlendMode;
} }
/// <summary> /// <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> /// </summary>
public FrameBlendMode BlendMethod { get; set; } public FrameBlendMode BlendMode { get; set; }
/// <summary> /// <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> /// </summary>
public FrameDisposalMode DisposalMethod { get; set; } public FrameDisposalMode DisposalMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the frame duration. The time to wait before displaying the next frame, /// Gets or sets the frame duration. The time to wait before displaying the next frame,
@ -47,8 +51,8 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
=> new() => new()
{ {
FrameDelay = (uint)metadata.Duration.TotalMilliseconds, FrameDelay = (uint)metadata.Duration.TotalMilliseconds,
BlendMethod = metadata.BlendMode, BlendMode = metadata.BlendMode,
DisposalMethod = GetMode(metadata.DisposalMode) DisposalMode = GetMode(metadata.DisposalMode)
}; };
/// <inheritdoc/> /// <inheritdoc/>
@ -57,10 +61,16 @@ public class WebpFrameMetadata : IFormatFrameMetadata<WebpFrameMetadata>
{ {
ColorTableMode = FrameColorTableMode.Global, ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay), Duration = TimeSpan.FromMilliseconds(this.FrameDelay),
DisposalMode = this.DisposalMethod, DisposalMode = this.DisposalMode,
BlendMode = this.BlendMethod, BlendMode = this.BlendMode,
}; };
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

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

@ -145,6 +145,12 @@ public class WebpMetadata : IFormatMetadata<WebpMetadata>
BackgroundColor = this.BackgroundColor BackgroundColor = this.BackgroundColor
}; };
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

626
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -3,6 +3,7 @@
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.IO;
@ -14,42 +15,19 @@ namespace SixLabors.ImageSharp.IO;
/// </summary> /// </summary>
internal sealed class ChunkedMemoryStream : Stream internal sealed class ChunkedMemoryStream : Stream
{ {
// The memory allocator. private readonly MemoryChunkBuffer memoryChunkBuffer;
private readonly MemoryAllocator allocator; private long length;
private long position;
// Data private int bufferIndex;
private MemoryChunk? memoryChunk; private int chunkIndex;
// 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 bool isDisposed; 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> /// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class. /// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary> /// </summary>
/// <param name="allocator">The memory allocator.</param> /// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(MemoryAllocator allocator) public ChunkedMemoryStream(MemoryAllocator allocator)
{ => this.memoryChunkBuffer = new(allocator);
this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator;
}
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanRead => !this.isDisposed; public override bool CanRead => !this.isDisposed;
@ -66,25 +44,7 @@ internal sealed class ChunkedMemoryStream : Stream
get get
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
return this.length;
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;
} }
} }
@ -94,93 +54,35 @@ internal sealed class ChunkedMemoryStream : Stream
get get
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
return this.position;
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;
} }
set set
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
this.SetPosition(value);
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;
}
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Flush()
{
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
switch (origin) this.Position = origin switch
{ {
case SeekOrigin.Begin: SeekOrigin.Begin => (int)offset,
this.Position = offset; SeekOrigin.Current => (int)(this.Position + offset),
break; SeekOrigin.End => (int)(this.Length + offset),
_ => throw new ArgumentOutOfRangeException(nameof(offset)),
case SeekOrigin.Current: };
this.Position += offset;
break;
case SeekOrigin.End:
this.Position = this.Length + offset;
break;
default:
ThrowInvalidSeek();
break;
}
return this.Position; return this.position;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -188,39 +90,13 @@ internal sealed class ChunkedMemoryStream : Stream
=> throw new NotSupportedException(); => throw new NotSupportedException();
/// <inheritdoc/> /// <inheritdoc/>
protected override void Dispose(bool disposing) public override int ReadByte()
{
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()
{ {
Unsafe.SkipInit(out byte b);
return this.Read(MemoryMarshal.CreateSpan(ref b, 1)) == 1 ? b : -1;
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
Guard.NotNull(buffer, nameof(buffer)); 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."; const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
return this.ReadImpl(buffer.AsSpan(offset, count)); return this.Read(buffer.AsSpan(offset, count));
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public override int Read(Span<byte> buffer)
public override int Read(Span<byte> buffer) => this.ReadImpl(buffer);
private int ReadImpl(Span<byte> buffer)
{ {
this.EnsureNotDisposed(); this.EnsureNotDisposed();
if (this.readChunk is null) int offset = 0;
{ int count = buffer.Length;
if (this.memoryChunk is null)
{
return 0;
}
this.readChunk = this.memoryChunk; long remaining = this.length - this.position;
this.readOffset = 0; if (remaining <= 0)
{
// Already at the end of the stream, nothing to read
return 0;
} }
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer; if (remaining > count)
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{ {
chunkSize = this.writeOffset; remaining = count;
} }
// 'remaining' can be less than the provided buffer length.
int bytesToRead = (int)remaining;
int bytesRead = 0; int bytesRead = 0;
int offset = 0; while (bytesToRead > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
int count = buffer.Length;
while (count > 0)
{ {
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 n = remainingBytesInCurrentChunk;
if (this.readChunk.Next is null) moveToNextChunk = true;
{
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;
}
} }
int readCount = Math.Min(count, chunkSize - this.readOffset); // Read n bytes from the current chunk
chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n).CopyTo(buffer.Slice(offset, n));
offset += readCount; bytesToRead -= n;
count -= readCount; offset += n;
this.readOffset += readCount; bytesRead += n;
bytesRead += readCount;
}
return bytesRead;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int ReadByte()
{
this.EnsureNotDisposed();
if (this.readChunk is null) if (moveToNextChunk)
{
if (this.memoryChunk is null)
{ {
return 0; this.chunkIndex = 0;
this.bufferIndex++;
} }
else
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)
{ {
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/> /// <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) public override void Write(byte[] buffer, int offset, int count)
{ {
Guard.NotNull(buffer, nameof(buffer)); 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."; const string bufferMessage = "Offset subtracted from the buffer length is less than count.";
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage);
this.WriteImpl(buffer.AsSpan(offset, count)); this.Write(buffer.AsSpan(offset, count));
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Write(ReadOnlySpan<byte> buffer)
public override void Write(ReadOnlySpan<byte> buffer) => this.WriteImpl(buffer);
private void WriteImpl(ReadOnlySpan<byte> buffer)
{ {
this.EnsureNotDisposed(); 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.memoryChunkBuffer.Expand();
this.writeChunk = this.memoryChunk; remaining = this.memoryChunkBuffer.Length - this.position;
this.writeOffset = 0;
} }
Guard.NotNull(this.writeChunk); int bytesToWrite = count;
int bytesWritten = 0;
Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan(); while (bytesToWrite > 0 && this.bufferIndex != this.memoryChunkBuffer.Length)
int chunkSize = this.writeChunk.Length;
int count = buffer.Length;
int offset = 0;
while (count > 0)
{ {
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 n = remainingBytesInCurrentChunk;
this.writeChunk.Next = this.AllocateMemoryChunk(); moveToNextChunk = true;
this.writeChunk = this.writeChunk.Next;
this.writeOffset = 0;
chunkBuffer = this.writeChunk.Buffer.GetSpan();
chunkSize = this.writeChunk.Length;
} }
int copyCount = Math.Min(count, chunkSize - this.writeOffset); // Write n bytes to the current chunk
buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); buffer.Slice(offset, n).CopyTo(chunk.Buffer.Slice(this.chunkIndex, n));
bytesToWrite -= n;
offset += n;
bytesWritten += n;
offset += copyCount; if (moveToNextChunk)
count -= copyCount; {
this.writeOffset += copyCount; this.chunkIndex = 0;
this.bufferIndex++;
}
else
{
this.chunkIndex += n;
}
} }
this.position += bytesWritten;
this.length += bytesWritten;
} }
/// <inheritdoc/> /// <summary>
public override void WriteByte(byte value) /// 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(); this.EnsureNotDisposed();
if (this.memoryChunk is null) this.Position = 0;
long remaining = this.length - this.position;
if (remaining <= 0)
{ {
this.memoryChunk = this.AllocateMemoryChunk(); // Already at the end of the stream, nothing to read
this.writeChunk = this.memoryChunk; return;
this.writeOffset = 0;
} }
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; // Read n bytes from the current chunk
int chunkSize = this.writeChunk.Length; stream.Write(chunk.Buffer.Memory.Span.Slice(this.chunkIndex, n));
bytesToRead -= n;
bytesRead += n;
if (this.writeOffset == chunkSize) if (moveToNextChunk)
{ {
// Allocate a new chunk if the current one is full this.chunkIndex = 0;
this.writeChunk.Next = this.AllocateMemoryChunk(); this.bufferIndex++;
this.writeChunk = this.writeChunk.Next; }
this.writeOffset = 0; else
chunkBuffer = this.writeChunk.Buffer; {
this.chunkIndex += n;
}
} }
chunkBuffer.GetSpan()[this.writeOffset++] = value; this.position += bytesRead;
} }
/// <summary> /// <summary>
/// Copy entire buffer into an array. /// Writes the stream contents to a byte array, regardless of the <see cref="Position"/> property.
/// </summary> /// </summary>
/// <returns>The <see cref="T:byte[]"/>.</returns> /// <returns>A new <see cref="T:byte[]"/>.</returns>
public byte[] ToArray() public byte[] ToArray()
{ {
int length = (int)this.Length; // This will throw if stream is closed this.EnsureNotDisposed();
byte[] copy = new byte[this.Length]; long position = this.position;
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.Position = 0;
_ = this.Read(copy, 0, copy.Length);
this.Position = position;
return copy; return copy;
} }
/// <summary> /// <inheritdoc/>
/// Write remainder of this stream to another stream. protected override void Dispose(bool disposing)
/// </summary>
/// <param name="stream">The stream to write to.</param>
public void WriteTo(Stream stream)
{ {
this.EnsureNotDisposed(); if (this.isDisposed)
{
Guard.NotNull(stream, nameof(stream)); 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.bufferIndex = 0;
this.readOffset = 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; this.position = newPosition;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null) // 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 // Loop through the current chunks, as we increment the chunk index, we subtract the length of the chunk
// point just past last byte of last chunk when done) // from the offset. Once the offset is less than the length of the chunk, we have found the correct chunk.
// loop until end of chunks is found while (offset != 0)
while (true)
{ {
if (this.readOffset == chunkSize) int chunkLength = this.memoryChunkBuffer[currentChunkIndex].Length;
if (offset < chunkLength)
{ {
// Exit if no more chunks are currently available // Found the correct chunk and the corresponding index
if (this.readChunk.Next is null) break;
{
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;
}
} }
int writeCount = chunkSize - this.readOffset; offset -= chunkLength;
stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); currentChunkIndex++;
this.readOffset = chunkSize;
} }
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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -507,48 +383,66 @@ internal sealed class ChunkedMemoryStream : Stream
} }
[MethodImpl(MethodImplOptions.NoInlining)] [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 sealed class MemoryChunkBuffer : IDisposable
private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); {
private readonly List<MemoryChunk> memoryChunks = new();
private readonly MemoryAllocator allocator;
private readonly int allocatorCapacity;
private bool isDisposed;
[MethodImpl(MethodImplOptions.NoInlining)] public MemoryChunkBuffer(MemoryAllocator allocator)
private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); {
this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] public int ChunkCount => this.memoryChunks.Count;
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++)));
return new MemoryChunk(buffer) public long Length { get; private set; }
public MemoryChunk this[int index] => this.memoryChunks[index];
public void Expand()
{ {
Next = null, IMemoryOwner<byte> buffer =
Length = buffer.Length() this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.ChunkCount)));
};
}
private static void ReleaseMemoryChunks(MemoryChunk? chunk) MemoryChunk chunk = new(buffer)
{ {
while (chunk != null) Length = buffer.Length()
};
this.memoryChunks.Add(chunk);
this.Length += chunk.Length;
}
public void Dispose()
{ {
chunk.Dispose(); if (!this.isDisposed)
chunk = chunk.Next; {
foreach (MemoryChunk chunk in this.memoryChunks)
{
chunk.Dispose();
}
this.memoryChunks.Clear();
this.Length = 0;
this.isDisposed = true;
}
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetChunkSize(int i) 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. // Increment chunks sizes with moderate speed, but without using too many buffers from the
// https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 // same ArrayPool bucket of the default MemoryAllocator.
#pragma warning disable IDE1006 // Naming Styles // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
const int _128K = 1 << 17; const int b128K = 1 << 17;
const int _4M = 1 << 22; const int b4M = 1 << 22;
return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; return i < 16 ? b128K * (1 << (int)((uint)i / 4)) : b4M;
#pragma warning restore IDE1006 // Naming Styles }
} }
private sealed class MemoryChunk : IDisposable private sealed class MemoryChunk : IDisposable
@ -559,27 +453,15 @@ internal sealed class ChunkedMemoryStream : Stream
public IMemoryOwner<byte> Buffer { get; } public IMemoryOwner<byte> Buffer { get; }
public MemoryChunk? Next { get; set; }
public int Length { get; init; } public int Length { get; init; }
private void Dispose(bool disposing) public void Dispose()
{ {
if (!this.isDisposed) if (!this.isDisposed)
{ {
if (disposing) this.Buffer.Dispose();
{
this.Buffer.Dispose();
}
this.isDisposed = true; 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 // Does the given stream contain enough data to fit in the header for the format
// and does that data match the format specification? // and does that data match the format specification?
// Individual formats should still check since they are public. // Individual formats should still check since they are public.
IImageFormat? format = null;
foreach (IImageFormatDetector formatDetector in configuration.ImageFormatsManager.FormatDetectors) foreach (IImageFormatDetector formatDetector in configuration.ImageFormatsManager.FormatDetectors)
{ {
if (formatDetector.HeaderSize <= headersBuffer.Length && formatDetector.TryDetectFormat(headersBuffer, out IImageFormat? attemptFormat)) 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> /// <summary>

10
src/ImageSharp/Image.cs

@ -72,12 +72,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <summary> /// <summary>
/// Gets any metadata associated with the image. /// Gets any metadata associated with the image.
/// </summary> /// </summary>
public ImageMetadata Metadata { get; } public ImageMetadata Metadata { get; private set; }
/// <summary> /// <summary>
/// Gets the size of the image in px units. /// Gets the size of the image in px units.
/// </summary> /// </summary>
public Size Size { get; internal set; } public Size Size { get; private set; }
/// <summary> /// <summary>
/// Gets the bounds of the image. /// Gets the bounds of the image.
@ -185,6 +185,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider
/// <param name="size">The <see cref="Size"/>.</param> /// <param name="size">The <see cref="Size"/>.</param>
protected void UpdateSize(Size size) => this.Size = size; protected void UpdateSize(Size size) => this.Size = size;
/// <summary>
/// Updates the metadata of the image after mutation.
/// </summary>
/// <param name="metadata">The <see cref="Metadata"/>.</param>
protected void UpdateMetadata(ImageMetadata metadata) => this.Metadata = metadata;
/// <summary> /// <summary>
/// Disposes the object and frees resources for the Garbage Collector. /// Disposes the object and frees resources for the Garbage Collector.
/// </summary> /// </summary>

32
src/ImageSharp/ImageFrame.cs

@ -25,25 +25,24 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata)
{ {
this.Configuration = configuration; this.Configuration = configuration;
this.Width = width; this.Size = new(width, height);
this.Height = height;
this.Metadata = metadata; this.Metadata = metadata;
} }
/// <summary> /// <summary>
/// Gets the width. /// Gets the frame width in px units.
/// </summary> /// </summary>
public int Width { get; private set; } public int Width => this.Size.Width;
/// <summary> /// <summary>
/// Gets the height. /// Gets the frame height in px units.
/// </summary> /// </summary>
public int Height { get; private set; } public int Height => this.Size.Height;
/// <summary> /// <summary>
/// Gets the metadata of the frame. /// Gets the metadata of the frame.
/// </summary> /// </summary>
public ImageFrameMetadata Metadata { get; } public ImageFrameMetadata Metadata { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public Configuration Configuration { get; } public Configuration Configuration { get; }
@ -51,8 +50,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <summary> /// <summary>
/// Gets the size of the frame. /// Gets the size of the frame.
/// </summary> /// </summary>
/// <returns>The <see cref="Size"/></returns> public Size Size { get; private set; }
public Size Size() => new(this.Width, this.Height);
/// <summary> /// <summary>
/// Gets the bounds of the frame. /// Gets the bounds of the frame.
@ -77,12 +75,14 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>; where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>;
/// <summary> /// <summary>
/// Updates the size of the image frame. /// Updates the size of the image frame after mutation.
/// </summary> /// </summary>
/// <param name="size">The size.</param> /// <param name="size">The <see cref="Size"/>.</param>
internal void UpdateSize(Size size) protected void UpdateSize(Size size) => this.Size = size;
{
this.Width = size.Width; /// <summary>
this.Height = size.Height; /// Updates the metadata of the image frame after mutation.
} /// </summary>
/// <param name="metadata">The <see cref="Metadata"/>.</param>
protected void UpdateMetadata(ImageFrameMetadata metadata) => this.Metadata = metadata;
} }

2
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -414,7 +414,7 @@ public sealed class ImageFrameCollection<TPixel> : ImageFrameCollection, IEnumer
{ {
ImageFrame<TPixel> result = new( ImageFrame<TPixel> result = new(
this.parent.Configuration, this.parent.Configuration,
source.Size(), source.Size,
source.Metadata.DeepClone()); source.Metadata.DeepClone());
source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup);
return result; return result;

24
src/ImageSharp/ImageFrame{TPixel}.cs

@ -322,7 +322,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <exception cref="ArgumentException">ImageFrame{TPixel}.CopyTo(): target must be of the same size!</exception> /// <exception cref="ArgumentException">ImageFrame{TPixel}.CopyTo(): target must be of the same size!</exception>
internal void CopyTo(Buffer2D<TPixel> target) internal void CopyTo(Buffer2D<TPixel> target)
{ {
if (this.Size() != target.Size()) if (this.Size != target.Size())
{ {
throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target)); throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target));
} }
@ -331,17 +331,29 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
} }
/// <summary> /// <summary>
/// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. /// Switches the buffers used by the image and the pixel source meaning that the Image will "own" the buffer
/// from the pixelSource and the pixel source will now own the Image buffer.
/// </summary> /// </summary>
/// <param name="pixelSource">The pixel source.</param> /// <param name="source">The pixel source.</param>
internal void SwapOrCopyPixelsBufferFrom(ImageFrame<TPixel> pixelSource) internal void SwapOrCopyPixelsBufferFrom(ImageFrame<TPixel> source)
{ {
Guard.NotNull(pixelSource, nameof(pixelSource)); Guard.NotNull(source, nameof(source));
Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, pixelSource.PixelBuffer); Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer);
this.UpdateSize(this.PixelBuffer.Size()); this.UpdateSize(this.PixelBuffer.Size());
} }
/// <summary>
/// Copies the metadata from the source image.
/// </summary>
/// <param name="source">The metadata source.</param>
internal void CopyMetadataFrom(ImageFrame<TPixel> source)
{
Guard.NotNull(source, nameof(source));
this.UpdateMetadata(source.Metadata);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

5
src/ImageSharp/ImageSharp.csproj

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

40
src/ImageSharp/Image{TPixel}.cs

@ -160,7 +160,7 @@ public sealed class Image<TPixel> : Image
/// <summary> /// <summary>
/// Gets the root frame. /// Gets the root frame.
/// </summary> /// </summary>
private IPixelSource<TPixel> PixelSourceUnsafe => this.frames.RootFrameUnsafe; private ImageFrame<TPixel> PixelSourceUnsafe => this.frames.RootFrameUnsafe;
/// <summary> /// <summary>
/// Gets or sets the pixel at the specified position. /// Gets or sets the pixel at the specified position.
@ -324,7 +324,7 @@ public sealed class Image<TPixel> : Image
} }
/// <summary> /// <summary>
/// Clones the current image /// Clones the current image.
/// </summary> /// </summary>
/// <returns>Returns a new image with all the same metadata as the original.</returns> /// <returns>Returns a new image with all the same metadata as the original.</returns>
public Image<TPixel> Clone() => this.Clone(this.Configuration); public Image<TPixel> Clone() => this.Clone(this.Configuration);
@ -395,22 +395,42 @@ public sealed class Image<TPixel> : Image
} }
/// <summary> /// <summary>
/// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. /// Switches the buffers used by the image and the pixel source meaning that the Image will
/// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer.
/// </summary> /// </summary>
/// <param name="pixelSource">The pixel source.</param> /// <param name="source">The pixel source.</param>
internal void SwapOrCopyPixelsBuffersFrom(Image<TPixel> pixelSource) internal void SwapOrCopyPixelsBuffersFrom(Image<TPixel> source)
{ {
Guard.NotNull(pixelSource, nameof(pixelSource)); Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed(); this.EnsureNotDisposed();
ImageFrameCollection<TPixel> sourceFrames = pixelSource.Frames; ImageFrameCollection<TPixel> sourceFrames = source.Frames;
for (int i = 0; i < this.frames.Count; i++) for (int i = 0; i < this.frames.Count; i++)
{ {
this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]); this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]);
} }
this.UpdateSize(pixelSource.Size); this.UpdateSize(source.Size);
}
/// <summary>
/// Copies the metadata from the source image.
/// </summary>
/// <param name="source">The metadata source.</param>
internal void CopyMetadataFrom(Image<TPixel> source)
{
Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed();
ImageFrameCollection<TPixel> sourceFrames = source.Frames;
for (int i = 0; i < this.frames.Count; i++)
{
this.frames[i].CopyMetadataFrom(sourceFrames[i]);
}
this.UpdateMetadata(source.Metadata);
} }
private static Size ValidateFramesAndGetSize(IEnumerable<ImageFrame<TPixel>> frames) private static Size ValidateFramesAndGetSize(IEnumerable<ImageFrame<TPixel>> frames)
@ -419,9 +439,9 @@ public sealed class Image<TPixel> : Image
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames)); ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
Size rootSize = rootFrame.Size(); Size rootSize = rootFrame.Size;
if (frames.Any(f => f.Size() != rootSize)) if (frames.Any(f => f.Size != rootSize))
{ {
throw new ArgumentException("The provided frames must be of the same size.", nameof(frames)); throw new ArgumentException("The provided frames must be of the same size.", nameof(frames));
} }

33
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata; namespace SixLabors.ImageSharp.Metadata;
@ -110,16 +111,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
&& this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata))
{ {
TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata());
this.formatMetadata[key] = derivedMeta; this.SetFormatMetadata(key, derivedMeta);
return derivedMeta; return derivedMeta;
} }
TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata();
this.formatMetadata[key] = newMeta; this.SetFormatMetadata(key, newMeta);
return newMeta; return newMeta;
} }
internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value) /// <summary>
/// Sets the metadata value associated with the specified key.
/// </summary>
/// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam>
/// <typeparam name="TFormatFrameMetadata">The type of format frame metadata.</typeparam>
/// <param name="key">The key of the value to set.</param>
/// <param name="value">The value to set.</param>
public void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
where TFormatMetadata : class where TFormatMetadata : class
where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata> where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata>
=> this.formatMetadata[key] = value; => this.formatMetadata[key] = value;
@ -143,4 +151,23 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
/// Synchronizes the profiles with the current metadata. /// Synchronizes the profiles with the current metadata.
/// </summary> /// </summary>
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
/// <summary>
/// This method is called after a process has been applied to the image frame.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param>
internal void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
// Always updated using the full frame dimensions.
// Individual format frame metadata will update with sub region dimensions if appropriate.
this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
foreach (KeyValuePair<IImageFormat, IFormatFrameMetadata> meta in this.formatMetadata)
{
meta.Value.AfterFrameApply(source, destination);
}
}
} }

16
src/ImageSharp/Metadata/ImageMetadata.cs

@ -230,6 +230,22 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
/// </summary> /// </summary>
internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this); internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
/// <summary>
/// This method is called after a process has been applied to the image.
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image.</param>
internal void AfterImageApply<TPixel>(Image<TPixel> destination)
where TPixel : unmanaged, IPixel<TPixel>
{
this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
foreach (KeyValuePair<IImageFormat, IFormatMetadata> meta in this.formatMetadata)
{
meta.Value.AfterImageApply(destination);
}
}
internal PixelTypeInfo GetDecodedPixelTypeInfo() internal PixelTypeInfo GetDecodedPixelTypeInfo()
{ {
// None found. Check if we have a decoded format to convert from. // None found. Check if we have a decoded format to convert from.

14
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -298,6 +299,19 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution); this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution);
} }
internal void SyncDimensions(int width, int height)
{
if (this.TryGetValue(ExifTag.PixelXDimension, out _))
{
this.SetValue(ExifTag.PixelXDimension, width);
}
if (this.TryGetValue(ExifTag.PixelYDimension, out _))
{
this.SetValue(ExifTag.PixelYDimension, height);
}
}
/// <summary> /// <summary>
/// Synchronizes the profiles with the specified metadata. /// Synchronizes the profiles with the specified metadata.
/// </summary> /// </summary>

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

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

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

@ -142,7 +142,7 @@ internal sealed partial class IccDataReader
ushort channelCount = this.ReadUInt16(); ushort channelCount = this.ReadUInt16();
var colorant = (IccColorantEncoding)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) // 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 // 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 && return arrayValid &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && Enum.IsDefined(this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && Enum.IsDefined(this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && Enum.IsDefined(this.Header.RenderingIntent) &&
this.Header.Size is >= minSize and < maxSize; this.Header.Size is >= minSize and < maxSize;
} }

75
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -11,8 +11,29 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary> /// </summary>
public class AffineTransformBuilder public class AffineTransformBuilder
{ {
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new(); private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = [];
private readonly List<Func<Size, Matrix3x2>> boundsMatrixFactories = new();
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
public AffineTransformBuilder()
: this(TransformSpace.Pixel)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the affine transform.
/// </param>
public AffineTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the affine transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees /// Prepends a rotation matrix using the given rotation angle in degrees
@ -31,8 +52,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians) public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend( => this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size), size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin. /// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -68,9 +88,7 @@ public class AffineTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians) public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append( => this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin. /// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -145,9 +163,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY) public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend( => this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
/// <summary> /// <summary>
/// Prepends a centered skew matrix from the give angles in radians. /// Prepends a centered skew matrix from the give angles in radians.
@ -156,9 +172,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend( => this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin. /// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -187,9 +201,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY) public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append( => this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
/// <summary> /// <summary>
/// Appends a centered skew matrix from the give angles in radians. /// Appends a centered skew matrix from the give angles in radians.
@ -198,9 +210,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append( => this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin. /// Appends a skew matrix using the given angles in degrees at the given origin.
@ -267,7 +277,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Prepend(_ => matrix, _ => matrix); return this.Prepend(_ => matrix);
} }
/// <summary> /// <summary>
@ -283,7 +293,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{ {
CheckDegenerate(matrix); CheckDegenerate(matrix);
return this.Append(_ => matrix, _ => matrix); return this.Append(_ => matrix);
} }
/// <summary> /// <summary>
@ -291,7 +301,8 @@ public class AffineTransformBuilder
/// </summary> /// </summary>
/// <param name="sourceSize">The source image size.</param> /// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <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> /// <summary>
/// Returns the combined transform matrix for a given source rectangle. /// Returns the combined transform matrix for a given source rectangle.
@ -335,18 +346,8 @@ public class AffineTransformBuilder
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle) public Size GetTransformedSize(Rectangle sourceRectangle)
{ {
Size size = sourceRectangle.Size; Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
// Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Func<Size, Matrix3x2> factory in this.boundsMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(size, matrix);
} }
private static void CheckDegenerate(Matrix3x2 matrix) private static void CheckDegenerate(Matrix3x2 matrix)
@ -357,17 +358,15 @@ public class AffineTransformBuilder
} }
} }
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory) private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory)
{ {
this.transformMatrixFactories.Insert(0, transformFactory); this.transformMatrixFactories.Insert(0, transformFactory);
this.boundsMatrixFactories.Insert(0, boundsFactory);
return this; return this;
} }
private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory) private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory)
{ {
this.transformMatrixFactories.Add(transformFactory); this.transformMatrixFactories.Add(transformFactory);
this.boundsMatrixFactories.Add(boundsFactory);
return this; return this;
} }
} }

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

@ -44,13 +44,13 @@ public static class BokehBlurExtensions
/// Applies a bokeh blur to the image. /// Applies a bokeh blur to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </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> /// <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); => 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. /// Applies a box blur to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="radius">The 'radius' value representing the size of the area to sample.</param>
/// <param name="borderWrapModeX"> /// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param> /// </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. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param> /// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) public static IImageProcessingContext BoxBlur(
{ this IImageProcessingContext source,
var processor = new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY); Rectangle rectangle,
return source.ApplyProcessor(processor, 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> /// </summary>
/// <param name="source">The current image processing context.</param> /// <param name="source">The current image processing context.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges(this IImageProcessingContext source) => public static IImageProcessingContext DetectEdges(this IImageProcessingContext source)
DetectEdges(source, KnownEdgeDetectorKernels.Sobel); => DetectEdges(source, KnownEdgeDetectorKernels.Sobel);
/// <summary> /// <summary>
/// Detects any edges within the image. /// 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. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, Rectangle rectangle)
this IImageProcessingContext source, => DetectEdges(source, rectangle, KnownEdgeDetectorKernels.Sobel);
Rectangle rectangle) =>
DetectEdges(source, KnownEdgeDetectorKernels.Sobel, rectangle);
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// 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="source">The current image processing context.</param>
/// <param name="kernel">The 2D edge detector kernel.</param> /// <param name="kernel">The 2D edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetector2DKernel kernel)
this IImageProcessingContext source, => DetectEdges(source, kernel, true);
EdgeDetector2DKernel kernel) =>
DetectEdges(source, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>.
@ -57,49 +53,41 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetector2DKernel kernel, EdgeDetector2DKernel kernel,
bool grayscale) bool grayscale)
{ => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale));
var processor = new EdgeDetector2DProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// Detects any edges within the image operating in grayscale mode.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <param name="source">The current image processing context.</param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <param name="rectangle"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="kernel">The 2D edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetector2DKernel kernel, Rectangle rectangle,
Rectangle rectangle) => EdgeDetector2DKernel kernel)
DetectEdges(source, kernel, true, rectangle); => DetectEdges(source, rectangle, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetector2DKernel"/>.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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="kernel">The 2D edge detector kernel.</param>
/// <param name="grayscale"> /// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection. /// Whether to convert the image to grayscale before performing edge detection.
/// </param> /// </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> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetector2DKernel kernel, EdgeDetector2DKernel kernel,
bool grayscale, bool grayscale)
Rectangle rectangle) => source.ApplyProcessor(new EdgeDetector2DProcessor(kernel, grayscale), rectangle);
{
var processor = new EdgeDetector2DProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// 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="source">The current image processing context.</param>
/// <param name="kernel">The edge detector kernel.</param> /// <param name="kernel">The edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorKernel kernel)
this IImageProcessingContext source, => DetectEdges(source, kernel, true);
EdgeDetectorKernel kernel) =>
DetectEdges(source, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>.
@ -125,66 +111,56 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetectorKernel kernel, EdgeDetectorKernel kernel,
bool grayscale) bool grayscale)
{ => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale));
var processor = new EdgeDetectorProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// Detects any edges within the image operating in grayscale mode.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <param name="source">The current image processing context.</param>
/// <param name="kernel">The edge detector kernel.</param>
/// <param name="rectangle"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="kernel">The edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetectorKernel kernel, Rectangle rectangle,
Rectangle rectangle) => EdgeDetectorKernel kernel)
DetectEdges(source, kernel, true, rectangle); => DetectEdges(source, rectangle, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetectorKernel"/>.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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="kernel">The edge detector kernel.</param>
/// <param name="grayscale"> /// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection. /// Whether to convert the image to grayscale before performing edge detection.
/// </param> /// </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> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetectorKernel kernel, EdgeDetectorKernel kernel,
bool grayscale, bool grayscale)
Rectangle rectangle) => source.ApplyProcessor(new EdgeDetectorProcessor(kernel, grayscale), rectangle);
{
var processor = new EdgeDetectorProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// Detects any edges within the image operating in grayscale mode.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(this IImageProcessingContext source, EdgeDetectorCompassKernel kernel)
this IImageProcessingContext source, => DetectEdges(source, kernel, true);
EdgeDetectorCompassKernel kernel) =>
DetectEdges(source, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="grayscale">
/// Whether to convert the image to grayscale before performing edge detection. /// Whether to convert the image to grayscale before performing edge detection.
/// </param> /// </param>
@ -193,47 +169,39 @@ public static class DetectEdgesExtensions
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetectorCompassKernel kernel, EdgeDetectorCompassKernel kernel,
bool grayscale) bool grayscale)
{ => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale));
var processor = new EdgeDetectorCompassProcessor(kernel, grayscale);
source.ApplyProcessor(processor);
return source;
}
/// <summary> /// <summary>
/// Detects any edges within the image operating in grayscale mode. /// Detects any edges within the image operating in grayscale mode.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <param name="source">The current image processing context.</param>
/// <param name="kernel">Thecompass edge detector kernel.</param>
/// <param name="rectangle"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="kernel">The compass edge detector kernel.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
EdgeDetectorCompassKernel kernel, Rectangle rectangle,
Rectangle rectangle) => EdgeDetectorCompassKernel kernel)
DetectEdges(source, kernel, true, rectangle); => DetectEdges(source, rectangle, kernel, true);
/// <summary> /// <summary>
/// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>. /// Detects any edges within the image using a <see cref="EdgeDetectorCompassKernel"/>.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </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> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DetectEdges( public static IImageProcessingContext DetectEdges(
this IImageProcessingContext source, this IImageProcessingContext source,
Rectangle rectangle,
EdgeDetectorCompassKernel kernel, EdgeDetectorCompassKernel kernel,
bool grayscale, bool grayscale)
Rectangle rectangle) => source.ApplyProcessor(new EdgeDetectorCompassProcessor(kernel, grayscale), rectangle);
{
var processor = new EdgeDetectorCompassProcessor(kernel, grayscale);
source.ApplyProcessor(processor, rectangle);
return source;
}
} }

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

@ -32,22 +32,25 @@ public static class GaussianBlurExtensions
/// Applies a Gaussian blur to the image. /// Applies a Gaussian blur to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <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); => source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle);
/// <summary> /// <summary>
/// Applies a Gaussian blur to the image. /// Applies a Gaussian blur to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="borderWrapModeX"> /// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param> /// </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. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param> /// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) public static IImageProcessingContext GaussianBlur(
{ this IImageProcessingContext source,
var processor = new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY); Rectangle rectangle,
return source.ApplyProcessor(processor, 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> /// </summary>
/// <param name="source">The current image processing context.</param> /// <param name="source">The current image processing context.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source) => public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source)
source.ApplyProcessor(new GaussianSharpenProcessor()); => source.ApplyProcessor(new GaussianSharpenProcessor());
/// <summary> /// <summary>
/// Applies a Gaussian sharpening filter to the image. /// 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="source">The current image processing context.</param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param> /// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma) => public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma)
source.ApplyProcessor(new GaussianSharpenProcessor(sigma)); => source.ApplyProcessor(new GaussianSharpenProcessor(sigma));
/// <summary> /// <summary>
/// Applies a Gaussian sharpening filter to the image. /// Applies a Gaussian sharpening filter to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen( public static IImageProcessingContext GaussianSharpen(
this IImageProcessingContext source, this IImageProcessingContext source,
float sigma, Rectangle rectangle,
Rectangle rectangle) => float sigma) =>
source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle); source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle);
/// <summary> /// <summary>
/// Applies a Gaussian sharpening filter to the image. /// Applies a Gaussian sharpening filter to the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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"> /// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter. /// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param> /// </param>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
/// <param name="borderWrapModeX"> /// <param name="borderWrapModeX">
/// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </param> /// </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. /// The <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </param> /// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) public static IImageProcessingContext GaussianSharpen(
{ this IImageProcessingContext source,
var processor = new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY); Rectangle rectangle,
return source.ApplyProcessor(processor, 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. /// Whether the filter is applied to alpha as well as the color channels.
/// </param> /// </param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns> /// <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)); => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha));
/// <summary> /// <summary>
/// Applies a median blur on the image. /// Applies a median blur on the image.
/// </summary> /// </summary>
/// <param name="source">The current image processing context.</param> /// <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="radius">The radius of the area to find the median for.</param>
/// <param name="preserveAlpha"> /// <param name="preserveAlpha">
/// Whether the filter is applied to alpha as well as the color channels. /// Whether the filter is applied to alpha as well as the color channels.
/// </param> /// </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> /// <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); => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle);
} }

6
src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs

@ -48,7 +48,6 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
Image<TPixel> clone = this.CreateTarget(); Image<TPixel> clone = this.CreateTarget();
this.CheckFrameCount(this.Source, clone); this.CheckFrameCount(this.Source, clone);
Configuration configuration = this.Configuration;
this.BeforeImageApply(clone); this.BeforeImageApply(clone);
for (int i = 0; i < this.Source.Frames.Count; i++) for (int i = 0; i < this.Source.Frames.Count; i++)
@ -77,9 +76,10 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
{ {
clone = ((ICloningImageProcessor<TPixel>)this).CloneAndExecute(); clone = ((ICloningImageProcessor<TPixel>)this).CloneAndExecute();
// We now need to move the pixel data/size data from the clone to the source. // We now need to move the pixel data/size data and any metadata from the clone to the source.
this.CheckFrameCount(this.Source, clone); this.CheckFrameCount(this.Source, clone);
this.Source.SwapOrCopyPixelsBuffersFrom(clone); this.Source.SwapOrCopyPixelsBuffersFrom(clone);
this.Source.CopyMetadataFrom(clone);
} }
finally finally
{ {
@ -157,7 +157,7 @@ public abstract class CloningImageProcessor<TPixel> : ICloningImageProcessor<TPi
Size destinationSize = this.GetDestinationSize(); Size destinationSize = this.GetDestinationSize();
// We will always be creating the clone even for mutate because we may need to resize the canvas. // We will always be creating the clone even for mutate because we may need to resize the canvas.
var destinationFrames = new ImageFrame<TPixel>[source.Frames.Count]; ImageFrame<TPixel>[] destinationFrames = new ImageFrame<TPixel>[source.Frames.Count];
for (int i = 0; i < destinationFrames.Length; i++) for (int i = 0; i < destinationFrames.Length; i++)
{ {
destinationFrames[i] = new ImageFrame<TPixel>( destinationFrames[i] = new ImageFrame<TPixel>(

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

@ -96,7 +96,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
} }
// Create a 0-filled buffer to use to store the result of the component convolutions // Create a 0-filled buffer to use to store the result of the component convolutions
using Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size(), AllocationOptions.Clean); using Buffer2D<Vector4> processingBuffer = this.Configuration.MemoryAllocator.Allocate2D<Vector4>(source.Size, AllocationOptions.Clean);
// Perform the 1D convolutions on all the kernel components and accumulate the results // Perform the 1D convolutions on all the kernel components and accumulate the results
this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer); this.OnFrameApplyCore(source, sourceRectangle, this.Configuration, processingBuffer);
@ -134,7 +134,7 @@ internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
Buffer2D<Vector4> processingBuffer) Buffer2D<Vector4> processingBuffer)
{ {
// Allocate the buffer with the intermediate convolution results // Allocate the buffer with the intermediate convolution results
using Buffer2D<ComplexVector4> firstPassBuffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size()); using Buffer2D<ComplexVector4> firstPassBuffer = configuration.MemoryAllocator.Allocate2D<ComplexVector4>(source.Size);
// Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width // Unlike in the standard 2 pass convolution processor, we use a rectangle of 1x the interest width
// to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation. // to speedup the actual convolution, by applying bulk pixel conversion and clamping calculation.

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

@ -62,14 +62,14 @@ internal class Convolution2DProcessor<TPixel> : ImageProcessor<TPixel>
source.CopyTo(targetPixels); 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. // Since the kernel sizes are identical we can use a single map.
map.BuildSamplingOffsetMap(this.KernelY, interest); map.BuildSamplingOffsetMap(this.KernelY, interest);
var operation = new Convolution2DRowOperation<TPixel>( Convolution2DRowOperation<TPixel> operation = new(
interest, interest,
targetPixels, targetPixels,
source.PixelBuffer, source.PixelBuffer,

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

@ -35,18 +35,48 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
Rectangle sourceRectangle, Rectangle sourceRectangle,
BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeX,
BorderWrappingMode borderWrapModeY) 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) : base(configuration, source, sourceRectangle)
{ {
this.Kernel = kernel; this.KernelX = kernelX;
this.KernelY = kernelY;
this.PreserveAlpha = preserveAlpha; this.PreserveAlpha = preserveAlpha;
this.BorderWrapModeX = borderWrapModeX; this.BorderWrapModeX = borderWrapModeX;
this.BorderWrapModeY = borderWrapModeY; this.BorderWrapModeY = borderWrapModeY;
} }
/// <summary> /// <summary>
/// Gets the convolution kernel. /// Gets the convolution kernel. X direction.
/// </summary>
public float[] KernelX { get; }
/// <summary>
/// Gets the convolution kernel. Y direction.
/// </summary> /// </summary>
public float[] Kernel { get; } public float[] KernelY { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels. /// Gets a value indicating whether the convolution filter is applied to alpha as well as the color channels.
@ -66,23 +96,23 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
using Buffer2D<TPixel> firstPassPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()); 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 // We can create a single sampling map with the size as if we were using the non separated 2D kernel
// the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur.
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 // Horizontal convolution
var horizontalOperation = new HorizontalConvolutionRowOperation( HorizontalConvolutionRowOperation horizontalOperation = new(
interest, interest,
firstPassPixels, firstPassPixels,
source.PixelBuffer, source.PixelBuffer,
mapXY, mapXY,
this.Kernel, this.KernelX,
this.Configuration, this.Configuration,
this.PreserveAlpha); this.PreserveAlpha);
@ -92,12 +122,12 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
in horizontalOperation); in horizontalOperation);
// Vertical convolution // Vertical convolution
var verticalOperation = new VerticalConvolutionRowOperation( VerticalConvolutionRowOperation verticalOperation = new(
interest, interest,
source.PixelBuffer, source.PixelBuffer,
firstPassPixels, firstPassPixels,
mapXY, mapXY,
this.Kernel, this.KernelY,
this.Configuration, this.Configuration,
this.PreserveAlpha); this.PreserveAlpha);
@ -140,7 +170,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRequiredBufferLength(Rectangle bounds) public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width; => 2 * bounds.Width;
@ -306,7 +336,7 @@ internal class Convolution2PassProcessor<TPixel> : ImageProcessor<TPixel>
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRequiredBufferLength(Rectangle bounds) public int GetRequiredBufferLength(Rectangle bounds)
=> 2 * bounds.Width; => 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);
}
}

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

@ -31,10 +31,34 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
bool preserveAlpha, bool preserveAlpha,
Image<TPixel> source, Image<TPixel> source,
Rectangle sourceRectangle) 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) : base(configuration, source, sourceRectangle)
{ {
this.KernelXY = kernelXY; this.KernelXY = kernelXY;
this.PreserveAlpha = preserveAlpha; this.PreserveAlpha = preserveAlpha;
this.BorderWrapModeX = borderWrapModeX;
this.BorderWrapModeY = borderWrapModeY;
} }
/// <summary> /// <summary>
@ -47,21 +71,31 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
/// </summary> /// </summary>
public bool PreserveAlpha { get; } 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/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
MemoryAllocator allocator = this.Configuration.MemoryAllocator; MemoryAllocator allocator = this.Configuration.MemoryAllocator;
using Buffer2D<TPixel> targetPixels = allocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> targetPixels = allocator.Allocate2D<TPixel>(source.Size);
source.CopyTo(targetPixels); 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>( ParallelRowIterator.IterateRows<RowOperation, Vector4>(
this.Configuration, this.Configuration,
interest, interest,
@ -121,7 +155,7 @@ internal class ConvolutionProcessor<TPixel> : ImageProcessor<TPixel>
ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span); ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span);
Span<TPixel> targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); 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; int row = y - this.bounds.Y;
ref int sampleRowBase = ref state.GetSampleRow((uint)row); 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 /> /// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// We need a clean copy for each pass to start from // We need a clean copy for each pass to start from
using ImageFrame<TPixel> cleanCopy = source.Clone(); using ImageFrame<TPixel> cleanCopy = source.Clone();
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); processor.Apply(source);
} }
@ -78,12 +78,12 @@ internal class EdgeDetectorCompassProcessor<TPixel> : ImageProcessor<TPixel>
{ {
using ImageFrame<TPixel> pass = cleanCopy.Clone(); 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); processor.Apply(pass);
} }
var operation = new RowOperation(source.PixelBuffer, pass.PixelBuffer, interest); RowOperation operation = new(source.PixelBuffer, pass.PixelBuffer, interest);
ParallelRowIterator.IterateRows( ParallelRowIterator.IterateRows(
this.Configuration, this.Configuration,
interest, interest,

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

@ -53,7 +53,7 @@ internal class EdgeDetectorProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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); 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> internal class GaussianBlurProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<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> /// <summary>
/// Initializes a new instance of the <see cref="GaussianBlurProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="GaussianBlurProcessor{TPixel}"/> class.
/// </summary> /// </summary>
@ -72,7 +54,7 @@ internal class GaussianBlurProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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); 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> internal class GaussianSharpenProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<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> /// <summary>
/// Initializes a new instance of the <see cref="GaussianSharpenProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="GaussianSharpenProcessor{TPixel}"/> class.
/// </summary> /// </summary>
@ -70,7 +54,7 @@ internal class GaussianSharpenProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) 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); processor.Apply(source);
} }

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

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

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

@ -38,7 +38,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int levels = Math.Clamp(this.definition.Levels, 1, 255); int levels = Math.Clamp(this.definition.Levels, 1, 255);
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height)); int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size()); using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size);
source.CopyTo(targetPixels); source.CopyTo(targetPixels);

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

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

4
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs

@ -43,7 +43,7 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns> /// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeStart(float radius, float center, int min, int max) public static int GetRangeStart(float radius, float center, int min, int max)
=> Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max); => Numerics.Clamp((int)MathF.Floor(center - radius), min, max);
/// <summary> /// <summary>
/// Gets the end position (inclusive) for a sampling range given /// Gets the end position (inclusive) for a sampling range given
@ -56,5 +56,5 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns> /// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeEnd(float radius, float center, int min, int max) public static int GetRangeEnd(float radius, float center, int min, int max)
=> Numerics.Clamp((int)MathF.Floor(center + radius), min, max); => Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max);
} }

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

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

7
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

@ -28,15 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this( : this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize), TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize),
sampler, sampler,
sourceSize) sourceSize)
=> this.Degrees = degrees; => this.Degrees = degrees;
// Helper constructor // Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize) private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix)) : base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel))
{ {
} }

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

Loading…
Cancel
Save