Skip to content

Commit 328e046

Browse files
Wire up connectors and gif encoder
1 parent afe2133 commit 328e046

27 files changed

Lines changed: 465 additions & 80 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats;
5+
internal class AnimatedImageFrameMetadata
6+
{
7+
/// <summary>
8+
/// Gets or sets the frame color table.
9+
/// </summary>
10+
public ReadOnlyMemory<Color>? ColorTable { get; set; }
11+
12+
/// <summary>
13+
/// Gets or sets the frame color table mode.
14+
/// </summary>
15+
public FrameColorTableMode ColorTableMode { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets the duration of the frame.
19+
/// </summary>
20+
public TimeSpan Duration { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets the frame alpha blending mode.
24+
/// </summary>
25+
public FrameBlendMode BlendMode { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the frame disposal mode.
29+
/// </summary>
30+
public FrameDisposalMode DisposalMode { get; set; }
31+
}
32+
33+
#pragma warning disable SA1201 // Elements should appear in the correct order
34+
internal enum FrameBlendMode
35+
#pragma warning restore SA1201 // Elements should appear in the correct order
36+
{
37+
/// <summary>
38+
/// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame.
39+
/// </summary>
40+
Source = 0,
41+
42+
/// <summary>
43+
/// Blend the current frame with the previous frame in the animation sequence within the rectangle covered
44+
/// by the current frame.
45+
/// If the current has any transparent areas, the corresponding areas of the previous frame will be visible
46+
/// through these transparent regions.
47+
/// </summary>
48+
Over = 1
49+
}
50+
51+
internal enum FrameDisposalMode
52+
{
53+
/// <summary>
54+
/// No disposal specified.
55+
/// The decoder is not required to take any action.
56+
/// </summary>
57+
Unspecified = 0,
58+
59+
/// <summary>
60+
/// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to
61+
/// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains
62+
/// transparency, the previous frame will be visible through these transparent areas.
63+
/// </summary>
64+
DoNotDispose = 1,
65+
66+
/// <summary>
67+
/// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is
68+
/// filled with the background color specified in the image metadata.
69+
/// This effectively erases the current frame by replacing it with the background color before the next frame is displayed.
70+
/// </summary>
71+
RestoreToBackground = 2,
72+
73+
/// <summary>
74+
/// Restore to previous. This method restores the area affected by the current frame to what it was before the
75+
/// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image
76+
/// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small
77+
/// part of the image changes from frame to frame.
78+
/// </summary>
79+
RestoreToPrevious = 3
80+
}
81+
82+
internal enum FrameColorTableMode
83+
{
84+
/// <summary>
85+
/// The frame uses the shared color table specified by the image metadata.
86+
/// </summary>
87+
Global,
88+
89+
/// <summary>
90+
/// The frame uses a color table specified by the frame metadata.
91+
/// </summary>
92+
Local
93+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats;
5+
internal class AnimatedImageMetadata
6+
{
7+
/// <summary>
8+
/// Gets or sets the shared color table.
9+
/// </summary>
10+
public ReadOnlyMemory<Color>? ColorTable { get; set; }
11+
12+
/// <summary>
13+
/// Gets or sets the shared color table mode.
14+
/// </summary>
15+
public FrameColorTableMode ColorTableMode { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets the default background color of the canvas when animating.
19+
/// This color may be used to fill the unused space on the canvas around the frames,
20+
/// as well as the transparent pixels of the first frame.
21+
/// The background color is also used when the disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
22+
/// </summary>
23+
public Color BackgroundColor { get; set; }
24+
25+
/// <summary>
26+
/// Gets or sets the number of times any animation is repeated.
27+
/// <remarks>
28+
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
29+
/// </remarks>
30+
/// </summary>
31+
public ushort RepeatCount { get; set; }
32+
}

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Runtime.Intrinsics;
99
using System.Runtime.Intrinsics.X86;
1010
using SixLabors.ImageSharp.Advanced;
11+
using SixLabors.ImageSharp.Formats.Png;
12+
using SixLabors.ImageSharp.Formats.Webp;
1113
using SixLabors.ImageSharp.Memory;
1214
using SixLabors.ImageSharp.Metadata;
1315
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@@ -86,8 +88,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
8688
Guard.NotNull(image, nameof(image));
8789
Guard.NotNull(stream, nameof(stream));
8890

89-
ImageMetadata metadata = image.Metadata;
90-
GifMetadata gifMetadata = metadata.GetGifMetadata();
91+
GifMetadata gifMetadata = GetGifMetadata(image);
9192
this.colorTableMode ??= gifMetadata.ColorTableMode;
9293
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
9394

@@ -96,7 +97,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
9697

9798
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
9899
// correct value is set for the background index when quantizing.
99-
image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
100+
GifFrameMetadata? frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
100101
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
101102

102103
if (this.quantizer is null)
@@ -140,7 +141,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
140141

141142
// Get the number of bits.
142143
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
143-
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
144+
this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
144145

145146
if (useGlobalTable)
146147
{
@@ -164,15 +165,69 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
164165

165166
quantized.Dispose();
166167

167-
this.EncodeAdditionalFrames(stream, image, globalPalette);
168+
this.EncodeAdditionalFrames(stream, image, globalPalette, transparencyIndex);
168169

169170
stream.WriteByte(GifConstants.EndIntroducer);
170171
}
171172

173+
private static GifMetadata GetGifMetadata<TPixel>(Image<TPixel> image)
174+
where TPixel : unmanaged, IPixel<TPixel>
175+
{
176+
if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
177+
{
178+
return gif;
179+
}
180+
181+
if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
182+
{
183+
AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
184+
return GifMetadata.FromAnimatedMetadata(ani);
185+
}
186+
187+
if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
188+
{
189+
AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
190+
return GifMetadata.FromAnimatedMetadata(ani);
191+
}
192+
193+
return new();
194+
}
195+
196+
private static GifFrameMetadata? GetGifFrameMetadata<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
197+
where TPixel : unmanaged, IPixel<TPixel>
198+
{
199+
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata? gif))
200+
{
201+
return gif;
202+
}
203+
204+
GifFrameMetadata? metadata = null;
205+
if (frame.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? png))
206+
{
207+
AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
208+
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
209+
}
210+
211+
if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
212+
{
213+
AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
214+
metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
215+
}
216+
217+
if (metadata?.ColorTableMode == GifColorTableMode.Global && transparencyIndex > -1)
218+
{
219+
metadata.HasTransparency = true;
220+
metadata.TransparencyIndex = unchecked((byte)transparencyIndex);
221+
}
222+
223+
return metadata;
224+
}
225+
172226
private void EncodeAdditionalFrames<TPixel>(
173227
Stream stream,
174228
Image<TPixel> image,
175-
ReadOnlyMemory<TPixel> globalPalette)
229+
ReadOnlyMemory<TPixel> globalPalette,
230+
int globalTransparencyIndex)
176231
where TPixel : unmanaged, IPixel<TPixel>
177232
{
178233
if (image.Frames.Count == 1)
@@ -195,8 +250,7 @@ private void EncodeAdditionalFrames<TPixel>(
195250
{
196251
// Gather the metadata for this frame.
197252
ImageFrame<TPixel> currentFrame = image.Frames[i];
198-
ImageFrameMetadata metadata = currentFrame.Metadata;
199-
metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
253+
GifFrameMetadata? gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
200254
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
201255

202256
if (!useLocal && !hasPaletteQuantizer && i > 0)

src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Numerics;
45
using SixLabors.ImageSharp.PixelFormats;
56

67
namespace SixLabors.ImageSharp.Formats.Gif;
@@ -76,4 +77,43 @@ private GifFrameMetadata(GifFrameMetadata other)
7677

7778
/// <inheritdoc/>
7879
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
80+
81+
internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
82+
{
83+
// TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
84+
int index = -1;
85+
float background = 1f;
86+
if (metadata.ColorTable.HasValue)
87+
{
88+
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
89+
for (int i = 0; i < colorTable.Length; i++)
90+
{
91+
Vector4 vector = (Vector4)colorTable[i];
92+
if (vector.W < background)
93+
{
94+
index = i;
95+
}
96+
}
97+
}
98+
99+
bool hasTransparency = index >= 0;
100+
101+
return new()
102+
{
103+
LocalColorTable = metadata.ColorTable,
104+
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
105+
FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
106+
DisposalMethod = GetMode(metadata.DisposalMode),
107+
HasTransparency = hasTransparency,
108+
TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
109+
};
110+
}
111+
112+
private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
113+
{
114+
FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose,
115+
FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground,
116+
FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious,
117+
_ => GifDisposalMethod.Unspecified,
118+
};
79119
}

src/ImageSharp/Formats/Gif/GifMetadata.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,30 @@ private GifMetadata(GifMetadata other)
7171

7272
/// <inheritdoc/>
7373
public IDeepCloneable DeepClone() => new GifMetadata(this);
74+
75+
internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
76+
{
77+
int index = 0;
78+
Color background = metadata.BackgroundColor;
79+
if (metadata.ColorTable.HasValue)
80+
{
81+
ReadOnlySpan<Color> colorTable = metadata.ColorTable.Value.Span;
82+
for (int i = 0; i < colorTable.Length; i++)
83+
{
84+
if (background == colorTable[i])
85+
{
86+
index = i;
87+
break;
88+
}
89+
}
90+
}
91+
92+
return new()
93+
{
94+
GlobalColorTable = metadata.ColorTable,
95+
ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
96+
RepeatCount = metadata.RepeatCount,
97+
BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255),
98+
};
99+
}
74100
}

src/ImageSharp/Formats/Gif/MetadataExtensions.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using SixLabors.ImageSharp.Formats;
56
using SixLabors.ImageSharp.Formats.Gif;
67
using SixLabors.ImageSharp.Metadata;
78

@@ -20,6 +21,21 @@ public static partial class MetadataExtensions
2021
public static GifMetadata GetGifMetadata(this ImageMetadata source)
2122
=> source.GetFormatMetadata(GifFormat.Instance);
2223

24+
/// <summary>
25+
/// Gets the gif format specific metadata for the image.
26+
/// </summary>
27+
/// <param name="source">The metadata this method extends.</param>
28+
/// <param name="metadata">
29+
/// When this method returns, contains the metadata associated with the specified image,
30+
/// if found; otherwise, the default value for the type of the metadata parameter.
31+
/// This parameter is passed uninitialized.
32+
/// </param>
33+
/// <returns>
34+
/// <see langword="true"/> if the gif metadata exists; otherwise, <see langword="false"/>.
35+
/// </returns>
36+
public static bool TryGetGifMetadata(this ImageMetadata source, [NotNullWhen(true)] out GifMetadata? metadata)
37+
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
38+
2339
/// <summary>
2440
/// Gets the gif format specific metadata for the image frame.
2541
/// </summary>
@@ -40,6 +56,32 @@ public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source)
4056
/// <returns>
4157
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
4258
/// </returns>
43-
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
59+
public static bool TryGetGifFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
4460
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
61+
62+
internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
63+
=> new()
64+
{
65+
ColorTable = source.GlobalColorTable,
66+
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
67+
RepeatCount = source.RepeatCount,
68+
};
69+
70+
internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source)
71+
=> new()
72+
{
73+
ColorTable = source.LocalColorTable,
74+
ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
75+
Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10),
76+
DisposalMode = GetMode(source.DisposalMethod),
77+
BlendMode = FrameBlendMode.Source,
78+
};
79+
80+
private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch
81+
{
82+
GifDisposalMethod.NotDispose => FrameDisposalMode.DoNotDispose,
83+
GifDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
84+
GifDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
85+
_ => FrameDisposalMode.Unspecified,
86+
};
4587
}

0 commit comments

Comments
 (0)