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/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
index 4e9a432b1..01b7fbce0 100644
--- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
@@ -48,13 +48,13 @@ public class CurFrameMetadata : IFormatFrameMetadata
/// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
///
- public byte EncodingWidth { get; set; }
+ public byte? EncodingWidth { get; set; }
///
/// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
///
- public byte EncodingHeight { get; set; }
+ public byte? EncodingHeight { get; set; }
///
/// Gets or sets the number of bits per pixel.
@@ -80,20 +80,6 @@ public class CurFrameMetadata : IFormatFrameMetadata
};
}
- byte encodingWidth = metadata.EncodingWidth switch
- {
- > 255 => 0,
- <= 255 and >= 1 => (byte)metadata.EncodingWidth,
- _ => 0
- };
-
- byte encodingHeight = metadata.EncodingHeight switch
- {
- > 255 => 0,
- <= 255 and >= 1 => (byte)metadata.EncodingHeight,
- _ => 0
- };
-
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
@@ -116,8 +102,8 @@ public class CurFrameMetadata : IFormatFrameMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
- EncodingWidth = encodingWidth,
- EncodingHeight = encodingHeight,
+ EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
+ EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
@@ -138,8 +124,8 @@ public class CurFrameMetadata : IFormatFrameMetadata
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
- this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
- this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
+ this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
+ this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
///
@@ -156,7 +142,7 @@ public class CurFrameMetadata : IFormatFrameMetadata
this.HotspotY = entry.BitCount;
}
- internal IconDirEntry ToIconDirEntry()
+ internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
@@ -164,8 +150,8 @@ public class CurFrameMetadata : IFormatFrameMetadata
return new()
{
- Width = this.EncodingWidth,
- Height = this.EncodingHeight,
+ Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
+ Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = this.HotspotX,
BitCount = this.HotspotY,
ColorCount = colorCount
@@ -233,13 +219,22 @@ public class CurFrameMetadata : IFormatFrameMetadata
};
}
- private static byte Scale(byte? value, int destination, float ratio)
+ private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
- return (byte)Math.Clamp(destination, 0, 255);
+ return ClampEncodingDimension(destination);
}
- return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}
+
+ private static byte ClampEncodingDimension(float? dimension)
+ => dimension switch
+ {
+ // Encoding dimensions can be between 0-256 where 0 means 256 or greater.
+ > 255 => 0,
+ <= 255 and >= 1 => (byte)dimension,
+ _ => 0
+ };
}
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..797e825dc 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.
@@ -182,22 +207,29 @@ internal sealed class GifEncoderCore
this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile);
}
- this.EncodeFirstFrame(stream, frameMetadata, quantized);
-
- // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
- TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
+ // If the token is cancelled during encoding of frames we must ensure the
+ // quantized frame is disposed.
+ try
+ {
+ this.EncodeFirstFrame(stream, frameMetadata, quantized, cancellationToken);
- this.EncodeAdditionalFrames(
- stream,
- image,
- globalPalette,
- derivedTransparencyIndex,
- frameMetadata.DisposalMode,
- cancellationToken);
+ // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
+ TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
- stream.WriteByte(GifConstants.EndIntroducer);
+ this.EncodeAdditionalFrames(
+ stream,
+ image,
+ globalPalette,
+ derivedTransparencyIndex,
+ frameMetadata.DisposalMode,
+ cancellationToken);
+ }
+ finally
+ {
+ stream.WriteByte(GifConstants.EndIntroducer);
- quantized?.Dispose();
+ quantized?.Dispose();
+ }
}
private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex)
@@ -236,61 +268,61 @@ 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();
+ }
}
}
private void EncodeFirstFrame(
Stream stream,
GifFrameMetadata metadata,
- IndexedImageFrame quantized)
+ IndexedImageFrame quantized,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ cancellationToken.ThrowIfCancellationRequested();
+
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
@@ -324,7 +356,9 @@ internal sealed class GifEncoderCore
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
- ImageFrame? 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 +375,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/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
index a2d1c0139..62aa705cb 100644
--- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
@@ -41,13 +41,13 @@ public class IcoFrameMetadata : IFormatFrameMetadata
/// Gets or sets the encoding width.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
///
- public byte EncodingWidth { get; set; }
+ public byte? EncodingWidth { get; set; }
///
/// Gets or sets the encoding height.
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
///
- public byte EncodingHeight { get; set; }
+ public byte? EncodingHeight { get; set; }
///
/// Gets or sets the number of bits per pixel.
@@ -73,20 +73,6 @@ public class IcoFrameMetadata : IFormatFrameMetadata
};
}
- byte encodingWidth = metadata.EncodingWidth switch
- {
- > 255 => 0,
- <= 255 and >= 1 => (byte)metadata.EncodingWidth,
- _ => 0
- };
-
- byte encodingHeight = metadata.EncodingHeight switch
- {
- > 255 => 0,
- <= 255 and >= 1 => (byte)metadata.EncodingHeight,
- _ => 0
- };
-
int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel;
BmpBitsPerPixel bbpp = bpp switch
{
@@ -109,8 +95,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata
{
BmpBitsPerPixel = bbpp,
Compression = compression,
- EncodingWidth = encodingWidth,
- EncodingHeight = encodingHeight,
+ EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
+ EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null
};
}
@@ -131,8 +117,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
- this.EncodingWidth = Scale(this.EncodingWidth, destination.Width, ratioX);
- this.EncodingHeight = Scale(this.EncodingHeight, destination.Height, ratioY);
+ this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
+ this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
}
///
@@ -147,7 +133,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata
this.EncodingHeight = entry.Height;
}
- internal IconDirEntry ToIconDirEntry()
+ internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
@@ -155,8 +141,8 @@ public class IcoFrameMetadata : IFormatFrameMetadata
return new()
{
- Width = this.EncodingWidth,
- Height = this.EncodingHeight,
+ Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
+ Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),
Planes = 1,
ColorCount = colorCount,
BitCount = this.Compression switch
@@ -228,13 +214,22 @@ public class IcoFrameMetadata : IFormatFrameMetadata
};
}
- private static byte Scale(byte? value, int destination, float ratio)
+ private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
- return (byte)Math.Clamp(destination, 0, 255);
+ return ClampEncodingDimension(destination);
}
- return Math.Min((byte)MathF.Ceiling(value.Value * ratio), (byte)Math.Clamp(destination, 0, 255));
+ return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
}
+
+ private static byte ClampEncodingDimension(float? dimension)
+ => dimension switch
+ {
+ // Encoding dimensions can be between 0-256 where 0 means 256 or greater.
+ > 255 => 0,
+ <= 255 and >= 1 => (byte)dimension,
+ _ => 0
+ };
}
diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
index 4b973d511..03e01f912 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(),
@@ -121,13 +123,13 @@ internal abstract class IconEncoderCore
image.Frames.Select(i =>
{
IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
- return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry());
+ return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
}).ToArray(),
IconFileType.CUR =>
image.Frames.Select(i =>
{
CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
- return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry());
+ return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
}).ToArray(),
_ => throw new NotSupportedException(),
};
diff --git a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
index dddc629b3..8b379e4d7 100644
--- a/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
+++ b/src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
@@ -17,25 +17,32 @@ internal class BinaryEncoder
///
/// The type of input pixel.
/// The configuration.
- /// The bytestream to write to.
+ /// The byte stream to write to.
/// The input image.
/// The ColorType to use.
- /// Data type of the pixles components.
- ///
+ /// Data type of the pixels components.
+ /// The token to monitor for cancellation requests.
+ ///
/// Thrown if an invalid combination of setting is requested.
///
- public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType)
+ public static void WritePixels(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ PbmColorType colorType,
+ PbmComponentType componentType,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
if (colorType == PbmColorType.Grayscale)
{
if (componentType == PbmComponentType.Byte)
{
- WriteGrayscale(configuration, stream, image);
+ WriteGrayscale(configuration, stream, image, cancellationToken);
}
else if (componentType == PbmComponentType.Short)
{
- WriteWideGrayscale(configuration, stream, image);
+ WriteWideGrayscale(configuration, stream, image, cancellationToken);
}
else
{
@@ -46,31 +53,28 @@ internal class BinaryEncoder
{
if (componentType == PbmComponentType.Byte)
{
- WriteRgb(configuration, stream, image);
+ WriteRgb(configuration, stream, image, cancellationToken);
}
else if (componentType == PbmComponentType.Short)
{
- WriteWideRgb(configuration, stream, image);
+ WriteWideRgb(configuration, stream, image, cancellationToken);
}
else
{
throw new ImageFormatException("Component type not supported for Color PBM.");
}
}
- else
+ else if (componentType == PbmComponentType.Bit)
{
- if (componentType == PbmComponentType.Bit)
- {
- WriteBlackAndWhite(configuration, stream, image);
- }
- else
- {
- throw new ImageFormatException("Component type not supported for Black & White PBM.");
- }
+ WriteBlackAndWhite(configuration, stream, image, cancellationToken);
}
}
- private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteGrayscale(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -82,6 +86,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL8Bytes(
@@ -94,7 +100,11 @@ internal class BinaryEncoder
}
}
- private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteWideGrayscale(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
const int bytesPerPixel = 2;
@@ -107,6 +117,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL16Bytes(
@@ -119,7 +131,11 @@ internal class BinaryEncoder
}
}
- private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteRgb(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
const int bytesPerPixel = 3;
@@ -132,6 +148,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgb24Bytes(
@@ -144,7 +162,11 @@ internal class BinaryEncoder
}
}
- private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteWideRgb(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
const int bytesPerPixel = 6;
@@ -157,6 +179,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgb48Bytes(
@@ -169,7 +193,12 @@ internal class BinaryEncoder
}
}
- private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteBlackAndWhite(
+ Configuration
+ configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -181,6 +210,8 @@ internal class BinaryEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL8(
diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
index 843f1880e..e0330ca6b 100644
--- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
+++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs
@@ -68,8 +68,7 @@ internal sealed class PbmEncoderCore
byte signature = this.DeduceSignature();
this.WriteHeader(stream, signature, image.Size);
-
- this.WritePixels(stream, image.Frames.RootFrame);
+ this.WritePixels(stream, image.Frames.RootFrame, cancellationToken);
stream.Flush();
}
@@ -167,16 +166,29 @@ internal sealed class PbmEncoderCore
///
/// The containing pixel data.
///
- private void WritePixels(Stream stream, ImageFrame image)
+ /// The token to monitor for cancellation requests.
+ private void WritePixels(Stream stream, ImageFrame image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
if (this.encoding == PbmEncoding.Plain)
{
- PlainEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType);
+ PlainEncoder.WritePixels(
+ this.configuration,
+ stream,
+ image,
+ this.colorType,
+ this.componentType,
+ cancellationToken);
}
else
{
- BinaryEncoder.WritePixels(this.configuration, stream, image, this.colorType, this.componentType);
+ BinaryEncoder.WritePixels(
+ this.configuration,
+ stream,
+ image,
+ this.colorType,
+ this.componentType,
+ cancellationToken);
}
}
}
diff --git a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs
index 29260f54a..bab508720 100644
--- a/src/ImageSharp/Formats/Pbm/PlainEncoder.cs
+++ b/src/ImageSharp/Formats/Pbm/PlainEncoder.cs
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
///
/// Pixel encoding methods for the PBM plain encoding.
///
-internal class PlainEncoder
+internal static class PlainEncoder
{
private const byte NewLine = 0x0a;
private const byte Space = 0x20;
@@ -31,45 +31,56 @@ internal class PlainEncoder
///
/// The type of input pixel.
/// The configuration.
- /// The bytestream to write to.
+ /// The byte stream to write to.
/// The input image.
/// The ColorType to use.
- /// Data type of the pixles components.
- public static void WritePixels(Configuration configuration, Stream stream, ImageFrame image, PbmColorType colorType, PbmComponentType componentType)
+ /// Data type of the pixels components.
+ /// The token to monitor for cancellation requests.
+ public static void WritePixels(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ PbmColorType colorType,
+ PbmComponentType componentType,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
if (colorType == PbmColorType.Grayscale)
{
if (componentType == PbmComponentType.Byte)
{
- WriteGrayscale(configuration, stream, image);
+ WriteGrayscale(configuration, stream, image, cancellationToken);
}
else
{
- WriteWideGrayscale(configuration, stream, image);
+ WriteWideGrayscale(configuration, stream, image, cancellationToken);
}
}
else if (colorType == PbmColorType.Rgb)
{
if (componentType == PbmComponentType.Byte)
{
- WriteRgb(configuration, stream, image);
+ WriteRgb(configuration, stream, image, cancellationToken);
}
else
{
- WriteWideRgb(configuration, stream, image);
+ WriteWideRgb(configuration, stream, image, cancellationToken);
}
}
else
{
- WriteBlackAndWhite(configuration, stream, image);
+ WriteBlackAndWhite(configuration, stream, image, cancellationToken);
}
// Write EOF indicator, as some encoders expect it.
stream.WriteByte(Space);
}
- private static void WriteGrayscale(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteGrayscale(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -83,6 +94,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL8(
configuration,
@@ -102,7 +115,11 @@ internal class PlainEncoder
}
}
- private static void WriteWideGrayscale(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteWideGrayscale(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -116,6 +133,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL16(
configuration,
@@ -135,7 +154,11 @@ internal class PlainEncoder
}
}
- private static void WriteRgb(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteRgb(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -149,6 +172,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgb24(
configuration,
@@ -174,7 +199,11 @@ internal class PlainEncoder
}
}
- private static void WriteWideRgb(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteWideRgb(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -188,6 +217,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgb48(
configuration,
@@ -213,7 +244,11 @@ internal class PlainEncoder
}
}
- private static void WriteBlackAndWhite(Configuration configuration, Stream stream, ImageFrame image)
+ private static void WriteBlackAndWhite(
+ Configuration configuration,
+ Stream stream,
+ ImageFrame image,
+ CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
int width = image.Width;
@@ -227,6 +262,8 @@ internal class PlainEncoder
for (int y = 0; y < height; y++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
Span pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
PixelOperations.Instance.ToL8(
configuration,
@@ -236,8 +273,7 @@ internal class PlainEncoder
int written = 0;
for (int x = 0; x < width; x++)
{
- byte value = (rowSpan[x].PackedValue < 128) ? One : Zero;
- plainSpan[written++] = value;
+ plainSpan[written++] = (rowSpan[x].PackedValue < 128) ? One : Zero;
plainSpan[written++] = Space;
}
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..ea36d9fe1 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);
@@ -225,91 +226,94 @@ internal sealed class PngEncoderCore : IDisposable
bool userAnimateRootFrame = this.animateRootFrame == true;
if ((!userAnimateRootFrame && !pngMetadata.AnimateRootFrame) || image.Frames.Count == 1)
{
+ cancellationToken.ThrowIfCancellationRequested();
FrameControl frameControl = new((uint)this.width, (uint)this.height);
this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
currentFrameIndex++;
}
- if (image.Frames.Count > 1)
+ try
{
- // Write the first animated frame.
- currentFrame = image.Frames[currentFrameIndex];
- PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
- FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
- FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
- uint sequenceNumber = 1;
- if (pngMetadata.AnimateRootFrame)
+ if (image.Frames.Count > 1)
{
- this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
- }
- else
- {
- sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
- }
-
- currentFrameIndex++;
+ // Write the first animated frame.
+ currentFrame = image.Frames[currentFrameIndex];
+ PngFrameMetadata frameMetadata = currentFrame.Metadata.GetPngMetadata();
+ FrameDisposalMode previousDisposal = frameMetadata.DisposalMode;
+ FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds, 0);
+ uint sequenceNumber = 1;
+ if (pngMetadata.AnimateRootFrame)
+ {
+ this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
+ }
+ else
+ {
+ sequenceNumber += this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, true);
+ }
- // Capture the global palette for reuse on subsequent frames.
- ReadOnlyMemory? 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 +323,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..da55ef9f9 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;
}
///
@@ -131,14 +137,30 @@ internal sealed class TiffEncoderCore
long ifdMarker = WriteHeader(writer, buffer);
- Image? metadataImage = image;
+ Image? imageMetadata = image;
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, imageMetadata, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker);
+ imageMetadata = 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;
+
+///