From 328e0465db6dfa46bc90d93eedb6fc4f85eec86f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 15 Nov 2023 12:40:49 +1000 Subject: [PATCH] Wire up connectors and gif encoder --- .../Formats/AnimatedImageFrameMetadata.cs | 93 +++++++++++++++++++ .../Formats/AnimatedImageMetadata.cs | 32 +++++++ src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 70 ++++++++++++-- .../Formats/Gif/GifFrameMetadata.cs | 40 ++++++++ src/ImageSharp/Formats/Gif/GifMetadata.cs | 26 ++++++ .../Formats/Gif/MetadataExtensions.cs | 44 ++++++++- .../Formats/Png/Chunks/AnimationControl.cs | 14 +-- .../Formats/Png/MetadataExtensions.cs | 48 +++++++++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 4 +- .../Formats/Png/PngDisposalMethod.cs | 6 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 4 +- src/ImageSharp/Formats/Png/PngMetadata.cs | 3 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 2 +- .../Formats/Webp/Chunks/WebpFrameData.cs | 8 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 2 +- .../Formats/Webp/MetadataExtensions.cs | 48 ++++++++++ .../Formats/Webp/WebpAnimationDecoder.cs | 6 +- .../Formats/Webp/WebpBlendingMethod.cs | 12 +-- .../Formats/Webp/WebpDisposalMethod.cs | 4 +- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 14 +-- src/ImageSharp/Metadata/FrameDecodingMode.cs | 20 ---- src/ImageSharp/Metadata/ImageMetadata.cs | 26 ++++++ .../Formats/Gif/GifEncoderTests.cs | 4 +- .../Formats/Png/PngEncoderTests.cs | 5 + .../Formats/Png/PngFrameMetadataTests.cs | 4 +- .../Formats/WebP/WebpDecoderTests.cs | 4 +- 27 files changed, 465 insertions(+), 80 deletions(-) create mode 100644 src/ImageSharp/Formats/AnimatedImageFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/AnimatedImageMetadata.cs delete mode 100644 src/ImageSharp/Metadata/FrameDecodingMode.cs 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); }