@ -2,13 +2,17 @@
// Licensed under the Six Labors Split License.
using System.Buffers ;
using System.Numerics ;
using System.Runtime.CompilerServices ;
using System.Runtime.InteropServices ;
using System.Runtime.Intrinsics ;
using System.Runtime.Intrinsics.X86 ;
using SixLabors.ImageSharp.Advanced ;
using SixLabors.ImageSharp.Memory ;
using SixLabors.ImageSharp.Metadata ;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp ;
using SixLabors.ImageSharp.PixelFormats ;
using SixLabors.ImageSharp.Processing ;
using SixLabors.ImageSharp.Processing.Processors.Quantization ;
namespace SixLabors.ImageSharp.Formats.Gif ;
@ -36,17 +40,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private readonly IQuantizer quantizer ;
private IQuantizer ? quantizer ;
/// <summary>
/// The color table mode: Global or local .
/// Whether the quantizer was supplied via options .
/// </summary>
private GifColorTableMode ? colorTableMode ;
private readonly bool hasQuantizer ;
/// <summary>
/// The number of bits requires to store the color palette .
/// The color table mode: Global or local .
/// </summary>
private int bitDepth ;
private GifColorTableMode ? colorTableMode ;
/// <summary>
/// The pixel sampling strategy for global quantization.
@ -56,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviou r or extending the library.</param>
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
/// <param name="encoder">The encoder with options.</param>
public GifEncoderCore ( Configuration configuration , GifEncoder encoder )
{
@ -64,6 +68,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this . memoryAllocator = configuration . MemoryAllocator ;
this . skipMetadata = encoder . SkipMetadata ;
this . quantizer = encoder . Quantizer ;
this . hasQuantizer = encoder . Quantizer is not null ;
this . colorTableMode = encoder . ColorTableMode ;
this . pixelSamplingStrategy = encoder . PixelSamplingStrategy ;
}
@ -86,8 +91,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this . colorTableMode ? ? = gifMetadata . ColorTableMode ;
bool useGlobalTable = this . colorTableMode = = GifColorTableMode . Global ;
// Quantize the image returning a palette.
IndexedImageFrame < TPixel > ? quantized ;
// Quantize the first image frame returning a palette.
IndexedImageFrame < TPixel > ? quantized = null ;
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
image . Frames . RootFrame . Metadata . TryGetGifMetadata ( out GifFrameMetadata ? frameMetadata ) ;
int transparencyIndex = GetTransparentIndex ( quantized , frameMetadata ) ;
if ( this . quantizer is null )
{
// Is this a gif with color information. If so use that, otherwise use octree.
if ( gifMetadata . ColorTableMode = = GifColorTableMode . Global & & gifMetadata . GlobalColorTable ? . Length > 0 )
{
// We avoid dithering by default to preserve the original colors.
this . quantizer = new PaletteQuantizer ( gifMetadata . GlobalColorTable . Value , new ( ) { Dither = null } , transparencyIndex ) ;
}
else
{
this . quantizer = KnownQuantizers . Octree ;
}
}
using ( IQuantizer < TPixel > frameQuantizer = this . quantizer . CreatePixelSpecificQuantizer < TPixel > ( this . configuration ) )
{
if ( useGlobalTable )
@ -102,19 +127,24 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
// Get the number of bits.
this . bitDepth = ColorNumerics . GetBitsNeededForColorDepth ( quantized . Palette . Length ) ;
// Write the header.
WriteHeader ( stream ) ;
// Write the LSD.
int index = GetTransparentIndex ( quantized ) ;
this . WriteLogicalScreenDescriptor ( metadata , image . Width , image . Height , index , useGlobalTable , stream ) ;
transparencyIndex = GetTransparentIndex ( quantized , frameMetadata ) ;
byte backgroundIndex = unchecked ( ( byte ) transparencyIndex ) ;
if ( transparencyIndex = = - 1 )
{
backgroundIndex = gifMetadata . BackgroundColorIndex ;
}
// Get the number of bits.
int bitDepth = ColorNumerics . GetBitsNeededForColorDepth ( quantized . Palette . Length ) ;
this . WriteLogicalScreenDescriptor ( metadata , image . Width , image . Height , backgroundIndex , useGlobalTable , bitDepth , stream ) ;
if ( useGlobalTable )
{
this . WriteColorTable ( quantized , stream ) ;
this . WriteColorTable ( quantized , bitDepth , stream ) ;
}
if ( ! this . skipMetadata )
@ -127,41 +157,68 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this . WriteApplicationExtensions ( stream , image . Frames . Count , gifMetadata . RepeatCount , xmpProfile ) ;
}
this . EncodeFrames ( stream , image , quantized , quantized . Palette . ToArray ( ) ) ;
this . EncodeFirstFrame ( stream , frameMetadata , quantized , transparencyIndex ) ;
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel [ ] globalPalette = image . Frames . Count = = 1 ? Array . Empty < TPixel > ( ) : quantized . Palette . ToArray ( ) ;
quantized . Dispose ( ) ;
this . EncodeAdditionalFrames ( stream , image , globalPalette ) ;
stream . WriteByte ( GifConstants . EndIntroducer ) ;
}
private void EncodeFrames < TPixel > (
private void EncodeAdditional Frames < TPixel > (
Stream stream ,
Image < TPixel > image ,
IndexedImageFrame < TPixel > quantized ,
ReadOnlyMemory < TPixel > palette )
ReadOnlyMemory < TPixel > globalPalette )
where TPixel : unmanaged , IPixel < TPixel >
{
if ( image . Frames . Count = = 1 )
{
return ;
}
PaletteQuantizer < TPixel > paletteQuantizer = default ;
bool hasPaletteQuantizer = false ;
for ( int i = 0 ; i < image . Frames . Count ; i + + )
// Store the first frame as a reference for de-duplication comparison.
ImageFrame < TPixel > previousFrame = image . Frames . RootFrame ;
// This frame is reused to store de-duplicated pixel buffers.
// This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
// frames using both local and global palettes.
using ImageFrame < TPixel > encodingFrame = new ( previousFrame . GetConfiguration ( ) , previousFrame . Size ( ) ) ;
for ( int i = 1 ; i < image . Frames . Count ; i + + )
{
// Gather the metadata for this frame.
ImageFrame < TPixel > frame = image . Frames [ i ] ;
ImageFrameMetadata metadata = frame . Metadata ;
bool hasMetadata = metadata . TryGetGifMetadata ( out GifFrameMetadata ? frameMetadata ) ;
bool useLocal = this . colorTableMode = = GifColorTableMode . Local | | ( hasMetadata & & frameMetadata ! . ColorTableMode = = GifColorTableMode . Local ) ;
ImageFrame < TPixel > currentF rame = image . Frames [ i ] ;
ImageFrameMetadata metadata = currentF rame. Metadata ;
metadata . TryGetGifMetadata ( out GifFrameMetadata ? gi fMetadata) ;
bool useLocal = this . colorTableMode = = GifColorTableMode . Local | | ( gifMetadata ? . ColorTableMode = = GifColorTableMode . Local ) ;
if ( ! useLocal & & ! hasPaletteQuantizer & & i > 0 )
{
// The palette quantizer can reuse the same 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.
// 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 = = true ? gifMetadata . TransparencyIndex : - 1 ;
paletteQuantizer = new ( this . configuration , this . quantizer ! . Options , globalPalette , transparencyIndex ) ;
hasPaletteQuantizer = true ;
paletteQuantizer = new ( this . configuration , this . quantizer . Options , palette ) ;
}
this . EncodeFrame ( stream , frame , i , useLocal , frameMetadata , ref quantized ! , ref paletteQuantizer ) ;
this . EncodeAdditionalFrame (
stream ,
previousFrame ,
currentFrame ,
encodingFrame ,
useLocal ,
gifMetadata ,
paletteQuantizer ) ;
// Clean up for the next run.
quantized . Dispose ( ) ;
previousFrame = currentFrame ;
}
if ( hasPaletteQuantizer )
@ -170,88 +227,419 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
}
private void EncodeFrame < TPixel > (
private void EncodeFirstF rame < TPixel > (
Stream stream ,
ImageFrame < TPixel > frame ,
int frameIndex ,
GifFrameMetadata ? metadata ,
IndexedImageFrame < TPixel > quantized ,
int transparencyIndex )
where TPixel : unmanaged , IPixel < TPixel >
{
this . WriteGraphicalControlExtension ( metadata , transparencyIndex , stream ) ;
Buffer2D < byte > indices = ( ( IPixelSource ) quantized ) . PixelBuffer ;
Rectangle interest = indices . FullRectangle ( ) ;
bool useLocal = this . colorTableMode = = GifColorTableMode . Local | | ( metadata ? . ColorTableMode = = GifColorTableMode . Local ) ;
int bitDepth = ColorNumerics . GetBitsNeededForColorDepth ( quantized . Palette . Length ) ;
this . WriteImageDescriptor ( interest , useLocal , bitDepth , stream ) ;
if ( useLocal )
{
this . WriteColorTable ( quantized , bitDepth , stream ) ;
}
this . WriteImageData ( indices , interest , stream , quantized . Palette . Length , transparencyIndex ) ;
}
private void EncodeAdditionalFrame < TPixel > (
Stream stream ,
ImageFrame < TPixel > previousFrame ,
ImageFrame < TPixel > currentFrame ,
ImageFrame < TPixel > encodingFrame ,
bool useLocal ,
GifFrameMetadata ? metadata ,
ref IndexedImageFrame < TPixel > quantized ,
ref PaletteQuantizer < TPixel > paletteQuantizer )
PaletteQuantizer < TPixel > globalPaletteQuantizer )
where TPixel : unmanaged , IPixel < TPixel >
{
// The first frame has already been quantized so we do not need to do so again.
if ( frameIndex > 0 )
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata ? . HasTransparency = = true ? metadata . TransparencyIndex : - 1 ;
Vector4 replacement = Vector4 . Zero ;
if ( transparencyIndex > = 0 )
{
if ( useLocal )
{
// Reassign using the current frame and details.
QuantizerOptions ? options = null ;
int colorTableLength = metadata ? . ColorTableLength ? ? 0 ;
if ( colorTableLength > 0 )
if ( metadata ? . LocalColorTable ? . Length > 0 )
{
options = new ( )
ReadOnlySpan < Color > palette = metadata . LocalColorTable . Value . Span ;
if ( transparencyIndex < palette . Length )
{
Dither = this . quantizer . Options . Dither ,
DitherScale = this . quantizer . Options . DitherScale ,
MaxColors = colorTableLength
} ;
replacement = palette [ transparencyIndex ] . ToScaledVector4 ( ) ;
}
}
}
else
{
ReadOnlySpan < TPixel > palette = globalPaletteQuantizer . Palette . Span ;
if ( transparencyIndex < palette . Length )
{
replacement = palette [ transparencyIndex ] . ToScaledVector4 ( ) ;
}
}
}
this . DeDuplicatePixels ( previousFrame , currentFrame , encodingFrame , replacement ) ;
using IQuantizer < TPixel > frameQuantizer = this . quantizer . CreatePixelSpecificQuantizer < TPixel > ( this . configuration , options ? ? this . quantizer . Options ) ;
quantized = frameQuantizer . BuildPaletteAndQuantizeFrame ( frame , frame . Bounds ( ) ) ;
IndexedImageFrame < TPixel > quantized ;
if ( useLocal )
{
// Reassign using the current frame and details.
if ( metadata ? . LocalColorTable ? . Length > 0 )
{
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlyMemory < Color > palette = metadata . LocalColorTable . Value ;
PaletteQuantizer quantizer = new ( palette , new ( ) { Dither = null } , transparencyIndex ) ;
using IQuantizer < TPixel > frameQuantizer = quantizer . CreatePixelSpecificQuantizer < TPixel > ( this . configuration , quantizer . Options ) ;
quantized = frameQuantizer . BuildPaletteAndQuantizeFrame ( encodingFrame , encodingFrame . Bounds ( ) ) ;
}
else
{
// Quantize the image using the global palette.
quantized = paletteQuantizer . QuantizeFrame ( frame , frame . Bounds ( ) ) ;
// We must quantize the frame to generate a local color table.
IQuantizer quantizer = this . hasQuantizer ? this . quantizer ! : KnownQuantizers . Octree ;
using IQuantizer < TPixel > frameQuantizer = quantizer . CreatePixelSpecificQuantizer < TPixel > ( this . configuration , quantizer . Options ) ;
quantized = frameQuantizer . BuildPaletteAndQuantizeFrame ( encodingFrame , encodingFrame . Bounds ( ) ) ;
}
}
else
{
// Quantize the image using the global palette.
// Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
globalPaletteQuantizer . SetTransparentIndex ( transparencyIndex ) ;
quantized = globalPaletteQuantizer . QuantizeFrame ( encodingFrame , encodingFrame . Bounds ( ) ) ;
}
// Recalculate the transparency index as depending on the quantizer used could have a new value.
transparencyIndex = GetTransparentIndex ( quantized , metadata ) ;
this . bitDepth = ColorNumerics . GetBitsNeededForColorDepth ( quantized . Palette . Length ) ;
// Trim down the buffer to the minimum size required.
Buffer2D < byte > indices = ( ( IPixelSource ) quantized ) . PixelBuffer ;
Rectangle interest = TrimTransparentPixels ( indices , transparencyIndex ) ;
this . WriteGraphicalControlExtension ( metadata , transparencyIndex , stream ) ;
int bitDepth = ColorNumerics . GetBitsNeededForColorDepth ( quantized . Palette . Length ) ;
this . WriteImageDescriptor ( interest , useLocal , bitDepth , stream ) ;
if ( useLocal )
{
this . WriteColorTable ( quantized , bitDepth , stream ) ;
}
// Do we have extension information to write?
int index = GetTransparentIndex ( quantized ) ;
if ( metadata ! = null | | index > - 1 )
this . WriteImageData ( indices , interest , stream , quantized . Palette . Length , transparencyIndex ) ;
}
private void DeDuplicatePixels < TPixel > (
ImageFrame < TPixel > backgroundFrame ,
ImageFrame < TPixel > sourceFrame ,
ImageFrame < TPixel > resultFrame ,
Vector4 replacement )
where TPixel : unmanaged , IPixel < TPixel >
{
IMemoryOwner < Vector4 > buffers = this . memoryAllocator . Allocate < Vector4 > ( backgroundFrame . Width * 3 ) ;
Span < Vector4 > background = buffers . GetSpan ( ) [ . . backgroundFrame . Width ] ;
Span < Vector4 > source = buffers . GetSpan ( ) [ backgroundFrame . Width . . ] ;
Span < Vector4 > result = buffers . GetSpan ( ) [ ( backgroundFrame . Width * 2 ) . . ] ;
// TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color
// is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better.
// This would require a more complex algorithm.
for ( int y = 0 ; y < backgroundFrame . Height ; y + + )
{
this . WriteGraphicalControlExtension ( metadata ? ? new ( ) , index , stream ) ;
PixelOperations < TPixel > . Instance . ToVector4 ( this . configuration , backgroundFrame . DangerousGetPixelRowMemory ( y ) . Span , background , PixelConversionModifiers . Scale ) ;
PixelOperations < TPixel > . Instance . ToVector4 ( this . configuration , sourceFrame . DangerousGetPixelRowMemory ( y ) . Span , source , PixelConversionModifiers . Scale ) ;
ref Vector256 < float > backgroundBase = ref Unsafe . As < Vector4 , Vector256 < float > > ( ref MemoryMarshal . GetReference ( background ) ) ;
ref Vector256 < float > sourceBase = ref Unsafe . As < Vector4 , Vector256 < float > > ( ref MemoryMarshal . GetReference ( source ) ) ;
ref Vector256 < float > resultBase = ref Unsafe . As < Vector4 , Vector256 < float > > ( ref MemoryMarshal . GetReference ( result ) ) ;
uint x = 0 ;
int remaining = background . Length ;
if ( Avx2 . IsSupported & & remaining > = 2 )
{
Vector256 < float > replacement256 = Vector256 . Create ( replacement . X , replacement . Y , replacement . Z , replacement . W , replacement . X , replacement . Y , replacement . Z , replacement . W ) ;
while ( remaining > = 2 )
{
Vector256 < float > b = Unsafe . Add ( ref backgroundBase , x ) ;
Vector256 < float > s = Unsafe . Add ( ref sourceBase , x ) ;
Vector256 < int > m = Avx . CompareEqual ( b , s ) . AsInt32 ( ) ;
m = Avx2 . HorizontalAdd ( m , m ) ;
m = Avx2 . HorizontalAdd ( m , m ) ;
m = Avx2 . CompareEqual ( m , Vector256 . Create ( - 4 ) ) ;
Unsafe . Add ( ref resultBase , x ) = Avx . BlendVariable ( s , replacement256 , m . AsSingle ( ) ) ;
x + + ;
remaining - = 2 ;
}
}
for ( int i = remaining ; i > = 0 ; i - - )
{
x = ( uint ) i ;
Vector4 b = Unsafe . Add ( ref Unsafe . As < Vector256 < float > , Vector4 > ( ref backgroundBase ) , x ) ;
Vector4 s = Unsafe . Add ( ref Unsafe . As < Vector256 < float > , Vector4 > ( ref sourceBase ) , x ) ;
ref Vector4 r = ref Unsafe . Add ( ref Unsafe . As < Vector256 < float > , Vector4 > ( ref resultBase ) , x ) ;
r = ( b = = s ) ? replacement : s ;
}
PixelOperations < TPixel > . Instance . FromVector4Destructive ( this . configuration , result , resultFrame . DangerousGetPixelRowMemory ( y ) . Span , PixelConversionModifiers . Scale ) ;
}
}
this . WriteImageDescriptor ( frame , useLocal , stream ) ;
private static Rectangle TrimTransparentPixels ( Buffer2D < byte > buffer , int transparencyIndex )
{
if ( transparencyIndex < 0 )
{
return buffer . FullRectangle ( ) ;
}
if ( useLocal )
byte trimmableIndex = unchecked ( ( byte ) transparencyIndex ) ;
int top = int . MinValue ;
int bottom = int . MaxValue ;
int left = int . MaxValue ;
int right = int . MinValue ;
int minY = - 1 ;
bool isTransparentRow = true ;
// Run through the buffer in a single pass. Use variables to track the min/max values.
for ( int y = 0 ; y < buffer . Height ; y + + )
{
isTransparentRow = true ;
Span < byte > rowSpan = buffer . DangerousGetRowSpan ( y ) ;
ref byte rowPtr = ref MemoryMarshal . GetReference ( rowSpan ) ;
nint rowLength = ( nint ) ( uint ) rowSpan . Length ;
nint x = 0 ;
#if NET7_0_OR_GREATER
if ( Vector128 . IsHardwareAccelerated & & rowLength > = Vector128 < byte > . Count )
{
Vector256 < byte > trimmableVec256 = Vector256 . Create ( trimmableIndex ) ;
if ( Vector256 . IsHardwareAccelerated & & rowLength > = Vector256 < byte > . Count )
{
do
{
Vector256 < byte > vec = Vector256 . LoadUnsafe ( ref rowPtr , ( nuint ) x ) ;
Vector256 < byte > notEquals = ~ Vector256 . Equals ( vec , trimmableVec256 ) ;
uint mask = notEquals . ExtractMostSignificantBits ( ) ;
if ( mask ! = 0 )
{
isTransparentRow = false ;
nint start = x + ( nint ) uint . TrailingZeroCount ( mask ) ;
nint end = ( nint ) uint . LeadingZeroCount ( mask ) ;
// end is from the end, but we need the index from the beginning
end = x + Vector256 < byte > . Count - 1 - end ;
left = Math . Min ( left , ( int ) start ) ;
right = Math . Max ( right , ( int ) end ) ;
}
x + = Vector256 < byte > . Count ;
}
while ( x < = rowLength - Vector256 < byte > . Count ) ;
}
Vector128 < byte > trimmableVec = Vector256 . IsHardwareAccelerated
? trimmableVec256 . GetLower ( )
: Vector128 . Create ( trimmableIndex ) ;
while ( x < = rowLength - Vector128 < byte > . Count )
{
Vector128 < byte > vec = Vector128 . LoadUnsafe ( ref rowPtr , ( nuint ) x ) ;
Vector128 < byte > notEquals = ~ Vector128 . Equals ( vec , trimmableVec ) ;
uint mask = notEquals . ExtractMostSignificantBits ( ) ;
if ( mask ! = 0 )
{
isTransparentRow = false ;
nint start = x + ( nint ) uint . TrailingZeroCount ( mask ) ;
nint end = ( nint ) uint . LeadingZeroCount ( mask ) - Vector128 < byte > . Count ;
// end is from the end, but we need the index from the beginning
end = x + Vector128 < byte > . Count - 1 - end ;
left = Math . Min ( left , ( int ) start ) ;
right = Math . Max ( right , ( int ) end ) ;
}
x + = Vector128 < byte > . Count ;
}
}
#else
if ( Sse41 . IsSupported & & rowLength > = Vector128 < byte > . Count )
{
Vector256 < byte > trimmableVec256 = Vector256 . Create ( trimmableIndex ) ;
if ( Avx2 . IsSupported & & rowLength > = Vector256 < byte > . Count )
{
do
{
Vector256 < byte > vec = Unsafe . ReadUnaligned < Vector256 < byte > > ( ref Unsafe . Add ( ref rowPtr , x ) ) ;
Vector256 < byte > notEquals = Avx2 . CompareEqual ( vec , trimmableVec256 ) ;
notEquals = Avx2 . Xor ( notEquals , Vector256 < byte > . AllBitsSet ) ;
int mask = Avx2 . MoveMask ( notEquals ) ;
if ( mask ! = 0 )
{
isTransparentRow = false ;
nint start = x + ( nint ) ( uint ) BitOperations . TrailingZeroCount ( mask ) ;
nint end = ( nint ) ( uint ) BitOperations . LeadingZeroCount ( ( uint ) mask ) ;
// end is from the end, but we need the index from the beginning
end = x + Vector256 < byte > . Count - 1 - end ;
left = Math . Min ( left , ( int ) start ) ;
right = Math . Max ( right , ( int ) end ) ;
}
x + = Vector256 < byte > . Count ;
}
while ( x < = rowLength - Vector256 < byte > . Count ) ;
}
Vector128 < byte > trimmableVec = Sse41 . IsSupported
? trimmableVec256 . GetLower ( )
: Vector128 . Create ( trimmableIndex ) ;
while ( x < = rowLength - Vector128 < byte > . Count )
{
Vector128 < byte > vec = Unsafe . ReadUnaligned < Vector128 < byte > > ( ref Unsafe . Add ( ref rowPtr , x ) ) ;
Vector128 < byte > notEquals = Sse2 . CompareEqual ( vec , trimmableVec ) ;
notEquals = Sse2 . Xor ( notEquals , Vector128 < byte > . AllBitsSet ) ;
int mask = Sse2 . MoveMask ( notEquals ) ;
if ( mask ! = 0 )
{
isTransparentRow = false ;
nint start = x + ( nint ) ( uint ) BitOperations . TrailingZeroCount ( mask ) ;
nint end = ( nint ) ( uint ) BitOperations . LeadingZeroCount ( ( uint ) mask ) - Vector128 < byte > . Count ;
// end is from the end, but we need the index from the beginning
end = x + Vector128 < byte > . Count - 1 - end ;
left = Math . Min ( left , ( int ) start ) ;
right = Math . Max ( right , ( int ) end ) ;
}
x + = Vector128 < byte > . Count ;
}
}
#endif
for ( ; x < rowLength ; + + x )
{
if ( Unsafe . Add ( ref rowPtr , x ) ! = trimmableIndex )
{
isTransparentRow = false ;
left = Math . Min ( left , ( int ) x ) ;
right = Math . Max ( right , ( int ) x ) ;
}
}
if ( ! isTransparentRow )
{
if ( y = = 0 )
{
// First row is opaque.
// Capture to prevent over assignment when a match is found below.
top = 0 ;
}
// The minimum top bounds have already been captured.
// Increment the bottom to include the current opaque row.
if ( minY < 0 & & top ! = 0 )
{
// Increment to the first opaque row.
top + + ;
}
minY = top ;
bottom = y ;
}
else
{
// We've yet to hit an opaque row. Capture the top position.
if ( minY < 0 )
{
top = Math . Max ( top , y ) ;
}
bottom = Math . Min ( bottom , y ) ;
}
}
if ( left = = int . MaxValue )
{
left = 0 ;
}
if ( right = = int . MinValue )
{
this . WriteColorTable ( quantized , stream ) ;
right = buffer . Width ;
}
this . WriteImageData ( quantized , stream ) ;
if ( top = = bottom | | left = = right )
{
// The entire image is transparent.
return buffer . FullRectangle ( ) ;
}
if ( ! isTransparentRow )
{
// Last row is opaque.
bottom = buffer . Height ;
}
return Rectangle . FromLTRB ( left , top , Math . Min ( right + 1 , buffer . Width ) , Math . Min ( bottom + 1 , buffer . Height ) ) ;
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">The quantized frame.</param>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The current gif frame metadata.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private static int GetTransparentIndex < TPixel > ( IndexedImageFrame < TPixel > quantized )
private static int GetTransparentIndex < TPixel > ( IndexedImageFrame < TPixel > ? quantized , GifFrameMetadata ? metadata )
where TPixel : unmanaged , IPixel < TPixel >
{
// Transparent pixels are much more likely to be found at the end of a palette.
int index = - 1 ;
ReadOnlySpan < TPixel > paletteSpan = quantized . Palette . Span ;
using IMemoryOwner < Rgba32 > rgbaOwner = quantized . Configuration . MemoryAllocator . Allocate < Rgba32 > ( paletteSpan . Length ) ;
Span < Rgba32 > rgbaSpan = rgbaOwner . GetSpan ( ) ;
PixelOperations < TPixel > . Instance . ToRgba32 ( quantized . Configuration , paletteSpan , rgbaSpan ) ;
ref Rgba32 rgbaSpanRef = ref MemoryMarshal . GetReference ( rgbaSpan ) ;
if ( metadata ? . HasTransparency = = true )
{
return metadata . TransparencyIndex ;
}
for ( int i = rgbaSpan . Length - 1 ; i > = 0 ; i - - )
int index = - 1 ;
if ( quantized ! = null )
{
if ( Unsafe . Add ( ref rgbaSpanRef , ( uint ) i ) . Equals ( default ) )
TPixel transparentPixel = default ;
transparentPixel . FromScaledVector4 ( Vector4 . Zero ) ;
ReadOnlySpan < TPixel > palette = quantized . Palette . Span ;
// Transparent pixels are much more likely to be found at the end of a palette.
for ( int i = palette . Length - 1 ; i > = 0 ; i - - )
{
index = i ;
if ( palette [ i ] . Equals ( transparentPixel ) )
{
index = i ;
}
}
}
@ -271,18 +659,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The image metadata.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
/// <param name="backgroundIndex">The index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor (
ImageMetadata metadata ,
int width ,
int height ,
int transparency Index,
byte background Index,
bool useGlobalTable ,
int bitDepth ,
Stream stream )
{
byte packedValue = GifLogicalScreenDescriptor . GetPackedValue ( useGlobalTable , this . bitDepth - 1 , false , this . bitDepth - 1 ) ;
byte packedValue = GifLogicalScreenDescriptor . GetPackedValue ( useGlobalTable , bitDepth - 1 , false , bitDepth - 1 ) ;
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@ -316,7 +706,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
width : ( ushort ) width ,
height : ( ushort ) height ,
packed : packedValue ,
backgroundColorIndex : unchecked ( ( byte ) transparencyIndex ) ,
backgroundColorIndex : backgroundIndex ,
ratio ) ;
Span < byte > buffer = stackalloc byte [ 2 0 ] ;
@ -412,16 +802,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension ( GifFrameMetadata metadata , int transparencyIndex , Stream stream )
private void WriteGraphicalControlExtension ( GifFrameMetadata ? metadata , int transparencyIndex , Stream stream )
{
GifFrameMetadata ? data = metadata ;
bool hasTransparency ;
if ( metadata is null )
{
data = new ( ) ;
hasTransparency = transparencyIndex > = 0 ;
}
else
{
hasTransparency = metadata . HasTransparency ;
}
byte packedValue = GifGraphicControlExtension . GetPackedValue (
disposalMethod : metadata . DisposalMethod ,
transparencyFlag : transparencyIndex > - 1 ) ;
disposalMethod : data ! . DisposalMethod ,
transparencyFlag : hasTransparency ) ;
GifGraphicControlExtension extension = new (
packed : packedValue ,
delayTime : ( ushort ) metadata . FrameDelay ,
transparencyIndex : unchecked ( ( byte ) transparencyIndex ) ) ;
delayTime : ( ushort ) data . FrameDelay ,
transparencyIndex : hasTransparency ? unchecked ( ( byte ) transparencyIndex ) : byte . MinValue ) ;
this . WriteExtension ( extension , stream ) ;
}
@ -443,7 +845,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
IMemoryOwner < byte > ? owner = null ;
Span < byte > extensionBuffer = stackalloc byte [ 0 ] ; // workaround compiler limitation
Span < byte > extensionBuffer = stackalloc byte [ 0 ] ; // workaround compiler limitation
if ( extensionSize > 1 2 8 )
{
owner = this . memoryAllocator . Allocate < byte > ( extensionSize + 3 ) ;
@ -466,26 +868,25 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
}
/// <summary>
/// Writes the image descriptor to the stream.
/// Writes the image frame descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="rectangle">The frame location and size.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor < TPixel > ( ImageFrame < TPixel > image , bool hasColorTable , Stream stream )
where TPixel : unmanaged , IPixel < TPixel >
private void WriteImageDescriptor ( Rectangle rectangle , bool hasColorTable , int bitDepth , Stream stream )
{
byte packedValue = GifImageDescriptor . GetPackedValue (
localColorTableFlag : hasColorTable ,
interfaceFlag : false ,
sortFlag : false ,
localColorTableSize : this . bitDepth - 1 ) ;
localColorTableSize : bitDepth - 1 ) ;
GifImageDescriptor descriptor = new (
left : 0 ,
top : 0 ,
width : ( ushort ) imag e. Width ,
height : ( ushort ) imag e. Height ,
left : ( ushort ) rectangle . X ,
top : ( ushort ) rectangle . Y ,
width : ( ushort ) rectangl e. Width ,
height : ( ushort ) rectangl e. Height ,
packed : packedValue ) ;
Span < byte > buffer = stackalloc byte [ 2 0 ] ;
@ -499,12 +900,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="bitDepth">The bit depth of the color palette.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteColorTable < TPixel > ( IndexedImageFrame < TPixel > image , Stream stream )
private void WriteColorTable < TPixel > ( IndexedImageFrame < TPixel > image , int bitDepth , Stream stream )
where TPixel : unmanaged , IPixel < TPixel >
{
// The maximum number of colors for the bit depth
int colorTableLength = ColorNumerics . GetColorCountForBitDepth ( this . bitDepth ) * Unsafe . SizeOf < Rgb24 > ( ) ;
int colorTableLength = ColorNumerics . GetColorCountForBitDepth ( bitDepth ) * Unsafe . SizeOf < Rgb24 > ( ) ;
using IMemoryOwner < byte > colorTable = this . memoryAllocator . Allocate < byte > ( colorTableLength , AllocationOptions . Clean ) ;
Span < byte > colorTableSpan = colorTable . GetSpan ( ) ;
@ -521,13 +923,23 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</type param>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels .</param>
/// <param name="indices">The <see cref="Buffer2DRegion{Byte}"/> containing indexed pixels.</ param>
/// <param name="interest">The region of interest .</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData < TPixel > ( IndexedImageFrame < TPixel > image , Stream stream )
where TPixel : unmanaged , IPixel < TPixel >
/// <param name="paletteLength">The length of the frame color palette.</param>
/// <param name="transparencyIndex">The index of the color used to represent transparency.</param>
private void WriteImageData ( Buffer2D < byte > indices , Rectangle interest , Stream stream , int paletteLength , int transparencyIndex )
{
using LzwEncoder encoder = new ( this . memoryAllocator , ( byte ) this . bitDepth ) ;
encoder . Encode ( ( ( IPixelSource ) image ) . PixelBuffer , stream ) ;
Buffer2DRegion < byte > region = indices . GetRegion ( interest ) ;
// Pad the bit depth when required for encoding the image data.
// This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette
// as decoders skip indexes that are out of range.
int padding = transparencyIndex > = paletteLength
? 1
: 0 ;
using LzwEncoder encoder = new ( this . memoryAllocator , ColorNumerics . GetBitsNeededForColorDepth ( paletteLength + padding ) ) ;
encoder . Encode ( region , stream ) ;
}
}