Skip to content

Commit c873e91

Browse files
Merge pull request #2842 from SixLabors/js/normalize-animation-encoder
Normalize Animation Encoders
2 parents 1ec479e + f492359 commit c873e91

26 files changed

+399
-191
lines changed

src/ImageSharp/Formats/FormatConnectingMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class FormatConnectingMetadata
4545
/// Gets the default background color of the canvas when animating.
4646
/// This color may be used to fill the unused space on the canvas around the frames,
4747
/// as well as the transparent pixels of the first frame.
48-
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
48+
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
4949
/// </summary>
5050
/// <remarks>
5151
/// Defaults to <see cref="Color.Transparent"/>.

src/ImageSharp/Formats/Gif/GifEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
66
/// <summary>
77
/// Image encoder for writing image data to a stream in gif format.
88
/// </summary>
9-
public sealed class GifEncoder : QuantizingImageEncoder
9+
public sealed class GifEncoder : QuantizingAnimatedImageEncoder
1010
{
1111
/// <summary>
1212
/// Gets the color table mode: Global or local.

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ internal sealed class GifEncoderCore
5454
/// </summary>
5555
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
5656

57+
/// <summary>
58+
/// The default background color of the canvas when animating.
59+
/// This color may be used to fill the unused space on the canvas around the frames,
60+
/// as well as the transparent pixels of the first frame.
61+
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
62+
/// </summary>
63+
private readonly Color? backgroundColor;
64+
65+
/// <summary>
66+
/// The number of times any animation is repeated.
67+
/// </summary>
68+
private readonly ushort? repeatCount;
69+
5770
/// <summary>
5871
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
5972
/// </summary>
@@ -68,6 +81,8 @@ public GifEncoderCore(Configuration configuration, GifEncoder encoder)
6881
this.hasQuantizer = encoder.Quantizer is not null;
6982
this.colorTableMode = encoder.ColorTableMode;
7083
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
84+
this.backgroundColor = encoder.BackgroundColor;
85+
this.repeatCount = encoder.RepeatCount;
7186
}
7287

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

144-
byte backgroundIndex = derivedTransparencyIndex >= 0
145-
? frameMetadata.TransparencyIndex
146-
: gifMetadata.BackgroundColorIndex;
159+
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
160+
{
161+
backgroundIndex = derivedTransparencyIndex >= 0
162+
? frameMetadata.TransparencyIndex
163+
: gifMetadata.BackgroundColorIndex;
164+
}
147165

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

162180
// Write application extensions.
163181
XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
164-
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
182+
this.WriteApplicationExtensions(stream, image.Frames.Count, this.repeatCount ?? gifMetadata.RepeatCount, xmpProfile);
165183
}
166184

167185
this.EncodeFirstFrame(stream, frameMetadata, quantized);
168186

169187
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
170188
TPixel[] globalPalette = image.Frames.Count == 1 ? [] : quantized.Palette.ToArray();
171189

172-
this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMode);
190+
this.EncodeAdditionalFrames(
191+
stream,
192+
image,
193+
globalPalette,
194+
derivedTransparencyIndex,
195+
frameMetadata.DisposalMode,
196+
cancellationToken);
173197

174198
stream.WriteByte(GifConstants.EndIntroducer);
175199

@@ -194,7 +218,8 @@ private void EncodeAdditionalFrames<TPixel>(
194218
Image<TPixel> image,
195219
ReadOnlyMemory<TPixel> globalPalette,
196220
int globalTransparencyIndex,
197-
FrameDisposalMode previousDisposalMode)
221+
FrameDisposalMode previousDisposalMode,
222+
CancellationToken cancellationToken)
198223
where TPixel : unmanaged, IPixel<TPixel>
199224
{
200225
if (image.Frames.Count == 1)
@@ -213,6 +238,16 @@ private void EncodeAdditionalFrames<TPixel>(
213238

214239
for (int i = 1; i < image.Frames.Count; i++)
215240
{
241+
if (cancellationToken.IsCancellationRequested)
242+
{
243+
if (hasPaletteQuantizer)
244+
{
245+
paletteQuantizer.Dispose();
246+
}
247+
248+
return;
249+
}
250+
216251
// Gather the metadata for this frame.
217252
ImageFrame<TPixel> currentFrame = image.Frames[i];
218253
ImageFrame<TPixel>? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
@@ -291,6 +326,10 @@ private void EncodeAdditionalFrame<TPixel>(
291326

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

329+
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
330+
? this.backgroundColor ?? Color.Transparent
331+
: Color.Transparent;
332+
294333
// Deduplicate and quantize the frame capturing only required parts.
295334
(bool difference, Rectangle bounds) =
296335
AnimationUtilities.DeDuplicatePixels(
@@ -299,7 +338,7 @@ private void EncodeAdditionalFrame<TPixel>(
299338
currentFrame,
300339
nextFrame,
301340
encodingFrame,
302-
Color.Transparent,
341+
background,
303342
true);
304343

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

430469
/// <summary>
431-
/// Returns the index of the most transparent color in the palette.
470+
/// Returns the index of the transparent color in the palette.
432471
/// </summary>
433472
/// <param name="quantized">The current quantized frame.</param>
434473
/// <param name="metadata">The current gif frame metadata.</param>
435474
/// <typeparam name="TPixel">The pixel format.</typeparam>
436-
/// <returns>
437-
/// The <see cref="int"/>.
438-
/// </returns>
475+
/// <returns>The <see cref="int"/>.</returns>
439476
private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifFrameMetadata? metadata)
440477
where TPixel : unmanaged, IPixel<TPixel>
441478
{
@@ -463,6 +500,47 @@ private static int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel>? quanti
463500
return index;
464501
}
465502

503+
/// <summary>
504+
/// Returns the index of the background color in the palette.
505+
/// </summary>
506+
/// <param name="quantized">The current quantized frame.</param>
507+
/// <param name="background">The background color to match.</param>
508+
/// <param name="index">The index in the palette of the background color.</param>
509+
/// <typeparam name="TPixel">The pixel format.</typeparam>
510+
/// <returns>The <see cref="bool"/>.</returns>
511+
private static bool TryGetBackgroundIndex<TPixel>(
512+
IndexedImageFrame<TPixel>? quantized,
513+
Color? background,
514+
out byte index)
515+
where TPixel : unmanaged, IPixel<TPixel>
516+
{
517+
int match = -1;
518+
if (quantized != null && background.HasValue)
519+
{
520+
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
521+
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
522+
for (int i = 0; i < palette.Length; i++)
523+
{
524+
if (!backgroundPixel.Equals(palette[i]))
525+
{
526+
continue;
527+
}
528+
529+
match = i;
530+
break;
531+
}
532+
}
533+
534+
if (match >= 0)
535+
{
536+
index = (byte)Numerics.Clamp(match, 0, 255);
537+
return true;
538+
}
539+
540+
index = 0;
541+
return false;
542+
}
543+
466544
/// <summary>
467545
/// Writes the file header signature and version to the stream.
468546
/// </summary>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats;
5+
6+
/// <summary>
7+
/// Defines the contract for all image encoders that allow encoding animation sequences.
8+
/// </summary>
9+
public interface IAnimatedImageEncoder
10+
{
11+
/// <summary>
12+
/// Gets the default background color of the canvas when animating in supported encoders.
13+
/// This color may be used to fill the unused space on the canvas around the frames,
14+
/// as well as the transparent pixels of the first frame.
15+
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
16+
/// </summary>
17+
Color? BackgroundColor { get; }
18+
19+
/// <summary>
20+
/// Gets the number of times any animation is repeated in supported encoders.
21+
/// </summary>
22+
ushort? RepeatCount { get; }
23+
24+
/// <summary>
25+
/// Gets a value indicating whether the root frame is shown as part of the animated sequence in supported encoders.
26+
/// </summary>
27+
bool? AnimateRootFrame { get; }
28+
}
29+
30+
/// <summary>
31+
/// Acts as a base class for all image encoders that allow encoding animation sequences.
32+
/// </summary>
33+
public abstract class AnimatedImageEncoder : ImageEncoder, IAnimatedImageEncoder
34+
{
35+
/// <inheritdoc/>
36+
public Color? BackgroundColor { get; init; }
37+
38+
/// <inheritdoc/>
39+
public ushort? RepeatCount { get; init; }
40+
41+
/// <inheritdoc/>
42+
public bool? AnimateRootFrame { get; init; } = true;
43+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Processing.Processors.Quantization;
5+
6+
namespace SixLabors.ImageSharp.Formats;
7+
8+
/// <summary>
9+
/// Defines the contract for all image encoders that allow color palette generation via quantization.
10+
/// </summary>
11+
public interface IQuantizingImageEncoder
12+
{
13+
/// <summary>
14+
/// Gets the quantizer used to generate the color palette.
15+
/// </summary>
16+
IQuantizer? Quantizer { get; }
17+
18+
/// <summary>
19+
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
20+
/// </summary>
21+
IPixelSamplingStrategy PixelSamplingStrategy { get; }
22+
}
23+
24+
/// <summary>
25+
/// Acts as a base class for all image encoders that allow color palette generation via quantization.
26+
/// </summary>
27+
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingImageEncoder
28+
{
29+
/// <inheritdoc/>
30+
public IQuantizer? Quantizer { get; init; }
31+
32+
/// <inheritdoc/>
33+
public IPixelSamplingStrategy PixelSamplingStrategy { get; init; } = new DefaultPixelSamplingStrategy();
34+
}
35+
36+
/// <summary>
37+
/// Acts as a base class for all image encoders that allow color palette generation via quantization when
38+
/// encoding animation sequences.
39+
/// </summary>
40+
public abstract class QuantizingAnimatedImageEncoder : QuantizingImageEncoder, IAnimatedImageEncoder
41+
{
42+
/// <inheritdoc/>
43+
public Color? BackgroundColor { get; }
44+
45+
/// <inheritdoc/>
46+
public ushort? RepeatCount { get; }
47+
48+
/// <inheritdoc/>
49+
public bool? AnimateRootFrame { get; }
50+
}

src/ImageSharp/Formats/Png/PngEncoder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
3-
#nullable disable
43

54
using SixLabors.ImageSharp.Processing.Processors.Quantization;
65

@@ -9,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
98
/// <summary>
109
/// Image encoder for writing image data to a stream in png format.
1110
/// </summary>
12-
public class PngEncoder : QuantizingImageEncoder
11+
public class PngEncoder : QuantizingAnimatedImageEncoder
1312
{
1413
/// <summary>
1514
/// Initializes a new instance of the <see cref="PngEncoder"/> class.

0 commit comments

Comments
 (0)