|
|
|
@ -9,7 +9,6 @@ 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; |
|
|
|
@ -19,6 +18,8 @@ namespace SixLabors.ImageSharp.Formats.Gif; |
|
|
|
/// </summary>
|
|
|
|
internal sealed class GifEncoderCore |
|
|
|
{ |
|
|
|
private readonly GifEncoder encoder; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Used for allocating memory during processing operations.
|
|
|
|
/// </summary>
|
|
|
|
@ -34,21 +35,6 @@ internal sealed class GifEncoderCore |
|
|
|
/// </summary>
|
|
|
|
private readonly bool skipMetadata; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The quantizer used to generate the color palette.
|
|
|
|
/// </summary>
|
|
|
|
private IQuantizer? quantizer; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The fallback quantizer to use when no quantizer is provided.
|
|
|
|
/// </summary>
|
|
|
|
private static readonly IQuantizer FallbackQuantizer = KnownQuantizers.Octree; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Whether the quantizer was supplied via options.
|
|
|
|
/// </summary>
|
|
|
|
private readonly bool hasQuantizer; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The color table mode: Global or local.
|
|
|
|
/// </summary>
|
|
|
|
@ -86,9 +72,8 @@ internal sealed class GifEncoderCore |
|
|
|
{ |
|
|
|
this.configuration = configuration; |
|
|
|
this.memoryAllocator = configuration.MemoryAllocator; |
|
|
|
this.encoder = encoder; |
|
|
|
this.skipMetadata = encoder.SkipMetadata; |
|
|
|
this.quantizer = encoder.Quantizer; |
|
|
|
this.hasQuantizer = encoder.Quantizer is not null; |
|
|
|
this.colorTableMode = encoder.ColorTableMode; |
|
|
|
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; |
|
|
|
this.backgroundColor = encoder.BackgroundColor; |
|
|
|
@ -124,64 +109,64 @@ internal sealed class GifEncoderCore |
|
|
|
|
|
|
|
// Quantize the first image frame returning a palette.
|
|
|
|
IndexedImageFrame<TPixel>? quantized = null; |
|
|
|
if (this.quantizer is null) |
|
|
|
IQuantizer? globalQuantizer = this.encoder.Quantizer; |
|
|
|
TransparentColorMode mode = this.transparentColorMode; |
|
|
|
|
|
|
|
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
|
|
|
|
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new()).DeepClone(o => o.TransparentColorMode = mode); |
|
|
|
|
|
|
|
if (globalQuantizer is null) |
|
|
|
{ |
|
|
|
// Is this a gif with color information. If so use that, otherwise use octree.
|
|
|
|
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) |
|
|
|
{ |
|
|
|
// We avoid dithering by default to preserve the original colors.
|
|
|
|
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); |
|
|
|
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256) |
|
|
|
{ |
|
|
|
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }); |
|
|
|
// We avoid dithering by default to preserve the original colors.
|
|
|
|
globalQuantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, options.DeepClone(o => o.Dither = null)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.quantizer = FallbackQuantizer; |
|
|
|
globalQuantizer = new OctreeQuantizer(options); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.quantizer = FallbackQuantizer; |
|
|
|
globalQuantizer = new OctreeQuantizer(options); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
|
|
|
|
TransparentColorMode mode = this.transparentColorMode; |
|
|
|
QuantizerOptions options = this.quantizer.Options.DeepClone(o => o.TransparentColorMode = mode); |
|
|
|
|
|
|
|
// Quantize the first frame.
|
|
|
|
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options)) |
|
|
|
{ |
|
|
|
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; |
|
|
|
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy; |
|
|
|
|
|
|
|
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame; |
|
|
|
if (useGlobalTableForFirstFrame) |
|
|
|
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame; |
|
|
|
if (useGlobalTableForFirstFrame) |
|
|
|
{ |
|
|
|
using IQuantizer<TPixel> firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options); |
|
|
|
if (useGlobalTable) |
|
|
|
{ |
|
|
|
if (useGlobalTable) |
|
|
|
{ |
|
|
|
frameQuantizer.BuildPalette(strategy, image); |
|
|
|
quantized = frameQuantizer.QuantizeFrame(encodingFrame, image.Bounds); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
frameQuantizer.BuildPalette(strategy, encodingFrame); |
|
|
|
quantized = frameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); |
|
|
|
} |
|
|
|
firstFrameQuantizer.BuildPalette(strategy, image); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( |
|
|
|
encodingFrame, |
|
|
|
options, |
|
|
|
encodingFrame.Bounds, |
|
|
|
frameMetadata, |
|
|
|
true, |
|
|
|
default, |
|
|
|
false, |
|
|
|
frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1, |
|
|
|
Color.Transparent); |
|
|
|
firstFrameQuantizer.BuildPalette(strategy, encodingFrame); |
|
|
|
} |
|
|
|
|
|
|
|
quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
quantized = this.QuantizeFrameAndUpdateMetadata( |
|
|
|
encodingFrame, |
|
|
|
globalQuantizer, |
|
|
|
default, |
|
|
|
encodingFrame.Bounds, |
|
|
|
frameMetadata, |
|
|
|
true, |
|
|
|
false, |
|
|
|
frameMetadata.HasTransparency ? frameMetadata.TransparencyIndex : -1, |
|
|
|
Color.Transparent); |
|
|
|
} |
|
|
|
|
|
|
|
// Write the header.
|
|
|
|
@ -231,14 +216,18 @@ internal sealed class GifEncoderCore |
|
|
|
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
|
|
|
|
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray(); |
|
|
|
|
|
|
|
this.EncodeAdditionalFrames( |
|
|
|
stream, |
|
|
|
image, |
|
|
|
options, |
|
|
|
globalPalette, |
|
|
|
derivedTransparencyIndex, |
|
|
|
frameMetadata.DisposalMode, |
|
|
|
cancellationToken); |
|
|
|
if (image.Frames.Count > 1) |
|
|
|
{ |
|
|
|
using PaletteQuantizer<TPixel> globalFrameQuantizer = new(this.configuration, globalQuantizer.Options, quantized.Palette.ToArray()); |
|
|
|
this.EncodeAdditionalFrames( |
|
|
|
stream, |
|
|
|
image, |
|
|
|
globalQuantizer, |
|
|
|
globalFrameQuantizer, |
|
|
|
derivedTransparencyIndex, |
|
|
|
frameMetadata.DisposalMode, |
|
|
|
cancellationToken); |
|
|
|
} |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
@ -264,70 +253,43 @@ internal sealed class GifEncoderCore |
|
|
|
private void EncodeAdditionalFrames<TPixel>( |
|
|
|
Stream stream, |
|
|
|
Image<TPixel> image, |
|
|
|
QuantizerOptions options, |
|
|
|
ReadOnlyMemory<TPixel> globalPalette, |
|
|
|
IQuantizer globalQuantizer, |
|
|
|
PaletteQuantizer<TPixel> globalFrameQuantizer, |
|
|
|
int globalTransparencyIndex, |
|
|
|
FrameDisposalMode previousDisposalMode, |
|
|
|
CancellationToken cancellationToken) |
|
|
|
where TPixel : unmanaged, IPixel<TPixel> |
|
|
|
{ |
|
|
|
if (image.Frames.Count == 1) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
PaletteQuantizer<TPixel> globalPaletteQuantizer = default; |
|
|
|
bool hasGlobalPaletteQuantizer = false; |
|
|
|
|
|
|
|
// 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.
|
|
|
|
using ImageFrame<TPixel> encodingFrame = new(previousFrame.Configuration, previousFrame.Size); |
|
|
|
|
|
|
|
try |
|
|
|
for (int i = 1; i < image.Frames.Count; i++) |
|
|
|
{ |
|
|
|
for (int i = 1; i < image.Frames.Count; i++) |
|
|
|
{ |
|
|
|
cancellationToken.ThrowIfCancellationRequested(); |
|
|
|
cancellationToken.ThrowIfCancellationRequested(); |
|
|
|
|
|
|
|
// Gather the metadata for this frame.
|
|
|
|
ImageFrame<TPixel> currentFrame = image.Frames[i]; |
|
|
|
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; |
|
|
|
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); |
|
|
|
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); |
|
|
|
// Gather the metadata for this frame.
|
|
|
|
ImageFrame<TPixel> currentFrame = image.Frames[i]; |
|
|
|
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null; |
|
|
|
GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex); |
|
|
|
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (gifMetadata.ColorTableMode == FrameColorTableMode.Local); |
|
|
|
|
|
|
|
if (!useLocal && !hasGlobalPaletteQuantizer && 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.
|
|
|
|
globalPaletteQuantizer = new(this.configuration, options, globalPalette); |
|
|
|
hasGlobalPaletteQuantizer = true; |
|
|
|
} |
|
|
|
this.EncodeAdditionalFrame( |
|
|
|
stream, |
|
|
|
previousFrame, |
|
|
|
currentFrame, |
|
|
|
nextFrame, |
|
|
|
encodingFrame, |
|
|
|
globalQuantizer, |
|
|
|
globalFrameQuantizer, |
|
|
|
useLocal, |
|
|
|
gifMetadata, |
|
|
|
previousDisposalMode); |
|
|
|
|
|
|
|
this.EncodeAdditionalFrame( |
|
|
|
stream, |
|
|
|
previousFrame, |
|
|
|
currentFrame, |
|
|
|
nextFrame, |
|
|
|
encodingFrame, |
|
|
|
options, |
|
|
|
useLocal, |
|
|
|
gifMetadata, |
|
|
|
globalPaletteQuantizer, |
|
|
|
previousDisposalMode); |
|
|
|
|
|
|
|
previousFrame = currentFrame; |
|
|
|
previousDisposalMode = gifMetadata.DisposalMode; |
|
|
|
} |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
if (hasGlobalPaletteQuantizer) |
|
|
|
{ |
|
|
|
globalPaletteQuantizer.Dispose(); |
|
|
|
} |
|
|
|
previousFrame = currentFrame; |
|
|
|
previousDisposalMode = gifMetadata.DisposalMode; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -363,10 +325,10 @@ internal sealed class GifEncoderCore |
|
|
|
ImageFrame<TPixel> currentFrame, |
|
|
|
ImageFrame<TPixel>? nextFrame, |
|
|
|
ImageFrame<TPixel> encodingFrame, |
|
|
|
QuantizerOptions options, |
|
|
|
IQuantizer globalQuantizer, |
|
|
|
PaletteQuantizer<TPixel> globalFrameQuantizer, |
|
|
|
bool useLocal, |
|
|
|
GifFrameMetadata metadata, |
|
|
|
PaletteQuantizer<TPixel> globalPaletteQuantizer, |
|
|
|
FrameDisposalMode previousDisposalMode) |
|
|
|
where TPixel : unmanaged, IPixel<TPixel> |
|
|
|
{ |
|
|
|
@ -393,13 +355,13 @@ internal sealed class GifEncoderCore |
|
|
|
background, |
|
|
|
true); |
|
|
|
|
|
|
|
using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata( |
|
|
|
using IndexedImageFrame<TPixel> quantized = this.QuantizeFrameAndUpdateMetadata( |
|
|
|
encodingFrame, |
|
|
|
options, |
|
|
|
globalQuantizer, |
|
|
|
globalFrameQuantizer, |
|
|
|
bounds, |
|
|
|
metadata, |
|
|
|
useLocal, |
|
|
|
globalPaletteQuantizer, |
|
|
|
difference, |
|
|
|
transparencyIndex, |
|
|
|
background); |
|
|
|
@ -418,13 +380,13 @@ internal sealed class GifEncoderCore |
|
|
|
this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex); |
|
|
|
} |
|
|
|
|
|
|
|
private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixel>( |
|
|
|
private IndexedImageFrame<TPixel> QuantizeFrameAndUpdateMetadata<TPixel>( |
|
|
|
ImageFrame<TPixel> encodingFrame, |
|
|
|
QuantizerOptions options, |
|
|
|
IQuantizer globalQuantizer, |
|
|
|
PaletteQuantizer<TPixel> globalFrameQuantizer, |
|
|
|
Rectangle bounds, |
|
|
|
GifFrameMetadata metadata, |
|
|
|
bool useLocal, |
|
|
|
PaletteQuantizer<TPixel> globalPaletteQuantizer, |
|
|
|
bool hasDuplicates, |
|
|
|
int transparencyIndex, |
|
|
|
Color transparentColor) |
|
|
|
@ -451,20 +413,19 @@ internal sealed class GifEncoderCore |
|
|
|
transparencyIndex = palette.Length; |
|
|
|
metadata.TransparencyIndex = ClampIndex(transparencyIndex); |
|
|
|
|
|
|
|
QuantizerOptions paletteOptions = options.DeepClone(o => |
|
|
|
QuantizerOptions options = globalQuantizer.Options.DeepClone(o => |
|
|
|
{ |
|
|
|
o.MaxColors = palette.Length; |
|
|
|
o.Dither = null; |
|
|
|
}); |
|
|
|
PaletteQuantizer quantizer = new(palette, paletteOptions, transparencyIndex, transparentColor); |
|
|
|
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, quantizer.Options); |
|
|
|
PaletteQuantizer quantizer = new(palette, options, transparencyIndex, transparentColor); |
|
|
|
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration); |
|
|
|
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// We must quantize the frame to generate a local color table.
|
|
|
|
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; |
|
|
|
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options); |
|
|
|
using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration); |
|
|
|
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); |
|
|
|
|
|
|
|
// The transparency index derived by the quantizer will differ from the index
|
|
|
|
@ -476,7 +437,7 @@ internal sealed class GifEncoderCore |
|
|
|
else |
|
|
|
{ |
|
|
|
// Just use the local palette.
|
|
|
|
QuantizerOptions paletteOptions = options.DeepClone(o => |
|
|
|
QuantizerOptions paletteOptions = globalQuantizer.Options.DeepClone(o => |
|
|
|
{ |
|
|
|
o.MaxColors = palette.Length; |
|
|
|
o.Dither = null; |
|
|
|
@ -489,8 +450,7 @@ internal sealed class GifEncoderCore |
|
|
|
else |
|
|
|
{ |
|
|
|
// We must quantize the frame to generate a local color table.
|
|
|
|
IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : FallbackQuantizer; |
|
|
|
using IQuantizer<TPixel> frameQuantizer = quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options); |
|
|
|
using IQuantizer<TPixel> frameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration); |
|
|
|
quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds); |
|
|
|
|
|
|
|
// The transparency index derived by the quantizer might differ from the index
|
|
|
|
@ -520,12 +480,12 @@ internal sealed class GifEncoderCore |
|
|
|
if (hasDuplicates && !metadata.HasTransparency) |
|
|
|
{ |
|
|
|
metadata.HasTransparency = true; |
|
|
|
transparencyIndex = globalPaletteQuantizer.Palette.Length; |
|
|
|
transparencyIndex = globalFrameQuantizer.Palette.Length; |
|
|
|
metadata.TransparencyIndex = ClampIndex(transparencyIndex); |
|
|
|
} |
|
|
|
|
|
|
|
globalPaletteQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>()); |
|
|
|
quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds); |
|
|
|
globalFrameQuantizer.SetTransparencyIndex(transparencyIndex, transparentColor.ToPixel<TPixel>()); |
|
|
|
quantized = globalFrameQuantizer.QuantizeFrame(encodingFrame, bounds); |
|
|
|
} |
|
|
|
|
|
|
|
return quantized; |
|
|
|
|