diff --git a/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs b/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
new file mode 100644
index 000000000..f753e7282
--- /dev/null
+++ b/src/ImageSharp/Formats/AlphaAwareImageEncoder.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Acts as a base encoder for all formats that are aware of and can handle alpha transparency.
+///
+public abstract class AlphaAwareImageEncoder : ImageEncoder
+{
+ ///
+ /// Gets or initializes the mode that determines how transparent pixels are handled during encoding.
+ ///
+ public TransparentColorMode TransparentColorMode { get; init; }
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 7c92d3e46..321a559b1 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -91,6 +91,11 @@ internal sealed class BmpEncoderCore
///
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
+ ///
+ /// The transparent color mode.
+ ///
+ private readonly TransparentColorMode transparentColorMode;
+
///
private readonly bool processedAlphaMask;
@@ -113,6 +118,7 @@ internal sealed class BmpEncoderCore
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
+ this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader;
@@ -181,14 +187,14 @@ internal sealed class BmpEncoderCore
Span buffer = stackalloc byte[infoHeaderSize];
- // for ico/cur encoder.
+ // For ico/cur encoder.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
- this.WriteImage(configuration, stream, image);
+ this.WriteImage(configuration, stream, image, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
@@ -345,44 +351,65 @@ internal sealed class BmpEncoderCore
///
/// The containing pixel data.
///
- private void WriteImage(Configuration configuration, Stream stream, Image image)
+ /// The token to monitor for cancellation requests.
+ private void WriteImage(
+ Configuration configuration,
+ Stream stream,
+ Image image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- Buffer2D pixels = image.Frames.RootFrame.PixelBuffer;
- switch (this.bitsPerPixel)
+ ImageFrame? clonedFrame = null;
+ try
{
- case BmpBitsPerPixel.Bit32:
- this.Write32BitPixelData(configuration, stream, pixels);
- break;
+ if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode))
+ {
+ clonedFrame = image.Frames.RootFrame.Clone();
+ EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
+ }
- case BmpBitsPerPixel.Bit24:
- this.Write24BitPixelData(configuration, stream, pixels);
- break;
+ ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame;
+ Buffer2D pixels = encodingFrame.PixelBuffer;
- case BmpBitsPerPixel.Bit16:
- this.Write16BitPixelData(configuration, stream, pixels);
- break;
+ switch (this.bitsPerPixel)
+ {
+ case BmpBitsPerPixel.Bit32:
+ this.Write32BitPixelData(configuration, stream, pixels, cancellationToken);
+ break;
- case BmpBitsPerPixel.Bit8:
- this.Write8BitPixelData(configuration, stream, image);
- break;
+ case BmpBitsPerPixel.Bit24:
+ this.Write24BitPixelData(configuration, stream, pixels, cancellationToken);
+ break;
- case BmpBitsPerPixel.Bit4:
- this.Write4BitPixelData(configuration, stream, image);
- break;
+ case BmpBitsPerPixel.Bit16:
+ this.Write16BitPixelData(configuration, stream, pixels, cancellationToken);
+ break;
- case BmpBitsPerPixel.Bit2:
- this.Write2BitPixelData(configuration, stream, image);
- break;
+ case BmpBitsPerPixel.Bit8:
+ this.Write8BitPixelData(configuration, stream, encodingFrame, cancellationToken);
+ break;
- case BmpBitsPerPixel.Bit1:
- this.Write1BitPixelData(configuration, stream, image);
- break;
- }
+ case BmpBitsPerPixel.Bit4:
+ this.Write4BitPixelData(configuration, stream, encodingFrame, cancellationToken);
+ break;
+
+ case BmpBitsPerPixel.Bit2:
+ this.Write2BitPixelData(configuration, stream, encodingFrame, cancellationToken);
+ break;
- if (this.processedAlphaMask)
+ case BmpBitsPerPixel.Bit1:
+ this.Write1BitPixelData(configuration, stream, encodingFrame, cancellationToken);
+ break;
+ }
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, encodingFrame);
+ }
+ }
+ finally
{
- ProcessedAlphaMask(stream, image);
+ clonedFrame?.Dispose();
}
}
@@ -396,7 +423,12 @@ internal sealed class BmpEncoderCore
/// The global configuration.
/// The to write to.
/// The containing pixel data.
- private void Write32BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels)
+ /// The token to monitor for cancellation requests.
+ private void Write32BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ Buffer2D pixels,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IMemoryOwner row = this.AllocateRow(pixels.Width, 4);
@@ -404,6 +436,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgra32Bytes(
configuration,
@@ -421,7 +455,12 @@ internal sealed class BmpEncoderCore
/// The global configuration.
/// The to write to.
/// The containing pixel data.
- private void Write24BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels)
+ /// The token to monitor for cancellation requests.
+ private void Write24BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ Buffer2D pixels,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = pixels.Width;
@@ -431,6 +470,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgr24Bytes(
configuration,
@@ -448,7 +489,12 @@ internal sealed class BmpEncoderCore
/// The global configuration.
/// The to write to.
/// The containing pixel data.
- private void Write16BitPixelData(Configuration configuration, Stream stream, Buffer2D pixels)
+ /// The token to monitor for cancellation requests.
+ private void Write16BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ Buffer2D pixels,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = pixels.Width;
@@ -458,6 +504,8 @@ internal sealed class BmpEncoderCore
for (int y = pixels.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.ToBgra5551Bytes(
@@ -476,21 +524,32 @@ internal sealed class BmpEncoderCore
/// The type of the pixel.
/// The global configuration.
/// The to write to.
- /// The containing pixel data.
- private void Write8BitPixelData(Configuration configuration, Stream stream, Image image)
+ /// The containing pixel data.
+ /// The token to monitor for cancellation requests.
+ private void Write8BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame encodingFrame,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- bool isL8 = typeof(TPixel) == typeof(L8);
+ PixelTypeInfo info = TPixel.GetPixelTypeInfo();
+ bool is8BitLuminance =
+ info.BitsPerPixel == 8
+ && info.ColorType == PixelColorType.Luminance
+ && info.AlphaRepresentation == PixelAlphaRepresentation.None
+ && info.ComponentInfo!.Value.ComponentCount == 1;
+
using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize8Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
- if (isL8)
+ if (is8BitLuminance)
{
- this.Write8BitPixelData(stream, image, colorPalette);
+ this.Write8BitLuminancePixelData(stream, encodingFrame, colorPalette, cancellationToken);
}
else
{
- this.Write8BitColor(configuration, stream, image, colorPalette);
+ this.Write8BitColor(configuration, stream, encodingFrame, colorPalette, cancellationToken);
}
}
@@ -500,21 +559,29 @@ internal sealed class BmpEncoderCore
/// The type of the pixel.
/// The global configuration.
/// The to write to.
- /// The containing pixel data.
+ /// The containing pixel data.
/// A byte span of size 1024 for the color palette.
- private void Write8BitColor(Configuration configuration, Stream stream, Image image, Span colorPalette)
+ /// The token to monitor for cancellation requests.
+ private void Write8BitColor(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame encodingFrame,
+ Span colorPalette,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration);
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
- using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
+ using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
- for (int y = image.Height - 1; y >= 0; y--)
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
ReadOnlySpan pixelSpan = quantized.DangerousGetRowSpan(y);
stream.Write(pixelSpan);
@@ -530,9 +597,14 @@ internal sealed class BmpEncoderCore
///
/// The type of the pixel.
/// The to write to.
- /// The containing pixel data.
+ /// The containing pixel data.
/// A byte span of size 1024 for the color palette.
- private void Write8BitPixelData(Stream stream, Image image, Span colorPalette)
+ /// The token to monitor for cancellation requests.
+ private void Write8BitLuminancePixelData(
+ Stream stream,
+ ImageFrame encodingFrame,
+ Span colorPalette,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// Create a color palette with 256 different gray values.
@@ -549,9 +621,11 @@ internal sealed class BmpEncoderCore
}
stream.Write(colorPalette);
- Buffer2D imageBuffer = image.GetRootFramePixelBuffer();
- for (int y = image.Height - 1; y >= 0; y--)
+ Buffer2D imageBuffer = encodingFrame.PixelBuffer;
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
ReadOnlySpan inputPixelRow = imageBuffer.DangerousGetRowSpan(y);
ReadOnlySpan outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
stream.Write(outputPixelRow);
@@ -569,8 +643,13 @@ internal sealed class BmpEncoderCore
/// The type of the pixel.
/// The global configuration.
/// The to write to.
- /// The containing pixel data.
- private void Write4BitPixelData(Configuration configuration, Stream stream, Image image)
+ /// The containing pixel data.
+ /// The token to monitor for cancellation requests.
+ private void Write4BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame encodingFrame,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
@@ -580,9 +659,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
+ frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
- using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
@@ -591,8 +670,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
- for (int y = image.Height - 1; y >= 0; y--)
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
@@ -619,8 +700,13 @@ internal sealed class BmpEncoderCore
/// The type of the pixel.
/// The global configuration.
/// The to write to.
- /// The containing pixel data.
- private void Write2BitPixelData(Configuration configuration, Stream stream, Image image)
+ /// The containing pixel data.
+ /// The token to monitor for cancellation requests.
+ private void Write2BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame encodingFrame,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
@@ -630,9 +716,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
+ frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
- using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
@@ -641,8 +727,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding;
- for (int y = image.Height - 1; y >= 0; y--)
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
pixelRowSpan = quantized.DangerousGetRowSpan(y);
int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4;
@@ -678,8 +766,13 @@ internal sealed class BmpEncoderCore
/// The type of the pixel.
/// The global configuration.
/// The to write to.
- /// The containing pixel data.
- private void Write1BitPixelData(Configuration configuration, Stream stream, Image image)
+ /// The containing pixel data.
+ /// The token to monitor for cancellation requests.
+ private void Write1BitPixelData(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame encodingFrame,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(configuration, new QuantizerOptions()
@@ -689,9 +782,9 @@ internal sealed class BmpEncoderCore
DitherScale = this.quantizer.Options.DitherScale
});
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
+ frameQuantizer.BuildPalette(this.pixelSamplingStrategy, encodingFrame);
- using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ using IndexedImageFrame quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize1Bit, AllocationOptions.Clean);
Span colorPalette = colorPaletteBuffer.GetSpan();
@@ -700,8 +793,10 @@ internal sealed class BmpEncoderCore
ReadOnlySpan quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
- for (int y = image.Height - 1; y >= 0; y--)
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
quantizedPixelRow = quantized.DangerousGetRowSpan(y);
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
@@ -766,10 +861,10 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices);
}
- private static void ProcessedAlphaMask(Stream stream, Image image)
+ private static void ProcessedAlphaMask(Stream stream, ImageFrame encodingFrame)
where TPixel : unmanaged, IPixel
{
- int arrayWidth = image.Width / 8;
+ int arrayWidth = encodingFrame.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
@@ -777,10 +872,10 @@ internal sealed class BmpEncoderCore
}
Span mask = stackalloc byte[arrayWidth];
- for (int y = image.Height - 1; y >= 0; y--)
+ for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
mask.Clear();
- Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y);
+ Span row = encodingFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int i = 0; i < arrayWidth; i++)
{
diff --git a/src/ImageSharp/Formats/EncodingUtilities.cs b/src/ImageSharp/Formats/EncodingUtilities.cs
new file mode 100644
index 000000000..a979fdf6f
--- /dev/null
+++ b/src/ImageSharp/Formats/EncodingUtilities.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Provides utilities for encoding images.
+///
+internal static class EncodingUtilities
+{
+ public static bool ShouldClearTransparentPixels(TransparentColorMode mode)
+ where TPixel : unmanaged, IPixel
+ => mode == TransparentColorMode.Clear &&
+ TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Unassociated;
+
+ ///
+ /// Convert transparent pixels, to pixels represented by , which can yield
+ /// to better compression in some cases.
+ ///
+ /// The type of the pixel.
+ /// The cloned where the transparent pixels will be changed.
+ /// The color to replace transparent pixels with.
+ public static void ClearTransparentPixels(ImageFrame clone, Color color)
+ where TPixel : unmanaged, IPixel
+ {
+ Buffer2DRegion buffer = clone.PixelBuffer.GetRegion();
+ ClearTransparentPixels(clone.Configuration, ref buffer, color);
+ }
+
+ ///
+ /// Convert transparent pixels, to pixels represented by , which can yield
+ /// to better compression in some cases.
+ ///
+ /// The type of the pixel.
+ /// The configuration.
+ /// The cloned where the transparent pixels will be changed.
+ /// The color to replace transparent pixels with.
+ public static void ClearTransparentPixels(
+ Configuration configuration,
+ ref Buffer2DRegion clone,
+ Color color)
+ where TPixel : unmanaged, IPixel
+ {
+ using IMemoryOwner vectors = configuration.MemoryAllocator.Allocate(clone.Width);
+ Span vectorsSpan = vectors.GetSpan();
+ Vector4 replacement = color.ToScaledVector4();
+ for (int y = 0; y < clone.Height; y++)
+ {
+ Span span = clone.DangerousGetRowSpan(y);
+ PixelOperations.Instance.ToVector4(configuration, span, vectorsSpan, PixelConversionModifiers.Scale);
+ ClearTransparentPixelRow(vectorsSpan, replacement);
+ PixelOperations.Instance.FromVector4Destructive(configuration, vectorsSpan, span, PixelConversionModifiers.Scale);
+ }
+ }
+
+ private static void ClearTransparentPixelRow(
+ Span vectorsSpan,
+ Vector4 replacement)
+ {
+ if (Vector128.IsHardwareAccelerated)
+ {
+ Vector128 replacement128 = replacement.AsVector128();
+
+ for (int i = 0; i < vectorsSpan.Length; i++)
+ {
+ ref Vector4 v = ref vectorsSpan[i];
+ Vector128 v128 = v.AsVector128();
+
+ // Do `vector == 0`
+ Vector128 mask = Vector128.Equals(v128, Vector128.Zero);
+
+ // Replicate the result for W to all elements (is AllBitsSet if the W was 0 and Zero otherwise)
+ mask = Vector128.Shuffle(mask, Vector128.Create(3, 3, 3, 3));
+
+ // Use the mask to select the replacement vector
+ // (replacement & mask) | (v128 & ~mask)
+ v = Vector128.ConditionalSelect(mask, replacement128, v128).AsVector4();
+ }
+ }
+ else
+ {
+ for (int i = 0; i < vectorsSpan.Length; i++)
+ {
+ if (vectorsSpan[i].W == 0F)
+ {
+ vectorsSpan[i] = replacement;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index a99b5862d..3d6990478 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -665,7 +665,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
return;
}
- Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
+ Rectangle interest = Rectangle.Intersect(frame.Bounds, this.restoreArea.Value);
Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 0ed7e8c98..3c6e269e4 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -67,6 +67,8 @@ internal sealed class GifEncoderCore
///
private readonly ushort? repeatCount;
+ private readonly TransparentColorMode transparentColorMode;
+
///
/// Initializes a new instance of the class.
///
@@ -83,6 +85,7 @@ internal sealed class GifEncoderCore
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
+ this.transparentColorMode = encoder.TransparentColorMode;
}
///
@@ -131,18 +134,40 @@ internal sealed class GifEncoderCore
}
}
+ // Quantize the first frame. Checking to see whether we can clear the transparent pixels
+ // to allow for a smaller color palette and encoded result.
using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration))
{
+ ImageFrame? clonedFrame = null;
+ Configuration configuration = this.configuration;
+ TransparentColorMode mode = this.transparentColorMode;
+ IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
+ if (EncodingUtilities.ShouldClearTransparentPixels(mode))
+ {
+ clonedFrame = image.Frames.RootFrame.Clone();
+
+ GifFrameMetadata frameMeta = clonedFrame.Metadata.GetGifMetadata();
+ Color background = frameMeta.DisposalMode == FrameDisposalMode.RestoreToBackground
+ ? this.backgroundColor ?? Color.Transparent
+ : Color.Transparent;
+
+ EncodingUtilities.ClearTransparentPixels(clonedFrame, background);
+ }
+
+ ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame;
+
if (useGlobalTable)
{
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
- quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(configuration, mode, strategy, image);
+ quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
else
{
- frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
- quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
+ frameQuantizer.BuildPalette(configuration, mode, strategy, encodingFrame);
+ quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds);
}
+
+ clonedFrame?.Dispose();
}
// Write the header.
@@ -236,52 +261,49 @@ internal sealed class GifEncoderCore
// This frame is reused to store de-duplicated pixel buffers.
using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size);
- for (int i = 1; i < image.Frames.Count; i++)
+ try
{
- if (cancellationToken.IsCancellationRequested)
+ for (int i = 1; i < image.Frames.Count; i++)
{
- if (hasPaletteQuantizer)
- {
- paletteQuantizer.Dispose();
- }
+ cancellationToken.ThrowIfCancellationRequested();
- return;
- }
+ // Gather the metadata for this frame.
+ ImageFrame currentFrame = image.Frames[i];
+ ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
+ GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
+ bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
- // Gather the metadata for this frame.
- ImageFrame currentFrame = image.Frames[i];
- ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
- GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
- bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local);
+ if (!useLocal && !hasPaletteQuantizer && i > 0)
+ {
+ // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
+ // This allows a reduction of memory usage across multi-frame gifs using a global palette
+ // and also allows use to reuse the cache from previous runs.
+ int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
+ paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
+ hasPaletteQuantizer = true;
+ }
- if (!useLocal && !hasPaletteQuantizer && i > 0)
- {
- // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
- // This allows a reduction of memory usage across multi-frame gifs using a global palette
- // and also allows use to reuse the cache from previous runs.
- int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
- paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
- hasPaletteQuantizer = true;
+ this.EncodeAdditionalFrame(
+ stream,
+ previousFrame,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ useLocal,
+ gifMetadata,
+ paletteQuantizer,
+ previousDisposalMode);
+
+ previousFrame = currentFrame;
+ previousDisposalMode = gifMetadata.DisposalMode;
}
-
- this.EncodeAdditionalFrame(
- stream,
- previousFrame,
- currentFrame,
- nextFrame,
- encodingFrame,
- useLocal,
- gifMetadata,
- paletteQuantizer,
- previousDisposalMode);
-
- previousFrame = currentFrame;
- previousDisposalMode = gifMetadata.DisposalMode;
}
-
- if (hasPaletteQuantizer)
+ finally
{
- paletteQuantizer.Dispose();
+ if (hasPaletteQuantizer)
+ {
+ paletteQuantizer.Dispose();
+ }
}
}
@@ -324,7 +346,9 @@ internal sealed class GifEncoderCore
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
- ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
+ ImageFrame? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground
+ ? null :
+ previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
@@ -341,6 +365,11 @@ internal sealed class GifEncoderCore
background,
true);
+ if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode))
+ {
+ EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
+ }
+
using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
encodingFrame,
bounds,
diff --git a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs
index 44431aa9a..d2c3ad690 100644
--- a/src/ImageSharp/Formats/IAnimatedImageEncoder.cs
+++ b/src/ImageSharp/Formats/IAnimatedImageEncoder.cs
@@ -30,7 +30,7 @@ public interface IAnimatedImageEncoder
///
/// Acts as a base class for all image encoders that allow encoding animation sequences.
///
-public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
+public abstract class AnimatedImageEncoder : AlphaAwareImageEncoder, IAnimatedImageEncoder
{
///
public Color? BackgroundColor { get; init; }
diff --git a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs
index e88b3ecf0..5edf6e40e 100644
--- a/src/ImageSharp/Formats/IQuantizingImageEncoder.cs
+++ b/src/ImageSharp/Formats/IQuantizingImageEncoder.cs
@@ -24,7 +24,7 @@ public interface IQuantizingImageEncoder
///
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
///
-public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
+public abstract class QuantizingImageEncoder : AlphaAwareImageEncoder, IQuantizingImageEncoder
{
///
public IQuantizer? Quantizer { get; init; }
diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
index 4b973d511..80c3ec4c3 100644
--- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
+++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
@@ -63,7 +63,6 @@ internal abstract class IconEncoderCore
this.entries[i].Entry.ImageOffset = (uint)stream.Position;
// We crop the frame to the size specified in the metadata.
- // TODO: we can optimize this by cropping the frame only if the new size is both required and different.
using Image encodingFrame = new(width, height);
for (int y = 0; y < height; y++)
{
@@ -82,6 +81,8 @@ internal abstract class IconEncoderCore
UseDoubleHeight = true,
SkipFileHeader = true,
SupportTransparency = false,
+ TransparentColorMode = this.encoder.TransparentColorMode,
+ PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
BitsPerPixel = encodingMetadata.BmpBitsPerPixel
},
IconFrameCompression.Png => new PngEncoder()
@@ -90,6 +91,7 @@ internal abstract class IconEncoderCore
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
BitDepth = PngBitDepth.Bit8,
ColorType = PngColorType.RgbWithAlpha,
+ TransparentColorMode = this.encoder.TransparentColorMode,
CompressionLevel = PngCompressionLevel.BestCompression
},
_ => throw new NotSupportedException(),
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index d9f71e1b5..63e675b50 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -68,12 +68,6 @@ public class PngEncoder : QuantizingAnimatedImageEncoder
///
public PngChunkFilter? ChunkFilter { get; init; }
- ///
- /// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
- /// should be converted to transparent black, which can yield in better compression in some cases.
- ///
- public PngTransparentColorMode TransparentColorMode { get; init; }
-
///
protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
{
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 398c80634..05220e801 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -7,6 +7,7 @@ using System.IO.Hashing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@@ -188,18 +189,18 @@ internal sealed class PngEncoderCore : IDisposable
ImageFrame currentFrame = image.Frames.RootFrame;
int currentFrameIndex = 0;
- bool clearTransparency = this.encoder.TransparentColorMode is PngTransparentColorMode.Clear;
+ bool clearTransparency = EncodingUtilities.ShouldClearTransparentPixels(this.encoder.TransparentColorMode);
if (clearTransparency)
{
currentFrame = clonedFrame = currentFrame.Clone();
- ClearTransparentPixels(currentFrame, Color.Transparent);
+ EncodingUtilities.ClearTransparentPixels(currentFrame, Color.Transparent);
}
// Do not move this. We require an accurate bit depth for the header chunk.
IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
pngMetadata,
currentFrame,
- currentFrame.Bounds(),
+ currentFrame.Bounds,
null);
this.WriteHeaderChunk(stream);
@@ -230,86 +231,88 @@ internal sealed class PngEncoderCore : IDisposable
currentFrameIndex++;
}
- if (image.Frames.Count > 1)
+ try
{
- // Write the first animated frame.
- currentFrame = image.Frames[currentFrameIndex];
- PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
- FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
- FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
- uint sequenceNumber = 1;
- if (pngMetadata.AnimateRootFrame)
+ if (image.Frames.Count > 1)
{
- this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
- }
- else
- {
- sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
- }
-
- currentFrameIndex++;
+ // Write the first animated frame.
+ currentFrame = image.Frames[currentFrameIndex];
+ PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
+ FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
+ FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
+ uint sequenceNumber = 1;
+ if (pngMetadata.AnimateRootFrame)
+ {
+ this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
+ }
+ else
+ {
+ sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
+ }
- // Capture the global palette for reuse on subsequent frames.
- ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
+ currentFrameIndex++;
- // Write following frames.
- ImageFrame previousFrame = image.Frames.RootFrame;
+ // Capture the global palette for reuse on subsequent frames.
+ ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
- // This frame is reused to store de-duplicated pixel buffers.
- using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
+ // Write following frames.
+ ImageFrame previousFrame = image.Frames.RootFrame;
- for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- break;
- }
+ // This frame is reused to store de-duplicated pixel buffers.
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size);
- ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
- currentFrame = image.Frames[currentFrameIndex];
- ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
-
- frameMetadata = currentFrame.Metadata.GetPngMetadata();
- bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
- Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
- ? this.backgroundColor ?? Color.Transparent
- : Color.Transparent;
-
- (bool difference, Rectangle bounds) =
- AnimationUtilities.DeDuplicatePixels(
- image.Configuration,
- prev,
- currentFrame,
- nextFrame,
- encodingFrame,
- background,
- blend);
-
- if (clearTransparency)
+ for (; currentFrameIndex < image.Frames.Count; currentFrameIndex++)
{
- ClearTransparentPixels(encodingFrame, background);
- }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
+ currentFrame = image.Frames[currentFrameIndex];
+ ImageFrame? nextFrame = currentFrameIndex < image.Frames.Count - 1 ? image.Frames[currentFrameIndex + 1] : null;
+
+ frameMetadata = currentFrame.Metadata.GetPngMetadata();
+ bool blend = frameMetadata.BlendMode == FrameBlendMode.Over;
+ Color background = frameMetadata.DisposalMode == FrameDisposalMode.RestoreToBackground
+ ? this.backgroundColor ?? Color.Transparent
+ : Color.Transparent;
+
+ (bool difference, Rectangle bounds) =
+ AnimationUtilities.DeDuplicatePixels(
+ image.Configuration,
+ prev,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ background,
+ blend);
+
+ if (clearTransparency)
+ {
+ EncodingUtilities.ClearTransparentPixels(encodingFrame, background);
+ }
- // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
- frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
+ // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
+ frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, sequenceNumber);
- // Dispose of previous quantized frame and reassign.
- quantized?.Dispose();
- quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
- sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
+ // Dispose of previous quantized frame and reassign.
+ quantized?.Dispose();
+ quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
+ sequenceNumber += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true) + 1;
- previousFrame = currentFrame;
- previousDisposal = frameMetadata.DisposalMode;
+ previousFrame = currentFrame;
+ previousDisposal = frameMetadata.DisposalMode;
+ }
}
- }
- this.WriteEndChunk(stream);
+ this.WriteEndChunk(stream);
- stream.Flush();
-
- // Dispose of allocations from final frame.
- clonedFrame?.Dispose();
- quantized?.Dispose();
+ stream.Flush();
+ }
+ finally
+ {
+ // Dispose of allocations from final frame.
+ clonedFrame?.Dispose();
+ quantized?.Dispose();
+ }
}
///
@@ -319,33 +322,6 @@ internal sealed class PngEncoderCore : IDisposable
this.currentScanline?.Dispose();
}
- ///
- /// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
- ///
- /// The type of the pixel.
- /// The cloned image frame where the transparent pixels will be changed.
- /// The color to replace transparent pixels with.
- private static void ClearTransparentPixels(ImageFrame clone, Color color)
- where TPixel : unmanaged, IPixel
- => clone.ProcessPixelRows(accessor =>
- {
- // TODO: We should be able to speed this up with SIMD and masking.
- Rgba32 transparent = color.ToPixel();
- for (int y = 0; y < accessor.Height; y++)
- {
- Span span = accessor.GetRowSpan(y);
- for (int x = 0; x < accessor.Width; x++)
- {
- ref TPixel pixel = ref span[x];
- Rgba32 rgba = pixel.ToRgba32();
- if (rgba.A is 0)
- {
- pixel = TPixel.FromRgba32(transparent);
- }
- }
- }
- });
-
///
/// Creates the quantized image and calculates and sets the bit depth.
///
diff --git a/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs b/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
deleted file mode 100644
index 76a89608b..000000000
--- a/src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats.Png;
-
-///
-/// Enum indicating how the transparency should be handled on encoding.
-///
-public enum PngTransparentColorMode
-{
- ///
- /// The transparency will be kept as is.
- ///
- Preserve = 0,
-
- ///
- /// Converts fully transparent pixels that may contain R, G, B values which are not 0,
- /// to transparent black, which can yield in better compression in some cases.
- ///
- Clear = 1,
-}
diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs
index b9c2078b3..1da9caffb 100644
--- a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs
+++ b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Qoi;
///
/// Image encoder for writing an image to a stream as a QOI image
///
-public class QoiEncoder : ImageEncoder
+public class QoiEncoder : AlphaAwareImageEncoder
{
///
/// Gets the color channels on the image that can be
diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
index 88d87a382..872cec3fd 100644
--- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
+++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
@@ -55,7 +55,7 @@ internal class QoiEncoderCore
Guard.NotNull(stream, nameof(stream));
this.WriteHeader(image, stream);
- this.WritePixels(image, stream);
+ this.WritePixels(image, stream, cancellationToken);
WriteEndOfStream(stream);
stream.Flush();
}
@@ -78,7 +78,7 @@ internal class QoiEncoderCore
stream.WriteByte((byte)qoiColorSpace);
}
- private void WritePixels(Image image, Stream stream)
+ private void WritePixels(Image image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// Start image encoding
@@ -86,137 +86,156 @@ internal class QoiEncoderCore
Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255);
Rgba32 currentRgba32 = default;
- Buffer2D pixels = image.Frames[0].PixelBuffer;
- using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width);
- Span rgbaRow = rgbaRowBuffer.GetSpan();
- for (int i = 0; i < pixels.Height; i++)
+ ImageFrame? clonedFrame = null;
+ try
{
- Span row = pixels.DangerousGetRowSpan(i);
- PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow);
- for (int j = 0; j < row.Length && i < pixels.Height; j++)
+ if (EncodingUtilities.ShouldClearTransparentPixels(this.encoder.TransparentColorMode))
{
- // We get the RGBA value from pixels
- currentRgba32 = rgbaRow[j];
+ clonedFrame = image.Frames.RootFrame.Clone();
+ EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
+ }
+
+ ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame;
+ Buffer2D pixels = encodingFrame.PixelBuffer;
+
+ using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width);
+ Span rgbaRow = rgbaRowBuffer.GetSpan();
+ Configuration configuration = this.configuration;
+ for (int i = 0; i < pixels.Height; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- // First, we check if the current pixel is equal to the previous one
- // If so, we do a QOI_OP_RUN
- if (currentRgba32.Equals(previousPixel))
+ Span row = pixels.DangerousGetRowSpan(i);
+ PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow);
+ for (int j = 0; j < row.Length && i < pixels.Height; j++)
{
- /* It looks like this isn't an error, but this makes possible that
- * files start with a QOI_OP_RUN if their first pixel is a fully opaque
- * black. However, the decoder of this project takes that into consideration
- *
- * To further details, see https://github.com/phoboslab/qoi/issues/258,
- * and we should discuss what to do about this approach and
- * if it's correct
- */
- int repetitions = 0;
- do
+ // We get the RGBA value from pixels
+ currentRgba32 = rgbaRow[j];
+
+ // First, we check if the current pixel is equal to the previous one
+ // If so, we do a QOI_OP_RUN
+ if (currentRgba32.Equals(previousPixel))
{
- repetitions++;
- j++;
- if (j == row.Length)
+ /* It looks like this isn't an error, but this makes possible that
+ * files start with a QOI_OP_RUN if their first pixel is a fully opaque
+ * black. However, the decoder of this project takes that into consideration
+ *
+ * To further details, see https://github.com/phoboslab/qoi/issues/258,
+ * and we should discuss what to do about this approach and
+ * if it's correct
+ */
+ int repetitions = 0;
+ do
{
- j = 0;
- i++;
- if (i == pixels.Height)
+ repetitions++;
+ j++;
+ if (j == row.Length)
{
- break;
+ j = 0;
+ i++;
+ if (i == pixels.Height)
+ {
+ break;
+ }
+
+ row = pixels.DangerousGetRowSpan(i);
+ PixelOperations.Instance.ToRgba32(configuration, row, rgbaRow);
}
- row = pixels.DangerousGetRowSpan(i);
- PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow);
+ currentRgba32 = rgbaRow[j];
}
+ while (currentRgba32.Equals(previousPixel) && repetitions < 62);
- currentRgba32 = rgbaRow[j];
- }
- while (currentRgba32.Equals(previousPixel) && repetitions < 62);
-
- j--;
- stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
+ j--;
+ stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
- /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
- * it will be taken and compared on the next iteration
- */
- continue;
- }
+ /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
+ * it will be taken and compared on the next iteration
+ */
+ continue;
+ }
- // else, we check if it exists in the previously seen pixels
- // If so, we do a QOI_OP_INDEX
- int pixelArrayPosition = GetArrayPosition(currentRgba32);
- if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
- {
- stream.WriteByte((byte)pixelArrayPosition);
- }
- else
- {
- // else, we check if the difference is less than -2..1
- // Since it wasn't found on the previously seen pixels, we save it
- previouslySeenPixels[pixelArrayPosition] = currentRgba32;
-
- int diffRed = currentRgba32.R - previousPixel.R;
- int diffGreen = currentRgba32.G - previousPixel.G;
- int diffBlue = currentRgba32.B - previousPixel.B;
-
- // If so, we do a QOI_OP_DIFF
- if (diffRed is >= -2 and <= 1 &&
- diffGreen is >= -2 and <= 1 &&
- diffBlue is >= -2 and <= 1 &&
- currentRgba32.A == previousPixel.A)
+ // else, we check if it exists in the previously seen pixels
+ // If so, we do a QOI_OP_INDEX
+ int pixelArrayPosition = GetArrayPosition(currentRgba32);
+ if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
{
- // Bottom limit is -2, so we add 2 to make it equal to 0
- int dr = diffRed + 2;
- int dg = diffGreen + 2;
- int db = diffBlue + 2;
- byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
- stream.WriteByte(valueToWrite);
+ stream.WriteByte((byte)pixelArrayPosition);
}
else
{
- // else, we check if the green difference is less than -32..31 and the rest -8..7
- // If so, we do a QOI_OP_LUMA
- int diffRedGreen = diffRed - diffGreen;
- int diffBlueGreen = diffBlue - diffGreen;
- if (diffGreen is >= -32 and <= 31 &&
- diffRedGreen is >= -8 and <= 7 &&
- diffBlueGreen is >= -8 and <= 7 &&
+ // else, we check if the difference is less than -2..1
+ // Since it wasn't found on the previously seen pixels, we save it
+ previouslySeenPixels[pixelArrayPosition] = currentRgba32;
+
+ int diffRed = currentRgba32.R - previousPixel.R;
+ int diffGreen = currentRgba32.G - previousPixel.G;
+ int diffBlue = currentRgba32.B - previousPixel.B;
+
+ // If so, we do a QOI_OP_DIFF
+ if (diffRed is >= -2 and <= 1 &&
+ diffGreen is >= -2 and <= 1 &&
+ diffBlue is >= -2 and <= 1 &&
currentRgba32.A == previousPixel.A)
{
- int dr_dg = diffRedGreen + 8;
- int db_dg = diffBlueGreen + 8;
- byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
- byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
- stream.WriteByte(byteToWrite1);
- stream.WriteByte(byteToWrite2);
+ // Bottom limit is -2, so we add 2 to make it equal to 0
+ int dr = diffRed + 2;
+ int dg = diffGreen + 2;
+ int db = diffBlue + 2;
+ byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
+ stream.WriteByte(valueToWrite);
}
else
{
- // else, we check if the alpha is equal to the previous pixel
- // If so, we do a QOI_OP_RGB
- if (currentRgba32.A == previousPixel.A)
+ // else, we check if the green difference is less than -32..31 and the rest -8..7
+ // If so, we do a QOI_OP_LUMA
+ int diffRedGreen = diffRed - diffGreen;
+ int diffBlueGreen = diffBlue - diffGreen;
+ if (diffGreen is >= -32 and <= 31 &&
+ diffRedGreen is >= -8 and <= 7 &&
+ diffBlueGreen is >= -8 and <= 7 &&
+ currentRgba32.A == previousPixel.A)
{
- stream.WriteByte((byte)QoiChunk.QoiOpRgb);
- stream.WriteByte(currentRgba32.R);
- stream.WriteByte(currentRgba32.G);
- stream.WriteByte(currentRgba32.B);
+ int dr_dg = diffRedGreen + 8;
+ int db_dg = diffBlueGreen + 8;
+ byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
+ byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
+ stream.WriteByte(byteToWrite1);
+ stream.WriteByte(byteToWrite2);
}
else
{
- // else, we do a QOI_OP_RGBA
- stream.WriteByte((byte)QoiChunk.QoiOpRgba);
- stream.WriteByte(currentRgba32.R);
- stream.WriteByte(currentRgba32.G);
- stream.WriteByte(currentRgba32.B);
- stream.WriteByte(currentRgba32.A);
+ // else, we check if the alpha is equal to the previous pixel
+ // If so, we do a QOI_OP_RGB
+ if (currentRgba32.A == previousPixel.A)
+ {
+ stream.WriteByte((byte)QoiChunk.QoiOpRgb);
+ stream.WriteByte(currentRgba32.R);
+ stream.WriteByte(currentRgba32.G);
+ stream.WriteByte(currentRgba32.B);
+ }
+ else
+ {
+ // else, we do a QOI_OP_RGBA
+ stream.WriteByte((byte)QoiChunk.QoiOpRgba);
+ stream.WriteByte(currentRgba32.R);
+ stream.WriteByte(currentRgba32.G);
+ stream.WriteByte(currentRgba32.B);
+ stream.WriteByte(currentRgba32.A);
+ }
}
}
}
- }
- previousPixel = currentRgba32;
+ previousPixel = currentRgba32;
+ }
}
}
+ finally
+ {
+ clonedFrame?.Dispose();
+ }
}
private static void WriteEndOfStream(Stream stream)
diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs
index 09b12e608..a4630a464 100644
--- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs
+++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Tga;
///
/// Image encoder for writing an image to a stream as a Targa true-vision image.
///
-public sealed class TgaEncoder : ImageEncoder
+public sealed class TgaEncoder : AlphaAwareImageEncoder
{
///
/// Gets the number of bits per pixel.
diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
index 1e05a9f71..e2ea9c4fe 100644
--- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
@@ -29,6 +29,8 @@ internal sealed class TgaEncoderCore
///
private readonly TgaCompression compression;
+ private readonly TransparentColorMode transparentColorMode;
+
///
/// Initializes a new instance of the class.
///
@@ -39,6 +41,7 @@ internal sealed class TgaEncoderCore
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
this.compression = encoder.Compression;
+ this.transparentColorMode = encoder.TransparentColorMode;
}
///
@@ -103,16 +106,33 @@ internal sealed class TgaEncoderCore
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, TgaFileHeader.Size);
- if (this.compression is TgaCompression.RunLength)
+
+ ImageFrame? clonedFrame = null;
+ try
{
- this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame, cancellationToken);
+ if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode))
+ {
+ clonedFrame = image.Frames.RootFrame.Clone();
+ EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
+ }
+
+ ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame;
+
+ if (this.compression is TgaCompression.RunLength)
+ {
+ this.WriteRunLengthEncodedImage(stream, encodingFrame, cancellationToken);
+ }
+ else
+ {
+ this.WriteImage(image.Configuration, stream, encodingFrame, cancellationToken);
+ }
+
+ stream.Flush();
}
- else
+ finally
{
- this.WriteImage(image.Configuration, stream, image.Frames.RootFrame, cancellationToken);
+ clonedFrame?.Dispose();
}
-
- stream.Flush();
}
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index b560067f3..4f6985f9d 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -49,6 +49,11 @@ internal sealed class TiffEncoderCore
///
private readonly DeflateCompressionLevel compressionLevel;
+ ///
+ /// The transparent color mode to use when encoding.
+ ///
+ private readonly TransparentColorMode transparentColorMode;
+
///
/// Whether to skip metadata during encoding.
///
@@ -59,20 +64,21 @@ internal sealed class TiffEncoderCore
///
/// Initializes a new instance of the class.
///
- /// The options for the encoder.
+ /// The options for the encoder.
/// The global configuration.
- public TiffEncoderCore(TiffEncoder options, Configuration configuration)
+ public TiffEncoderCore(TiffEncoder encoder, Configuration configuration)
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
- this.PhotometricInterpretation = options.PhotometricInterpretation;
- this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
- this.pixelSamplingStrategy = options.PixelSamplingStrategy;
- this.BitsPerPixel = options.BitsPerPixel;
- this.HorizontalPredictor = options.HorizontalPredictor;
- this.CompressionType = options.Compression;
- this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
- this.skipMetadata = options.SkipMetadata;
+ this.PhotometricInterpretation = encoder.PhotometricInterpretation;
+ this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
+ this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
+ this.BitsPerPixel = encoder.BitsPerPixel;
+ this.HorizontalPredictor = encoder.HorizontalPredictor;
+ this.CompressionType = encoder.Compression;
+ this.compressionLevel = encoder.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
+ this.skipMetadata = encoder.SkipMetadata;
+ this.transparentColorMode = encoder.TransparentColorMode;
}
///
@@ -135,10 +141,26 @@ internal sealed class TiffEncoderCore
foreach (ImageFrame frame in image.Frames)
{
- cancellationToken.ThrowIfCancellationRequested();
+ ImageFrame? clonedFrame = null;
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
- metadataImage = null;
+ if (EncodingUtilities.ShouldClearTransparentPixels(this.transparentColorMode))
+ {
+ clonedFrame = frame.Clone();
+ EncodingUtilities.ClearTransparentPixels(clonedFrame, Color.Transparent);
+ }
+
+ ImageFrame encodingFrame = clonedFrame ?? frame;
+
+ ifdMarker = this.WriteFrame(writer, encodingFrame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
+ metadataImage = null;
+ }
+ finally
+ {
+ clonedFrame?.Dispose();
+ }
}
long currentOffset = writer.BaseStream.Position;
diff --git a/src/ImageSharp/Formats/TransparentColorMode.cs b/src/ImageSharp/Formats/TransparentColorMode.cs
new file mode 100644
index 000000000..39986b502
--- /dev/null
+++ b/src/ImageSharp/Formats/TransparentColorMode.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Specifies how transparent pixels should be handled during encoding.
+///
+public enum TransparentColorMode
+{
+ ///
+ /// Retains the original color values of transparent pixels.
+ ///
+ Preserve = 0,
+
+ ///
+ /// Converts transparent pixels with non-zero color components
+ /// to fully transparent pixels (all components set to zero),
+ /// which may improve compression.
+ ///
+ Clear = 1,
+}
diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
index 46030dde3..fd6f508e4 100644
--- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
@@ -49,7 +49,7 @@ internal static class AlphaEncoder
quality,
skipMetadata,
effort,
- WebpTransparentColorMode.Preserve,
+ TransparentColorMode.Preserve,
false,
0);
diff --git a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
index 2170eb198..736070a1c 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs
@@ -47,7 +47,7 @@ internal static unsafe class PredictorEncoder
int[][] bestHisto,
bool nearLossless,
int nearLosslessQuality,
- WebpTransparentColorMode transparentColorMode,
+ TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool lowEffort)
{
@@ -202,7 +202,7 @@ internal static unsafe class PredictorEncoder
int[][] histoArgb,
int[][] bestHisto,
int maxQuantization,
- WebpTransparentColorMode transparentColorMode,
+ TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
Span modes,
@@ -340,19 +340,20 @@ internal static unsafe class PredictorEncoder
int xEnd,
int y,
int maxQuantization,
- WebpTransparentColorMode transparentColorMode,
+ TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
Span output,
Span scratch)
{
- if (transparentColorMode == WebpTransparentColorMode.Preserve)
+ if (transparentColorMode == TransparentColorMode.Preserve)
{
PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output, scratch);
}
else
{
#pragma warning disable SA1503 // Braces should not be omitted
+#pragma warning disable RCS1001 // Add braces (when expression spans over multiple lines)
fixed (uint* currentRow = currentRowSpan)
fixed (uint* upperRow = upperRowSpan)
{
@@ -466,6 +467,7 @@ internal static unsafe class PredictorEncoder
}
}
}
+#pragma warning restore RCS1001 // Add braces (when expression spans over multiple lines)
#pragma warning restore SA1503 // Braces should not be omitted
///
@@ -577,7 +579,7 @@ internal static unsafe class PredictorEncoder
Span argbScratch,
Span argb,
int maxQuantization,
- WebpTransparentColorMode transparentColorMode,
+ TransparentColorMode transparentColorMode,
bool usedSubtractGreen,
bool nearLossless,
bool lowEffort)
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index e07724969..f08844839 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -69,7 +69,7 @@ internal class Vp8LEncoder : IDisposable
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
///
- private readonly WebpTransparentColorMode transparentColorMode;
+ private readonly TransparentColorMode transparentColorMode;
///
/// Whether to skip metadata during encoding.
@@ -114,7 +114,7 @@ internal class Vp8LEncoder : IDisposable
uint quality,
bool skipMetadata,
WebpEncodingMethod method,
- WebpTransparentColorMode transparentColorMode,
+ TransparentColorMode transparentColorMode,
bool nearLossless,
int nearLosslessQuality)
{
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index bfaaa831e..b74337ef3 100644
--- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -343,7 +343,7 @@ internal class WebpAnimationDecoder : IDisposable
return;
}
- Rectangle interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
+ Rectangle interest = Rectangle.Intersect(imageFrame.Bounds, this.restoreArea.Value);
Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
TPixel backgroundPixel = backgroundColor.ToPixel();
pixelRegion.Fill(backgroundPixel);
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs
index 226719c79..622fe0181 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs
@@ -58,13 +58,6 @@ public sealed class WebpEncoder : AnimatedImageEncoder
///
public int FilterStrength { get; init; } = 60;
- ///
- /// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
- /// RGB information for better compression.
- /// The default value is Clear.
- ///
- public WebpTransparentColorMode TransparentColorMode { get; init; } = WebpTransparentColorMode.Clear;
-
///
/// Gets a value indicating whether near lossless mode should be used.
/// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality.
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index 37d2ae0a1..b8f27a832 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -54,7 +54,7 @@ internal sealed class WebpEncoderCore
/// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible
/// RGB information for better compression.
///
- private readonly WebpTransparentColorMode transparentColorMode;
+ private readonly TransparentColorMode transparentColorMode;
///
/// Whether to skip metadata during encoding.
@@ -166,7 +166,7 @@ internal sealed class WebpEncoderCore
// Encode the first frame.
ImageFrame previousFrame = image.Frames.RootFrame;
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
- hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
+ hasAlpha |= encoder.Encode(previousFrame, previousFrame.Bounds, frameMetadata, stream, hasAnimation);
if (hasAnimation)
{
@@ -178,10 +178,7 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
- if (cancellationToken.IsCancellationRequested)
- {
- break;
- }
+ cancellationToken.ThrowIfCancellationRequested();
ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame currentFrame = image.Frames[i];
@@ -253,7 +250,7 @@ internal sealed class WebpEncoderCore
WebpFrameMetadata frameMetadata = previousFrame.Metadata.GetWebpMetadata();
FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
- hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
+ hasAlpha |= encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds, frameMetadata);
// Encode additional frames
// This frame is reused to store de-duplicated pixel buffers.
@@ -261,10 +258,7 @@ internal sealed class WebpEncoderCore
for (int i = 1; i < image.Frames.Count; i++)
{
- if (cancellationToken.IsCancellationRequested)
- {
- break;
- }
+ cancellationToken.ThrowIfCancellationRequested();
ImageFrame? prev = previousDisposal == FrameDisposalMode.RestoreToBackground ? null : previousFrame;
ImageFrame currentFrame = image.Frames[i];
diff --git a/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs b/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs
deleted file mode 100644
index c12b8ed97..000000000
--- a/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Formats.Webp;
-
-///
-/// Enum indicating how the transparency should be handled on encoding.
-///
-public enum WebpTransparentColorMode
-{
- ///
- /// Discard the transparency information for better compression.
- ///
- Clear = 0,
-
- ///
- /// The transparency will be kept as is.
- ///
- Preserve = 1,
-}
diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs
index fdde5019e..686292bc7 100644
--- a/src/ImageSharp/ImageFrame.cs
+++ b/src/ImageSharp/ImageFrame.cs
@@ -56,7 +56,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// Gets the bounds of the frame.
///
/// The
- public Rectangle Bounds() => new(0, 0, this.Width, this.Height);
+ public Rectangle Bounds => new(0, 0, this.Width, this.Height);
///
public void Dispose()
diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs
index 2287f65cd..de71e77ca 100644
--- a/src/ImageSharp/ImageFrame{TPixel}.cs
+++ b/src/ImageSharp/ImageFrame{TPixel}.cs
@@ -429,7 +429,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource
ParallelRowIterator.IterateRowIntervals(
configuration,
- this.Bounds(),
+ this.Bounds,
in operation);
return target;
diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs
index 2eb05ea93..f0fa1438d 100644
--- a/src/ImageSharp/Memory/Buffer2DExtensions.cs
+++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs
@@ -25,27 +25,65 @@ public static class Buffer2DExtensions
return buffer.FastMemoryGroup.View;
}
+ ///
+ /// Performs a deep clone of the buffer covering the specified .
+ ///
+ /// The element type.
+ /// The source buffer.
+ /// The configuration.
+ /// The rectangle to clone.
+ /// The .
+ internal static Buffer2D CloneRegion(this Buffer2D source, Configuration configuration, Rectangle rectangle)
+ where T : unmanaged
+ {
+ Buffer2D buffer = configuration.MemoryAllocator.Allocate2D(
+ rectangle.Width,
+ rectangle.Height,
+ configuration.PreferContiguousImageBuffers);
+
+ // Optimization for when the size of the area is the same as the buffer size.
+ Buffer2DRegion sourceRegion = source.GetRegion(rectangle);
+ if (sourceRegion.IsFullBufferArea)
+ {
+ sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup);
+ }
+ else
+ {
+ for (int y = 0; y < rectangle.Height; y++)
+ {
+ sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y));
+ }
+ }
+
+ return buffer;
+ }
+
///
/// TODO: Does not work with multi-buffer groups, should be specific to Resize.
- /// Copy columns of inplace,
- /// from positions starting at to positions at .
+ /// Copy columns of in-place,
+ /// from positions starting at to positions at .
///
+ /// The element type.
+ /// The .
+ /// The source column index.
+ /// The destination column index.
+ /// The number of columns to copy.
internal static unsafe void DangerousCopyColumns(
this Buffer2D buffer,
int sourceIndex,
- int destIndex,
+ int destinationIndex,
int columnCount)
where T : struct
{
DebugGuard.NotNull(buffer, nameof(buffer));
DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex));
- DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex));
- CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount);
+ DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex));
+ CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount);
int elementSize = Unsafe.SizeOf();
int width = buffer.Width * elementSize;
int sOffset = sourceIndex * elementSize;
- int dOffset = destIndex * elementSize;
+ int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize;
Span span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span);
@@ -73,9 +111,7 @@ public static class Buffer2DExtensions
/// The
internal static Rectangle FullRectangle(this Buffer2D buffer)
where T : struct
- {
- return new Rectangle(0, 0, buffer.Width, buffer.Height);
- }
+ => new(0, 0, buffer.Width, buffer.Height);
///
/// Return a to the subregion represented by 'rectangle'
@@ -86,11 +122,11 @@ public static class Buffer2DExtensions
/// The
internal static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle)
where T : unmanaged =>
- new Buffer2DRegion(buffer, rectangle);
+ new(buffer, rectangle);
internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height)
where T : unmanaged =>
- new Buffer2DRegion(buffer, new Rectangle(x, y, width, height));
+ new(buffer, new Rectangle(x, y, width, height));
///
/// Return a to the whole area of 'buffer'
@@ -100,7 +136,7 @@ public static class Buffer2DExtensions
/// The
internal static Buffer2DRegion GetRegion(this Buffer2D buffer)
where T : unmanaged =>
- new Buffer2DRegion(buffer);
+ new(buffer);
///
/// Returns the size of the buffer.
@@ -115,6 +151,8 @@ public static class Buffer2DExtensions
///
/// Gets the bounds of the buffer.
///
+ /// The element type
+ /// The
/// The
internal static Rectangle Bounds(this Buffer2D buffer)
where T : struct =>
diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
index 033b0a25a..f4b257b58 100644
--- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
@@ -107,7 +107,7 @@ public readonly struct Buffer2DRegion
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion GetSubRegion(int x, int y, int width, int height)
{
- var rectangle = new Rectangle(x, y, width, height);
+ Rectangle rectangle = new(x, y, width, height);
return this.GetSubRegion(rectangle);
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
index b4b1ffc6f..e2e933f3c 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
@@ -36,8 +36,13 @@ internal static class MemoryGroupExtensions
///
/// Returns a slice that is expected to be within the bounds of a single buffer.
- /// Otherwise is thrown.
///
+ /// The type of element.
+ /// The group.
+ /// The start index of the slice.
+ /// The length of the slice.
+ /// Slice is out of bounds.
+ /// The slice.
internal static Memory GetBoundedMemorySlice(this IMemoryGroup group, long start, int length)
where T : struct
{
diff --git a/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs b/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
index 4d95e060d..63c489508 100644
--- a/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
+++ b/src/ImageSharp/Processing/DefaultImageProcessorContext{TPixel}.cs
@@ -61,9 +61,7 @@ internal class DefaultImageProcessorContext : IInternalImageProcessingCo
///
public IImageProcessingContext ApplyProcessor(IImageProcessor processor)
- {
- return this.ApplyProcessor(processor, this.GetCurrentBounds());
- }
+ => this.ApplyProcessor(processor, this.GetCurrentBounds());
///
public IImageProcessingContext ApplyProcessor(IImageProcessor processor, Rectangle rectangle)
@@ -74,11 +72,9 @@ internal class DefaultImageProcessorContext : IInternalImageProcessingCo
// interim clone if the first processor in the pipeline is a cloning processor.
if (processor is ICloningImageProcessor cloningImageProcessor)
{
- using (ICloningImageProcessor pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle))
- {
- this.destination = pixelProcessor.CloneAndExecute();
- return this;
- }
+ using ICloningImageProcessor pixelProcessor = cloningImageProcessor.CreatePixelSpecificCloningProcessor(this.Configuration, this.source, rectangle);
+ this.destination = pixelProcessor.CloneAndExecute();
+ return this;
}
// Not a cloning processor? We need to create a clone to operate on.
diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
index 713d4d5b7..676acee0f 100644
--- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
+++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
-using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -42,7 +41,7 @@ public static partial class ProcessingExtensions
/// The containing all the sums.
public static Buffer2D CalculateIntegralImage(this ImageFrame source)
where TPixel : unmanaged, IPixel
- => source.CalculateIntegralImage(source.Bounds());
+ => source.CalculateIntegralImage(source.Bounds);
///
/// Apply an image integral.
@@ -56,7 +55,7 @@ public static partial class ProcessingExtensions
{
Configuration configuration = source.Configuration;
- var interest = Rectangle.Intersect(bounds, source.Bounds());
+ Rectangle interest = Rectangle.Intersect(bounds, source.Bounds);
int startY = interest.Y;
int startX = interest.X;
int endY = interest.Height;
diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
index 73c7c3302..e17de49d7 100644
--- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Binarization;
///
/// Performs Bradley Adaptive Threshold filter against an image.
///
+/// The pixel format.
internal class AdaptiveThresholdProcessor : ImageProcessor
where TPixel : unmanaged, IPixel
{
@@ -30,7 +31,7 @@ internal class AdaptiveThresholdProcessor : ImageProcessor
///
protected override void OnFrameApply(ImageFrame source)
{
- var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
+ Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
Configuration configuration = this.Configuration;
TPixel upper = this.definition.Upper.ToPixel();
@@ -97,19 +98,23 @@ internal class AdaptiveThresholdProcessor : ImageProcessor
Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length);
PixelOperations.Instance.ToL8(this.configuration, rowSpan, span);
+ int startY = this.startY;
int maxX = this.bounds.Width - 1;
int maxY = this.bounds.Height - 1;
+ int clusterSize = this.clusterSize;
+ float thresholdLimit = this.thresholdLimit;
+ Buffer2D image = this.intImage;
for (int x = 0; x < rowSpan.Length; x++)
{
- int x1 = Math.Clamp(x - this.clusterSize + 1, 0, maxX);
- int x2 = Math.Min(x + this.clusterSize + 1, maxX);
- int y1 = Math.Clamp(y - this.startY - this.clusterSize + 1, 0, maxY);
- int y2 = Math.Min(y - this.startY + this.clusterSize + 1, maxY);
+ int x1 = Math.Clamp(x - clusterSize + 1, 0, maxX);
+ int x2 = Math.Min(x + clusterSize + 1, maxX);
+ int y1 = Math.Clamp(y - startY - clusterSize + 1, 0, maxY);
+ int y2 = Math.Min(y - startY + clusterSize + 1, maxY);
uint count = (uint)((x2 - x1) * (y2 - y1));
- ulong sum = Math.Min(this.intImage[x2, y2] - this.intImage[x1, y2] - this.intImage[x2, y1] + this.intImage[x1, y1], ulong.MaxValue);
+ ulong sum = Math.Min(image[x2, y2] - image[x1, y2] - image[x2, y1] + image[x1, y1], ulong.MaxValue);
- if (span[x].PackedValue * count <= sum * this.thresholdLimit)
+ if (span[x].PackedValue * count <= sum * thresholdLimit)
{
rowSpan[x] = this.lower;
}
diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
index 1c76ea6a4..ad87f36c1 100644
--- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs
@@ -38,8 +38,8 @@ internal class BinaryThresholdProcessor : ImageProcessor
Rectangle sourceRectangle = this.SourceRectangle;
Configuration configuration = this.Configuration;
- var interest = Rectangle.Intersect(sourceRectangle, source.Bounds());
- var operation = new RowOperation(
+ Rectangle interest = Rectangle.Intersect(sourceRectangle, source.Bounds);
+ RowOperation operation = new(
interest.X,
source.PixelBuffer,
upper,
@@ -169,10 +169,8 @@ internal class BinaryThresholdProcessor : ImageProcessor
{
return chroma / (max + min);
}
- else
- {
- return chroma / (2F - max - min);
- }
+
+ return chroma / (2F - max - min);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
index 5931b7c40..a96fa1993 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
@@ -75,12 +75,12 @@ internal class BokehBlurProcessor : ImageProcessor
///
protected override void OnFrameApply(ImageFrame source)
{
- var sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
+ Rectangle sourceRectangle = Rectangle.Intersect(this.SourceRectangle, source.Bounds);
// Preliminary gamma highlight pass
if (this.gamma == 3F)
{
- var gammaOperation = new ApplyGamma3ExposureRowOperation(sourceRectangle, source.PixelBuffer, this.Configuration);
+ ApplyGamma3ExposureRowOperation gammaOperation = new(sourceRectangle, source.PixelBuffer, this.Configuration);
ParallelRowIterator.IterateRows(
this.Configuration,
sourceRectangle,
@@ -88,7 +88,7 @@ internal class BokehBlurProcessor