Browse Source

Simplify GifEncoderCore, seal palette quantizers

pull/2894/head
James Jackson-South 10 months ago
parent
commit
b989843f18
  1. 216
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 2
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  3. 2
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs

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

@ -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;

2
src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// <summary>
/// A palette quantizer consisting of web safe colors as defined in the CSS Color Module Level 4.
/// </summary>
public class WebSafePaletteQuantizer : PaletteQuantizer
public sealed class WebSafePaletteQuantizer : PaletteQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.

2
src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs

@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
/// A palette quantizer consisting of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821.
/// The hex codes were collected and defined by Nicholas Rougeux <see href="https://www.c82.net/werner"/>
/// </summary>
public class WernerPaletteQuantizer : PaletteQuantizer
public sealed class WernerPaletteQuantizer : PaletteQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.

Loading…
Cancel
Save