diff --git a/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
new file mode 100644
index 000000000..5f4015180
--- /dev/null
+++ b/src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats;
+internal class AnimatedImageFrameMetadata
+{
+ ///
+ /// Gets or sets the frame color table.
+ ///
+ public ReadOnlyMemory? ColorTable { get; set; }
+
+ ///
+ /// Gets or sets the frame color table mode.
+ ///
+ public FrameColorTableMode ColorTableMode { get; set; }
+
+ ///
+ /// Gets or sets the duration of the frame.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ ///
+ /// Gets or sets the frame alpha blending mode.
+ ///
+ public FrameBlendMode BlendMode { get; set; }
+
+ ///
+ /// Gets or sets the frame disposal mode.
+ ///
+ public FrameDisposalMode DisposalMode { get; set; }
+}
+
+#pragma warning disable SA1201 // Elements should appear in the correct order
+internal enum FrameBlendMode
+#pragma warning restore SA1201 // Elements should appear in the correct order
+{
+ ///
+ /// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame.
+ ///
+ Source = 0,
+
+ ///
+ /// Blend the current frame with the previous frame in the animation sequence within the rectangle covered
+ /// by the current frame.
+ /// If the current has any transparent areas, the corresponding areas of the previous frame will be visible
+ /// through these transparent regions.
+ ///
+ Over = 1
+}
+
+internal enum FrameDisposalMode
+{
+ ///
+ /// No disposal specified.
+ /// The decoder is not required to take any action.
+ ///
+ Unspecified = 0,
+
+ ///
+ /// Do not dispose. The current frame is not disposed of, or in other words, not cleared or altered when moving to
+ /// the next frame. This means that the next frame is drawn over the current frame, and if the next frame contains
+ /// transparency, the previous frame will be visible through these transparent areas.
+ ///
+ DoNotDispose = 1,
+
+ ///
+ /// Restore to background color. When transitioning to the next frame, the area occupied by the current frame is
+ /// filled with the background color specified in the image metadata.
+ /// This effectively erases the current frame by replacing it with the background color before the next frame is displayed.
+ ///
+ RestoreToBackground = 2,
+
+ ///
+ /// Restore to previous. This method restores the area affected by the current frame to what it was before the
+ /// current frame was displayed. It essentially "undoes" the current frame, reverting to the state of the image
+ /// before the frame was displayed, then the next frame is drawn. This is useful for animations where only a small
+ /// part of the image changes from frame to frame.
+ ///
+ RestoreToPrevious = 3
+}
+
+internal enum FrameColorTableMode
+{
+ ///
+ /// The frame uses the shared color table specified by the image metadata.
+ ///
+ Global,
+
+ ///
+ /// The frame uses a color table specified by the frame metadata.
+ ///
+ Local
+}
diff --git a/src/ImageSharp/Formats/AnimatedImageMetadata.cs b/src/ImageSharp/Formats/AnimatedImageMetadata.cs
new file mode 100644
index 000000000..d89ec41f0
--- /dev/null
+++ b/src/ImageSharp/Formats/AnimatedImageMetadata.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats;
+internal class AnimatedImageMetadata
+{
+ ///
+ /// Gets or sets the shared color table.
+ ///
+ public ReadOnlyMemory? ColorTable { get; set; }
+
+ ///
+ /// Gets or sets the shared color table mode.
+ ///
+ public FrameColorTableMode ColorTableMode { get; set; }
+
+ ///
+ /// Gets or sets 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 .
+ ///
+ public Color BackgroundColor { get; set; }
+
+ ///
+ /// Gets or sets the number of times any animation is repeated.
+ ///
+ /// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
+ ///
+ ///
+ public ushort RepeatCount { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 926cc091c..33942ce54 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -8,6 +8,8 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@@ -86,8 +88,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
- ImageMetadata metadata = image.Metadata;
- GifMetadata gifMetadata = metadata.GetGifMetadata();
+ GifMetadata gifMetadata = GetGifMetadata(image);
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
@@ -96,7 +97,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Work out if there is an explicit transparent index set for the frame. We use that to ensure the
// correct value is set for the background index when quantizing.
- image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
+ GifFrameMetadata? frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (this.quantizer is null)
@@ -140,7 +141,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
- this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
+ this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
if (useGlobalTable)
{
@@ -164,15 +165,69 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
quantized.Dispose();
- this.EncodeAdditionalFrames(stream, image, globalPalette);
+ this.EncodeAdditionalFrames(stream, image, globalPalette, transparencyIndex);
stream.WriteByte(GifConstants.EndIntroducer);
}
+ private static GifMetadata GetGifMetadata(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
+ {
+ return gif;
+ }
+
+ if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
+ {
+ AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
+ return GifMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
+ {
+ AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
+ return GifMetadata.FromAnimatedMetadata(ani);
+ }
+
+ return new();
+ }
+
+ private static GifFrameMetadata? GetGifFrameMetadata(ImageFrame frame, int transparencyIndex)
+ where TPixel : unmanaged, IPixel
+ {
+ if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata? gif))
+ {
+ return gif;
+ }
+
+ GifFrameMetadata? metadata = null;
+ if (frame.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? png))
+ {
+ AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
+ metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
+ {
+ AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
+ metadata = GifFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (metadata?.ColorTableMode == GifColorTableMode.Global && transparencyIndex > -1)
+ {
+ metadata.HasTransparency = true;
+ metadata.TransparencyIndex = unchecked((byte)transparencyIndex);
+ }
+
+ return metadata;
+ }
+
private void EncodeAdditionalFrames(
Stream stream,
Image image,
- ReadOnlyMemory globalPalette)
+ ReadOnlyMemory globalPalette,
+ int globalTransparencyIndex)
where TPixel : unmanaged, IPixel
{
if (image.Frames.Count == 1)
@@ -195,8 +250,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{
// Gather the metadata for this frame.
ImageFrame currentFrame = image.Frames[i];
- ImageFrameMetadata metadata = currentFrame.Metadata;
- metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
+ GifFrameMetadata? gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
index faabf7dfa..f8734bb5a 100644
--- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
@@ -76,4 +77,43 @@ public class GifFrameMetadata : IDeepCloneable
///
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
+
+ internal static GifFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
+ {
+ // TODO: v4 How do I link the parent metadata to the frame metadata to get the global color table?
+ int index = -1;
+ float background = 1f;
+ if (metadata.ColorTable.HasValue)
+ {
+ ReadOnlySpan colorTable = metadata.ColorTable.Value.Span;
+ for (int i = 0; i < colorTable.Length; i++)
+ {
+ Vector4 vector = (Vector4)colorTable[i];
+ if (vector.W < background)
+ {
+ index = i;
+ }
+ }
+ }
+
+ bool hasTransparency = index >= 0;
+
+ return new()
+ {
+ LocalColorTable = metadata.ColorTable,
+ ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
+ FrameDelay = (int)Math.Round(metadata.Duration.TotalMilliseconds / 10),
+ DisposalMethod = GetMode(metadata.DisposalMode),
+ HasTransparency = hasTransparency,
+ TransparencyIndex = hasTransparency ? unchecked((byte)index) : byte.MinValue,
+ };
+ }
+
+ private static GifDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
+ {
+ FrameDisposalMode.DoNotDispose => GifDisposalMethod.NotDispose,
+ FrameDisposalMode.RestoreToBackground => GifDisposalMethod.RestoreToBackground,
+ FrameDisposalMode.RestoreToPrevious => GifDisposalMethod.RestoreToPrevious,
+ _ => GifDisposalMethod.Unspecified,
+ };
}
diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs
index d25e2a5cc..1331edee8 100644
--- a/src/ImageSharp/Formats/Gif/GifMetadata.cs
+++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs
@@ -71,4 +71,30 @@ public class GifMetadata : IDeepCloneable
///
public IDeepCloneable DeepClone() => new GifMetadata(this);
+
+ internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
+ {
+ int index = 0;
+ Color background = metadata.BackgroundColor;
+ if (metadata.ColorTable.HasValue)
+ {
+ ReadOnlySpan colorTable = metadata.ColorTable.Value.Span;
+ for (int i = 0; i < colorTable.Length; i++)
+ {
+ if (background == colorTable[i])
+ {
+ index = i;
+ break;
+ }
+ }
+ }
+
+ return new()
+ {
+ GlobalColorTable = metadata.ColorTable,
+ ColorTableMode = metadata.ColorTableMode == FrameColorTableMode.Global ? GifColorTableMode.Global : GifColorTableMode.Local,
+ RepeatCount = metadata.RepeatCount,
+ BackgroundColorIndex = (byte)Numerics.Clamp(index, 0, 255),
+ };
+ }
}
diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
index 9ba95952e..f4eaffe6b 100644
--- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
@@ -20,6 +21,21 @@ public static partial class MetadataExtensions
public static GifMetadata GetGifMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(GifFormat.Instance);
+ ///
+ /// Gets the gif format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ ///
+ /// When this method returns, contains the metadata associated with the specified image,
+ /// if found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the gif metadata exists; otherwise, .
+ ///
+ public static bool TryGetGifMetadata(this ImageMetadata source, [NotNullWhen(true)] out GifMetadata? metadata)
+ => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
+
///
/// Gets the gif format specific metadata for the image frame.
///
@@ -40,6 +56,32 @@ public static partial class MetadataExtensions
///
/// if the gif frame metadata exists; otherwise, .
///
- public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
+ public static bool TryGetGifFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
+
+ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
+ => new()
+ {
+ ColorTable = source.GlobalColorTable,
+ ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
+ RepeatCount = source.RepeatCount,
+ };
+
+ internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this GifFrameMetadata source)
+ => new()
+ {
+ ColorTable = source.LocalColorTable,
+ ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
+ Duration = TimeSpan.FromMilliseconds(source.FrameDelay * 10),
+ DisposalMode = GetMode(source.DisposalMethod),
+ BlendMode = FrameBlendMode.Source,
+ };
+
+ private static FrameDisposalMode GetMode(GifDisposalMethod method) => method switch
+ {
+ GifDisposalMethod.NotDispose => FrameDisposalMode.DoNotDispose,
+ GifDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
+ GifDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
+ _ => FrameDisposalMode.Unspecified,
+ };
}
diff --git a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
index a9f99a9e4..cd78f8088 100644
--- a/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
@@ -9,7 +9,7 @@ internal readonly struct AnimationControl
{
public const int Size = 8;
- public AnimationControl(int numberFrames, int numberPlays)
+ public AnimationControl(uint numberFrames, uint numberPlays)
{
this.NumberFrames = numberFrames;
this.NumberPlays = numberPlays;
@@ -18,12 +18,12 @@ internal readonly struct AnimationControl
///
/// Gets the number of frames
///
- public int NumberFrames { get; }
+ public uint NumberFrames { get; }
///
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
///
- public int NumberPlays { get; }
+ public uint NumberPlays { get; }
///
/// Writes the acTL to the given buffer.
@@ -31,8 +31,8 @@ internal readonly struct AnimationControl
/// The buffer to write to.
public void WriteTo(Span buffer)
{
- BinaryPrimitives.WriteInt32BigEndian(buffer[..4], this.NumberFrames);
- BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], this.NumberPlays);
+ BinaryPrimitives.WriteInt32BigEndian(buffer[..4], (int)this.NumberFrames);
+ BinaryPrimitives.WriteInt32BigEndian(buffer[4..8], (int)this.NumberPlays);
}
///
@@ -42,6 +42,6 @@ internal readonly struct AnimationControl
/// The parsed acTL.
public static AnimationControl Parse(ReadOnlySpan data)
=> new(
- numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
- numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
+ numberFrames: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
+ numberPlays: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]));
}
diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
index f24b8d1b5..4a606d3a4 100644
--- a/src/ImageSharp/Formats/Png/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
@@ -20,17 +21,56 @@ public static partial class MetadataExtensions
public static PngMetadata GetPngMetadata(this ImageMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
///
- /// Gets the aPng format specific metadata for the image frame.
+ /// Gets the png format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The metadata.
+ ///
+ /// if the png metadata exists; otherwise, .
+ ///
+ public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(true)] out PngMetadata? metadata)
+ => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
+
+ ///
+ /// Gets the png format specific metadata for the image frame.
///
/// The metadata this method extends.
/// The .
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
///
- /// Gets the aPng format specific metadata for the image frame.
+ /// Gets the png format specific metadata for the image frame.
///
/// The metadata this method extends.
/// The metadata.
- /// The .
- public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
+ ///
+ /// if the png frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
+
+ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this PngMetadata source)
+ => new()
+ {
+ ColorTable = source.ColorTable,
+ ColorTableMode = FrameColorTableMode.Global,
+ RepeatCount = (ushort)Numerics.Clamp(source.RepeatCount, 0, ushort.MaxValue),
+ };
+
+ internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this PngFrameMetadata source)
+ => new()
+ {
+ ColorTableMode = FrameColorTableMode.Global,
+ Duration = TimeSpan.FromMilliseconds(source.FrameDelay.ToDouble() * 1000),
+ DisposalMode = GetMode(source.DisposalMethod),
+ BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over,
+ };
+
+ private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch
+ {
+ PngDisposalMethod.None => FrameDisposalMode.DoNotDispose,
+ PngDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
+ PngDisposalMethod.RestoreToPrevious => FrameDisposalMode.RestoreToPrevious,
+ _ => FrameDisposalMode.Unspecified,
+ };
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index d8305a3f5..7d573efb6 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -621,8 +621,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
frame = image.Frames.AddFrame(previousFrame ?? image.Frames.RootFrame);
// If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
- if (previousFrameControl.DisposeOperation == PngDisposalMethod.Background
- || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.Previous))
+ if (previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToBackground
+ || (previousFrame is null && previousFrameControl.DisposeOperation == PngDisposalMethod.RestoreToPrevious))
{
Rectangle restoreArea = previousFrameControl.Bounds;
Rectangle interest = Rectangle.Intersect(frame.Bounds(), restoreArea);
diff --git a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
index 17391de95..a431e8941 100644
--- a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
+++ b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Png;
@@ -16,10 +16,10 @@ public enum PngDisposalMethod
///
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
///
- Background,
+ RestoreToBackground,
///
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
///
- Previous
+ RestoreToPrevious
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 04e3b1d84..be6991bab 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -176,7 +176,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (image.Frames.Count > 1)
{
- this.WriteAnimationControlChunk(stream, image.Frames.Count, pngMetadata.RepeatCount);
+ this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount);
// TODO: We should attempt to optimize the output by clipping the indexed result to
// non-transparent bounds. That way we can assign frame control bounds and encode
@@ -621,7 +621,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The containing image data.
/// The number of frames.
/// The number of times to loop this APNG.
- private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount)
+ private void WriteAnimationControlChunk(Stream stream, uint framesCount, uint playsCount)
{
AnimationControl acTL = new(framesCount, playsCount);
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index b113dbfc1..6110cdd0c 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png.Chunks;
-using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png;
@@ -82,7 +81,7 @@ public class PngMetadata : IDeepCloneable
///
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
///
- public int RepeatCount { get; set; }
+ public uint RepeatCount { get; set; }
///
public IDeepCloneable DeepClone() => new PngMetadata(this);
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index d502fd606..49b059b07 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -160,7 +160,7 @@ internal abstract class BitWriterBase
/// The number of times to loop the animation. If it is 0, this means infinitely.
public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount)
{
- WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
+ WebpAnimationParameter chunk = new(background.ToBgra32().PackedValue, loopCount);
chunk.WriteTo(stream);
}
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
index f22a3fd54..aee518326 100644
--- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
@@ -32,8 +32,8 @@ internal readonly struct WebpFrameData
width,
height,
duration,
- (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending,
- (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose)
+ (flags & 2) == 0 ? WebpBlendingMethod.Over : WebpBlendingMethod.Source,
+ (flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.None)
{
}
@@ -93,13 +93,13 @@ internal readonly struct WebpFrameData
{
byte flags = 0;
- if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend)
+ if (this.BlendingMethod is WebpBlendingMethod.Source)
{
// Set blending flag.
flags |= 2;
}
- if (this.DisposalMethod is WebpDisposalMethod.Dispose)
+ if (this.DisposalMethod is WebpDisposalMethod.RestoreToBackground)
{
// Set disposal flag.
flags |= 1;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 4fdbb31d3..0821be577 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -260,7 +260,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
- BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 98e50bb9c..3fea72c07 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -334,7 +334,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
- BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
index 7f0920f2d..44da191d2 100644
--- a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
@@ -18,10 +20,56 @@ public static partial class MetadataExtensions
/// The .
public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
+ ///
+ /// Gets the webp format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The metadata.
+ ///
+ /// if the webp metadata exists; otherwise, .
+ ///
+ public static bool TryGetWebpMetadata(this ImageMetadata source, [NotNullWhen(true)] out WebpMetadata? metadata)
+ => source.TryGetFormatMetadata(WebpFormat.Instance, out metadata);
+
///
/// Gets the webp format specific metadata for the image frame.
///
/// The metadata this method extends.
/// The .
public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
+
+ ///
+ /// Gets the webp format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The metadata.
+ ///
+ /// if the webp frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetWebpFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out WebpFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(WebpFormat.Instance, out metadata);
+
+ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this WebpMetadata source)
+ => new()
+ {
+ ColorTableMode = FrameColorTableMode.Global,
+ RepeatCount = source.RepeatCount,
+ BackgroundColor = source.BackgroundColor
+ };
+
+ internal static AnimatedImageFrameMetadata ToAnimatedImageFrameMetadata(this WebpFrameMetadata source)
+ => new()
+ {
+ ColorTableMode = FrameColorTableMode.Global,
+ Duration = TimeSpan.FromMilliseconds(source.FrameDelay),
+ DisposalMode = GetMode(source.DisposalMethod),
+ BlendMode = source.BlendMethod == WebpBlendingMethod.Over ? FrameBlendMode.Over : FrameBlendMode.Source,
+ };
+
+ private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch
+ {
+ WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
+ WebpDisposalMethod.None => FrameDisposalMode.DoNotDispose,
+ _ => FrameDisposalMode.DoNotDispose,
+ };
}
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index 66e69d9a4..f081cfcd8 100644
--- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -89,7 +89,7 @@ internal class WebpAnimationDecoder : IDisposable
this.metadata = new ImageMetadata();
this.webpMetadata = this.metadata.GetWebpMetadata();
- this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount;
+ this.webpMetadata.RepeatCount = features.AnimationLoopCount;
Span buffer = stackalloc byte[4];
uint frameCount = 0;
@@ -195,14 +195,14 @@ internal class WebpAnimationDecoder : IDisposable
Rectangle regionRectangle = frameData.Bounds;
- if (frameData.DisposalMethod is WebpDisposalMethod.Dispose)
+ if (frameData.DisposalMethod is WebpDisposalMethod.RestoreToBackground)
{
this.RestoreToBackground(imageFrame, backgroundColor);
}
using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo);
- bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending;
+ bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.Over;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
diff --git a/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
index cbd0e9a8c..482d62cd2 100644
--- a/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
@@ -9,14 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public enum WebpBlendingMethod
{
///
- /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
- /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
+ /// Do not blend. After disposing of the previous frame,
+ /// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
///
- AlphaBlending = 0,
+ Source = 0,
///
- /// Do not blend. After disposing of the previous frame,
- /// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
+ /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
+ /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle.
///
- DoNotBlend = 1
+ Over = 1,
}
diff --git a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
index d409973a9..397c2ee50 100644
--- a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
@@ -11,10 +11,10 @@ public enum WebpDisposalMethod
///
/// Do not dispose. Leave the canvas as is.
///
- DoNotDispose = 0,
+ None = 0,
///
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.
///
- Dispose = 1
+ RestoreToBackground = 1
}
diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
index a6bb0a7b8..9d0d8d08d 100644
--- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
@@ -22,8 +22,8 @@ public class WebpMetadata : IDeepCloneable
private WebpMetadata(WebpMetadata other)
{
this.FileFormat = other.FileFormat;
- this.AnimationLoopCount = other.AnimationLoopCount;
- this.AnimationBackground = other.AnimationBackground;
+ this.RepeatCount = other.RepeatCount;
+ this.BackgroundColor = other.BackgroundColor;
}
///
@@ -34,15 +34,15 @@ public class WebpMetadata : IDeepCloneable
///
/// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely.
///
- public ushort AnimationLoopCount { get; set; } = 1;
+ public ushort RepeatCount { get; set; } = 1;
///
- /// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
- /// This color MAY be used to fill the unused space on the canvas around the frames,
+ /// Gets or sets 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 method is 1.
+ /// The background color is also used when the Disposal method is .
///
- public Color AnimationBackground { get; set; }
+ public Color BackgroundColor { get; set; }
///
public IDeepCloneable DeepClone() => new WebpMetadata(this);
diff --git a/src/ImageSharp/Metadata/FrameDecodingMode.cs b/src/ImageSharp/Metadata/FrameDecodingMode.cs
deleted file mode 100644
index 3a5965489..000000000
--- a/src/ImageSharp/Metadata/FrameDecodingMode.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Metadata;
-
-///
-/// Enumerated frame process modes to apply to multi-frame images.
-///
-public enum FrameDecodingMode
-{
- ///
- /// Decodes all the frames of a multi-frame image.
- ///
- All,
-
- ///
- /// Decodes only the first frame of a multi-frame image.
- ///
- First
-}
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index f54fc5c7a..e1284b50e 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -183,6 +183,32 @@ public sealed class ImageMetadata : IDeepCloneable
return newMeta;
}
+ ///
+ /// Gets the metadata value associated with the specified key.
+ ///
+ /// The type of format metadata.
+ /// The key of the value to get.
+ ///
+ /// When this method returns, contains the metadata associated with the specified key,
+ /// if the key is found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the frame metadata exists for the specified key; otherwise, .
+ ///
+ public bool TryGetFormatMetadata(IImageFormat key, out TFormatMetadata? metadata)
+ where TFormatMetadata : class, IDeepCloneable
+ {
+ if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
+ {
+ metadata = (TFormatMetadata)meta;
+ return true;
+ }
+
+ metadata = default;
+ return false;
+ }
+
///
public ImageMetadata DeepClone() => new(this);
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
index 31001e31b..65d186c91 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
@@ -245,7 +245,7 @@ public class GifEncoderTests
int count = 0;
foreach (ImageFrame frame in image.Frames)
{
- if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
+ if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _))
{
count++;
}
@@ -261,7 +261,7 @@ public class GifEncoderTests
count = 0;
foreach (ImageFrame frame in image2.Frames)
{
- if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
+ if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _))
{
count++;
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 92c07a27a..a6840b33e 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -3,6 +3,7 @@
// ReSharper disable InconsistentNaming
using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@@ -453,6 +454,10 @@ public partial class PngEncoderTests
memStream.Position = 0;
image.DebugSave(provider: provider, encoder: PngEncoder, null, false);
+ image.DebugSave(provider: provider, encoder: new GifEncoder(), "gif", false);
+
+ string path = provider.Utility.GetTestOutputFileName("gif");
+ image.Save(path);
using Image output = Image.Load(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
index e29585c2d..9ba261728 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
@@ -14,7 +14,7 @@ public class PngFrameMetadataTests
PngFrameMetadata meta = new()
{
FrameDelay = new(1, 0),
- DisposalMethod = PngDisposalMethod.Background,
+ DisposalMethod = PngDisposalMethod.RestoreToBackground,
BlendMethod = PngBlendMethod.Over,
};
@@ -25,7 +25,7 @@ public class PngFrameMetadataTests
Assert.True(meta.BlendMethod.Equals(clone.BlendMethod));
clone.FrameDelay = new(2, 1);
- clone.DisposalMethod = PngDisposalMethod.Previous;
+ clone.DisposalMethod = PngDisposalMethod.RestoreToPrevious;
clone.BlendMethod = PngBlendMethod.Source;
Assert.False(meta.FrameDelay.Equals(clone.FrameDelay));
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index 4b03671e1..6301f341c 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -307,7 +307,7 @@ public class WebpDecoderTests
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
- Assert.Equal(0, webpMetaData.AnimationLoopCount);
+ Assert.Equal(0, webpMetaData.RepeatCount);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@@ -324,7 +324,7 @@ public class WebpDecoderTests
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
- Assert.Equal(0, webpMetaData.AnimationLoopCount);
+ Assert.Equal(0, webpMetaData.RepeatCount);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}