Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize Animation Encoders #2842

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/FormatConnectingMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class FormatConnectingMetadata
/// Gets the default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Color.Transparent"/>.
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Gif/GifEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public sealed class GifEncoder : QuantizingImageEncoder
public sealed class GifEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Gets the color table mode: Global or local.
Expand Down
100 changes: 89 additions & 11 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;

/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;

/// <summary>
/// The number of times any animation is repeated.
/// </summary>
private readonly ushort? repeatCount;

/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
Expand All @@ -68,6 +81,8 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder)
this.hasQuantizer = encoder.Quantizer is not null;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
}

/// <summary>
Expand Down Expand Up @@ -141,9 +156,12 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}

byte backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}

// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
Expand All @@ -161,15 +179,21 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken

// Write application extensions.
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
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();

this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
this.EncodeAdditionalFrames(
stream,
image,
globalPalette,
derivedTransparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);

stream.WriteByte(GifConstants.EndIntroducer);

Expand All @@ -194,7 +218,8 @@ private void EncodeAdditionalFrames<TPixel>(
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode)
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
Expand All @@ -213,6 +238,16 @@ private void EncodeAdditionalFrames<TPixel>(

for (int i = 1; i < image.Frames.Count; i++)
{
if (cancellationToken.IsCancellationRequested)
{
if (hasPaletteQuantizer)
{
paletteQuantizer.Dispose();
}

return;
}

// 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;
Expand Down Expand Up @@ -291,6 +326,10 @@ private void EncodeAdditionalFrame<TPixel>(

ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ? null : previousFrame;

Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;

// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
Expand All @@ -299,7 +338,7 @@ private void EncodeAdditionalFrame<TPixel>(
currentFrame,
nextFrame,
encodingFrame,
Color.Transparent,
background,
true);

using IndexedImageFrame<TPixel> quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
Expand Down Expand Up @@ -428,14 +467,12 @@ private IndexedImageFrame<TPixel> QuantizeAdditionalFrameAndUpdateMetadata<TPixe
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);

/// <summary>
/// Returns the index of the most transparent color in the palette.
/// Returns the index of the transparent color in the palette.
/// </summary>
/// <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>
/// <returns>The <see cref="int"/>.</returns>
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand Down Expand Up @@ -463,6 +500,47 @@ private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quanti
return index;
}

/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="background">The background color to match.</param>
/// <param name="index">The index in the palette of the background color.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="bool"/>.</returns>
private static bool TryGetBackgroundIndex<TPixel>(
IndexedImageFrame<TPixel>? quantized,
Color? background,
out byte index)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null && background.HasValue)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}

match = i;
break;
}
}

if (match >= 0)
{
index = (byte)Numerics.Clamp(match, 0, 255);
return true;
}

index = 0;
return false;
}

/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions src/ImageSharp/Formats/IAnimatedImageEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Defines the contract for all image encoders that allow encoding animation sequences.
/// </summary>
public interface IAnimatedImageEncoder
{
/// <summary>
/// Gets the default background color of the canvas when animating in supported encoders.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
Color? BackgroundColor { get; }

/// <summary>
/// Gets the number of times any animation is repeated in supported encoders.
/// </summary>
ushort? RepeatCount { get; }

/// <summary>
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
/// </summary>
bool? AnimateRootFrame { get; }
}

/// <summary>
/// Acts as a base class for all image encoders that allow encoding animation sequences.
/// </summary>
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; init; }

/// <inheritdoc/>
public ushort? RepeatCount { get; init; }

/// <inheritdoc/>
public bool? AnimateRootFrame { get; init; } = true;
}
50 changes: 50 additions & 0 deletions src/ImageSharp/Formats/IQuantizingImageEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Processing.Processors.Quantization;

namespace SixLabors.ImageSharp.Formats;

/// <summary>
/// Defines the contract for all image encoders that allow color palette generation via quantization.
/// </summary>
public interface IQuantizingImageEncoder
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer? Quantizer { get; }

/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; }
}

/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
{
/// <inheritdoc/>
public IQuantizer? Quantizer { get; init; }

/// <inheritdoc/>
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
}

/// <summary>
/// Acts as a base class for all image encoders that allow color palette generation via quantization when
/// encoding animation sequences.
/// </summary>
public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
{
/// <inheritdoc/>
public Color? BackgroundColor { get; }

/// <inheritdoc/>
public ushort? RepeatCount { get; }

/// <inheritdoc/>
public bool? AnimateRootFrame { get; }
}
3 changes: 1 addition & 2 deletions src/ImageSharp/Formats/Png/PngEncoder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable

using SixLabors.ImageSharp.Processing.Processors.Quantization;

Expand All @@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public class PngEncoder : QuantizingImageEncoder
public class PngEncoder : QuantizingAnimatedImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
Expand Down
Loading
Loading