Browse Source

Wire up connectors and gif encoder

pull/2588/head
James Jackson-South 2 years ago
parent
commit
328e0465db
  1. 93
      src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs
  2. 32
      src/ImageSharp/Formats/AnimatedImageMetadata.cs
  3. 70
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 40
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  5. 26
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  6. 44
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  7. 14
      src/ImageSharp/Formats/Png/Chunks/AnimationControl.cs
  8. 48
      src/ImageSharp/Formats/Png/MetadataExtensions.cs
  9. 4
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  10. 6
      src/ImageSharp/Formats/Png/PngDisposalMethod.cs
  11. 4
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  12. 3
      src/ImageSharp/Formats/Png/PngMetadata.cs
  13. 2
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  14. 8
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  15. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  16. 2
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  17. 48
      src/ImageSharp/Formats/Webp/MetadataExtensions.cs
  18. 6
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  19. 12
      src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
  20. 4
      src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
  21. 14
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  22. 20
      src/ImageSharp/Metadata/FrameDecodingMode.cs
  23. 26
      src/ImageSharp/Metadata/ImageMetadata.cs
  24. 4
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  25. 5
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  26. 4
      tests/ImageSharp.Tests/Formats/Png/PngFrameMetadataTests.cs
  27. 4
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

93
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
{
/// <summary>
/// Gets or sets the frame color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the duration of the frame.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets the frame alpha blending mode.
/// </summary>
public FrameBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the frame disposal mode.
/// </summary>
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
{
/// <summary>
/// Do not blend. Render the current frame on the canvas by overwriting the rectangle covered by the current frame.
/// </summary>
Source = 0,
/// <summary>
/// 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.
/// </summary>
Over = 1
}
internal enum FrameDisposalMode
{
/// <summary>
/// No disposal specified.
/// The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// 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.
/// </summary>
DoNotDispose = 1,
/// <summary>
/// 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.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// 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.
/// </summary>
RestoreToPrevious = 3
}
internal enum FrameColorTableMode
{
/// <summary>
/// The frame uses the shared color table specified by the image metadata.
/// </summary>
Global,
/// <summary>
/// The frame uses a color table specified by the frame metadata.
/// </summary>
Local
}

32
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
{
/// <summary>
/// Gets or sets the shared color table.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the shared color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
/// <summary>
/// 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 <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
/// 0 means to repeat indefinitely, count is set as repeat n-1 times. Defaults to 1.
/// </remarks>
/// </summary>
public ushort RepeatCount { get; set; }
}

70
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<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(ImageFrame<TPixel> frame, int transparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>(
Stream stream,
Image<TPixel> image,
ReadOnlyMemory<TPixel> globalPalette)
ReadOnlyMemory<TPixel> globalPalette,
int globalTransparencyIndex)
where TPixel : unmanaged, IPixel<TPixel>
{
if (image.Frames.Count == 1)
@ -195,8 +250,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
{
// Gather the metadata for this frame.
ImageFrame<TPixel> 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)

40
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
/// <inheritdoc/>
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<Color> 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,
};
}

26
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -71,4 +71,30 @@ public class GifMetadata : IDeepCloneable
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifMetadata(this);
internal static GifMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
{
int index = 0;
Color background = metadata.BackgroundColor;
if (metadata.ColorTable.HasValue)
{
ReadOnlySpan<Color> 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),
};
}
}

44
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);
/// <summary>
/// Gets the gif format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">
/// 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.
/// </param>
/// <returns>
/// <see langword="true"/> if the gif metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetGifMetadata(this ImageMetadata source, [NotNullWhen(true)] out GifMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
/// <summary>
/// Gets the gif format specific metadata for the image frame.
/// </summary>
@ -40,6 +56,32 @@ public static partial class MetadataExtensions
/// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
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,
};
}

14
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
/// <summary>
/// Gets the number of frames
/// </summary>
public int NumberFrames { get; }
public uint NumberFrames { get; }
/// <summary>
/// Gets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int NumberPlays { get; }
public uint NumberPlays { get; }
/// <summary>
/// Writes the acTL to the given buffer.
@ -31,8 +31,8 @@ internal readonly struct AnimationControl
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> 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);
}
/// <summary>
@ -42,6 +42,6 @@ internal readonly struct AnimationControl
/// <returns>The parsed acTL.</returns>
public static AnimationControl Parse(ReadOnlySpan<byte> data)
=> new(
numberFrames: BinaryPrimitives.ReadInt32BigEndian(data[..4]),
numberPlays: BinaryPrimitives.ReadInt32BigEndian(data[4..8]));
numberFrames: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
numberPlays: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]));
}

48
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);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// Gets the png format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the png metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetPngMetadata(this ImageMetadata source, [NotNullWhen(true)] out PngMetadata? metadata)
=> source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
/// <summary>
/// Gets the png format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static PngFrameMetadata GetPngFrameMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(PngFormat.Instance);
/// <summary>
/// Gets the aPng format specific metadata for the image frame.
/// Gets the png format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>The <see cref="PngFrameMetadata"/>.</returns>
public static bool TryGetPngFrameMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out PngFrameMetadata? metadata) => source.TryGetFormatMetadata(PngFormat.Instance, out metadata);
/// <returns>
/// <see langword="true"/> if the png frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
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,
};
}

4
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);

6
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
/// <summary>
/// The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
/// </summary>
Background,
RestoreToBackground,
/// <summary>
/// The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
/// </summary>
Previous
RestoreToPrevious
}

4
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
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="framesCount">The number of frames.</param>
/// <param name="playsCount">The number of times to loop this APNG.</param>
private void WriteAnimationControlChunk(Stream stream, int framesCount, int playsCount)
private void WriteAnimationControlChunk(Stream stream, uint framesCount, uint playsCount)
{
AnimationControl acTL = new(framesCount, playsCount);

3
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
/// <summary>
/// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping.
/// </summary>
public int RepeatCount { get; set; }
public uint RepeatCount { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);

2
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -160,7 +160,7 @@ internal abstract class BitWriterBase
/// <param name="loopCount">The number of times to loop the animation. If it is 0, this means infinitely.</param>
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);
}

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

2
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);
}
}

2
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);
}
}

48
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
/// <returns>The <see cref="WebpMetadata"/>.</returns>
public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the webp format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the webp metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetWebpMetadata(this ImageMetadata source, [NotNullWhen(true)] out WebpMetadata? metadata)
=> source.TryGetFormatMetadata(WebpFormat.Instance, out metadata);
/// <summary>
/// Gets the webp format specific metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="WebpFrameMetadata"/>.</returns>
public static WebpFrameMetadata GetWebpMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the webp format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">The metadata.</param>
/// <returns>
/// <see langword="true"/> if the webp frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
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,
};
}

6
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<byte> 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<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(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;

12
src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs

@ -9,14 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public enum WebpBlendingMethod
{
/// <summary>
/// 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.
/// </summary>
AlphaBlending = 0,
Source = 0,
/// <summary>
/// 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.
/// </summary>
DoNotBlend = 1
Over = 1,
}

4
src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs

@ -11,10 +11,10 @@ public enum WebpDisposalMethod
/// <summary>
/// Do not dispose. Leave the canvas as is.
/// </summary>
DoNotDispose = 0,
None = 0,
/// <summary>
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.
/// </summary>
Dispose = 1
RestoreToBackground = 1
}

14
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;
}
/// <summary>
@ -34,15 +34,15 @@ public class WebpMetadata : IDeepCloneable
/// <summary>
/// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely.
/// </summary>
public ushort AnimationLoopCount { get; set; } = 1;
public ushort RepeatCount { get; set; } = 1;
/// <summary>
/// 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 <see cref="WebpDisposalMethod.RestoreToBackground"/>.
/// </summary>
public Color AnimationBackground { get; set; }
public Color BackgroundColor { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpMetadata(this);

20
src/ImageSharp/Metadata/FrameDecodingMode.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Metadata;
/// <summary>
/// Enumerated frame process modes to apply to multi-frame images.
/// </summary>
public enum FrameDecodingMode
{
/// <summary>
/// Decodes all the frames of a multi-frame image.
/// </summary>
All,
/// <summary>
/// Decodes only the first frame of a multi-frame image.
/// </summary>
First
}

26
src/ImageSharp/Metadata/ImageMetadata.cs

@ -183,6 +183,32 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
return newMeta;
}
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>
/// <typeparam name="TFormatMetadata">The type of format metadata.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <param name="metadata">
/// 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.
/// </param>
/// <returns>
/// <see langword="true"/> if the frame metadata exists for the specified key; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> 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;
}
/// <inheritdoc/>
public ImageMetadata DeepClone() => new(this);

4
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -245,7 +245,7 @@ public class GifEncoderTests
int count = 0;
foreach (ImageFrame<TPixel> 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<TPixel> frame in image2.Frames)
{
if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata _))
if (frame.Metadata.TryGetGifFrameMetadata(out GifFrameMetadata _))
{
count++;
}

5
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<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageComparer.Exact.VerifySimilarity(output, image);

4
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));

4
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);
}

Loading…
Cancel
Save