diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
index 7caaa5868..fc58ef344 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
@@ -656,6 +657,36 @@ internal static partial class SimdUtils
return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte();
}
+ ///
+ /// Blend packed 32-bit unsigned integers from and using .
+ /// The high bit of each corresponding byte determines the selection.
+ /// If the high bit is set the element of is selected.
+ /// The element of is selected otherwise.
+ ///
+ /// The left vector.
+ /// The right vector.
+ /// The mask vector.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask)
+ => BlendVariable(left.AsByte(), right.AsByte(), mask.AsByte()).AsUInt32();
+
+ ///
+ /// Count the number of leading zero bits in a mask.
+ /// Similar in behavior to the x86 instruction LZCNT.
+ ///
+ /// The value.
+ public static ushort LeadingZeroCount(ushort value)
+ => (ushort)(BitOperations.LeadingZeroCount(value) - 16);
+
+ ///
+ /// Count the number of trailing zero bits in an integer value.
+ /// Similar in behavior to the x86 instruction TZCNT.
+ ///
+ /// The value.
+ public static ushort TrailingZeroCount(ushort value)
+ => (ushort)(BitOperations.TrailingZeroCount(value << 16) - 16);
+
///
/// as many elements as possible, slicing them down (keeping the remainder).
///
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/AnimationUtilities.cs b/src/ImageSharp/Formats/AnimationUtilities.cs
new file mode 100644
index 000000000..67ee72e95
--- /dev/null
+++ b/src/ImageSharp/Formats/AnimationUtilities.cs
@@ -0,0 +1,244 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+using SixLabors.ImageSharp.Advanced;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats;
+
+///
+/// Utility methods for animated formats.
+///
+internal static class AnimationUtilities
+{
+ ///
+ /// Deduplicates pixels between the previous and current frame returning only the changed pixels and bounds.
+ ///
+ /// The type of pixel format.
+ /// The configuration.
+ /// The previous frame if present.
+ /// The current frame.
+ /// The next frame if present.
+ /// The resultant output.
+ /// The value to use when replacing duplicate pixels.
+ /// Whether the resultant frame represents an animation blend.
+ /// The clamping bound to apply when calculating difference bounds.
+ /// The representing the operation result.
+ public static (bool Difference, Rectangle Bounds) DeDuplicatePixels(
+ Configuration configuration,
+ ImageFrame? previousFrame,
+ ImageFrame currentFrame,
+ ImageFrame? nextFrame,
+ ImageFrame resultFrame,
+ Color replacement,
+ bool blend,
+ ClampingMode clampingMode = ClampingMode.None)
+ where TPixel : unmanaged, IPixel
+ {
+ MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
+ using IMemoryOwner buffers = memoryAllocator.Allocate(currentFrame.Width * 4, AllocationOptions.Clean);
+ Span previous = buffers.GetSpan()[..currentFrame.Width];
+ Span current = buffers.GetSpan().Slice(currentFrame.Width, currentFrame.Width);
+ Span next = buffers.GetSpan().Slice(currentFrame.Width * 2, currentFrame.Width);
+ Span result = buffers.GetSpan()[(currentFrame.Width * 3)..];
+
+ Rgba32 bg = replacement;
+
+ int top = int.MinValue;
+ int bottom = int.MaxValue;
+ int left = int.MaxValue;
+ int right = int.MinValue;
+
+ bool hasDiff = false;
+ for (int y = 0; y < currentFrame.Height; y++)
+ {
+ if (previousFrame != null)
+ {
+ PixelOperations.Instance.ToRgba32(configuration, previousFrame.DangerousGetPixelRowMemory(y).Span, previous);
+ }
+
+ PixelOperations.Instance.ToRgba32(configuration, currentFrame.DangerousGetPixelRowMemory(y).Span, current);
+
+ if (nextFrame != null)
+ {
+ PixelOperations.Instance.ToRgba32(configuration, nextFrame.DangerousGetPixelRowMemory(y).Span, next);
+ }
+
+ ref Vector256 previousBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(previous));
+ ref Vector256 currentBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(current));
+ ref Vector256 nextBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(next));
+ ref Vector256 resultBase256 = ref Unsafe.As>(ref MemoryMarshal.GetReference(result));
+
+ int i = 0;
+ uint x = 0;
+ bool hasRowDiff = false;
+ int length = current.Length;
+ int remaining = current.Length;
+
+ if (Avx2.IsSupported && remaining >= 8)
+ {
+ Vector256 r256 = previousFrame != null ? Vector256.Create(bg.PackedValue) : Vector256.Zero;
+ Vector256 vmb256 = Vector256.Zero;
+ if (blend)
+ {
+ vmb256 = Avx2.CompareEqual(vmb256, vmb256);
+ }
+
+ while (remaining >= 8)
+ {
+ Vector256 p = Unsafe.Add(ref previousBase256, x).AsUInt32();
+ Vector256 c = Unsafe.Add(ref currentBase256, x).AsUInt32();
+
+ Vector256 eq = Avx2.CompareEqual(p, c);
+ Vector256 r = Avx2.BlendVariable(c, r256, Avx2.And(eq, vmb256));
+
+ if (nextFrame != null)
+ {
+ Vector256 n = Avx2.ShiftRightLogical(Unsafe.Add(ref nextBase256, x).AsUInt32(), 24).AsInt32();
+ eq = Avx2.AndNot(Avx2.CompareGreaterThan(Avx2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
+ }
+
+ Unsafe.Add(ref resultBase256, x) = r.AsByte();
+
+ uint msk = (uint)Avx2.MoveMask(eq.AsByte());
+ msk = ~msk;
+
+ if (msk != 0)
+ {
+ // If is diff is found, the left side is marked by the min of previously found left side and the start position.
+ // The right is the max of the previously found right side and the end position.
+ int start = i + (BitOperations.TrailingZeroCount(msk) / sizeof(uint));
+ int end = i + (8 - (BitOperations.LeadingZeroCount(msk) / sizeof(uint)));
+ left = Math.Min(left, start);
+ right = Math.Max(right, end);
+ hasRowDiff = true;
+ hasDiff = true;
+ }
+
+ x++;
+ i += 8;
+ remaining -= 8;
+ }
+ }
+
+ if (Sse2.IsSupported && remaining >= 4)
+ {
+ // Update offset since we may be operating on the remainder previously incremented by pixel steps of 8.
+ x *= 2;
+ Vector128 r128 = previousFrame != null ? Vector128.Create(bg.PackedValue) : Vector128.Zero;
+ Vector128 vmb128 = Vector128.Zero;
+ if (blend)
+ {
+ vmb128 = Sse2.CompareEqual(vmb128, vmb128);
+ }
+
+ while (remaining >= 4)
+ {
+ Vector128 p = Unsafe.Add(ref Unsafe.As, Vector128>(ref previousBase256), x);
+ Vector128 c = Unsafe.Add(ref Unsafe.As, Vector128>(ref currentBase256), x);
+
+ Vector128 eq = Sse2.CompareEqual(p, c);
+ Vector128 r = SimdUtils.HwIntrinsics.BlendVariable(c, r128, Sse2.And(eq, vmb128));
+
+ if (nextFrame != null)
+ {
+ Vector128 n = Sse2.ShiftRightLogical(Unsafe.Add(ref Unsafe.As, Vector128>(ref nextBase256), x), 24).AsInt32();
+ eq = Sse2.AndNot(Sse2.CompareGreaterThan(Sse2.ShiftRightLogical(c, 24).AsInt32(), n).AsUInt32(), eq);
+ }
+
+ Unsafe.Add(ref Unsafe.As, Vector128>(ref resultBase256), x) = r;
+
+ ushort msk = (ushort)(uint)Sse2.MoveMask(eq.AsByte());
+ msk = (ushort)~msk;
+ if (msk != 0)
+ {
+ // If is diff is found, the left side is marked by the min of previously found left side and the start position.
+ // The right is the max of the previously found right side and the end position.
+ int start = i + (SimdUtils.HwIntrinsics.TrailingZeroCount(msk) / sizeof(uint));
+ int end = i + (4 - (SimdUtils.HwIntrinsics.LeadingZeroCount(msk) / sizeof(uint)));
+ left = Math.Min(left, start);
+ right = Math.Max(right, end);
+ hasRowDiff = true;
+ hasDiff = true;
+ }
+
+ x++;
+ i += 4;
+ remaining -= 4;
+ }
+ }
+
+ // TODO: v4 AdvSimd when we can use .NET 8
+ for (i = remaining; i > 0; i--)
+ {
+ x = (uint)(length - i);
+
+ Rgba32 p = Unsafe.Add(ref MemoryMarshal.GetReference(previous), x);
+ Rgba32 c = Unsafe.Add(ref MemoryMarshal.GetReference(current), x);
+ Rgba32 n = Unsafe.Add(ref MemoryMarshal.GetReference(next), x);
+ ref Rgba32 r = ref Unsafe.Add(ref MemoryMarshal.GetReference(result), x);
+
+ bool peq = c.Rgba == (previousFrame != null ? p.Rgba : bg.Rgba);
+ Rgba32 val = (blend & peq) ? replacement : c;
+
+ peq &= nextFrame == null || (n.Rgba >> 24 >= c.Rgba >> 24);
+ r = val;
+
+ if (!peq)
+ {
+ // If is diff is found, the left side is marked by the min of previously found left side and the diff position.
+ // The right is the max of the previously found right side and the diff position + 1.
+ left = Math.Min(left, (int)x);
+ right = Math.Max(right, (int)x + 1);
+ hasRowDiff = true;
+ hasDiff = true;
+ }
+ }
+
+ if (hasRowDiff)
+ {
+ if (top == int.MinValue)
+ {
+ top = y;
+ }
+
+ bottom = y + 1;
+ }
+
+ PixelOperations.Instance.FromRgba32(configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span);
+ }
+
+ Rectangle bounds = Rectangle.FromLTRB(
+ left = Numerics.Clamp(left, 0, resultFrame.Width - 1),
+ top = Numerics.Clamp(top, 0, resultFrame.Height - 1),
+ Numerics.Clamp(right, left + 1, resultFrame.Width),
+ Numerics.Clamp(bottom, top + 1, resultFrame.Height));
+
+ // Webp requires even bounds
+ if (clampingMode == ClampingMode.Even)
+ {
+ bounds.Width = Math.Min(resultFrame.Width, bounds.Width + (bounds.X & 1));
+ bounds.Height = Math.Min(resultFrame.Height, bounds.Height + (bounds.Y & 1));
+ bounds.X = Math.Max(0, bounds.X - (bounds.X & 1));
+ bounds.Y = Math.Max(0, bounds.Y - (bounds.Y & 1));
+ }
+
+ return (hasDiff, bounds);
+ }
+}
+
+#pragma warning disable SA1201 // Elements should appear in the correct order
+internal enum ClampingMode
+#pragma warning restore SA1201 // Elements should appear in the correct order
+{
+ None,
+
+ Even,
+}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index bc41c89dc..aecbbbbc7 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -797,6 +797,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.gifMetadata.GlobalColorTable = colorTable;
}
}
+
+ this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
}
private unsafe struct ScratchBuffer
diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs
index 150ee9ccf..ab05548ac 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Advanced;
-
namespace SixLabors.ImageSharp.Formats.Gif;
///
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 926cc091c..f0e1aafd7 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
@@ -4,10 +4,9 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
-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 +85,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,8 +94,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);
- int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
+ GifFrameMetadata frameMetadata = GetGifFrameMetadata(image.Frames.RootFrame, -1);
if (this.quantizer is null)
{
@@ -105,6 +102,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
// We avoid dithering by default to preserve the original colors.
+ int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex);
}
else
@@ -131,16 +129,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
WriteHeader(stream);
// Write the LSD.
- transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
- byte backgroundIndex = unchecked((byte)transparencyIndex);
- if (transparencyIndex == -1)
+ int derivedTransparencyIndex = GetTransparentIndex(quantized, null);
+ if (derivedTransparencyIndex >= 0)
{
- backgroundIndex = gifMetadata.BackgroundColorIndex;
+ frameMetadata.HasTransparency = true;
+ frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
}
+ byte backgroundIndex = derivedTransparencyIndex >= 0
+ ? frameMetadata.TransparencyIndex
+ : gifMetadata.BackgroundColorIndex;
+
// 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)
{
@@ -157,22 +159,78 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
}
- this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex);
+ this.EncodeFirstFrame(stream, frameMetadata, quantized);
// Capture the global palette for reuse on subsequent frames and cleanup the quantized frame.
TPixel[] globalPalette = image.Frames.Count == 1 ? Array.Empty() : quantized.Palette.ToArray();
- quantized.Dispose();
-
- this.EncodeAdditionalFrames(stream, image, globalPalette);
+ this.EncodeAdditionalFrames(stream, image, globalPalette, derivedTransparencyIndex, frameMetadata.DisposalMethod);
stream.WriteByte(GifConstants.EndIntroducer);
+
+ quantized?.Dispose();
+ }
+
+ private static GifMetadata GetGifMetadata(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
+ {
+ return (GifMetadata)gif.DeepClone();
+ }
+
+ 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 explicit new instance so we do not mutate the original metadata.
+ return new();
+ }
+
+ private static GifFrameMetadata GetGifFrameMetadata(ImageFrame frame, int transparencyIndex)
+ where TPixel : unmanaged, IPixel
+ {
+ if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
+ {
+ return (GifFrameMetadata)gif.DeepClone();
+ }
+
+ GifFrameMetadata? metadata = null;
+ if (frame.Metadata.TryGetPngMetadata(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 = ClampIndex(transparencyIndex);
+ }
+
+ return metadata ?? new();
}
private void EncodeAdditionalFrames(
Stream stream,
Image image,
- ReadOnlyMemory globalPalette)
+ ReadOnlyMemory globalPalette,
+ int globalTransparencyIndex,
+ GifDisposalMethod previousDisposalMethod)
where TPixel : unmanaged, IPixel
{
if (image.Frames.Count == 1)
@@ -187,24 +245,22 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame previousFrame = image.Frames.RootFrame;
// This frame is reused to store de-duplicated pixel buffers.
- // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate
- // frames using both local and global palettes.
using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size());
for (int i = 1; i < image.Frames.Count; i++)
{
// Gather the metadata for this frame.
ImageFrame currentFrame = image.Frames[i];
- ImageFrameMetadata metadata = currentFrame.Metadata;
- metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata);
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local);
+ ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
+ GifFrameMetadata gifMetadata = GetGifFrameMetadata(currentFrame, globalTransparencyIndex);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0)
{
// The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging.
// This allows a reduction of memory usage across multi-frame gifs using a global palette
// and also allows use to reuse the cache from previous runs.
- int transparencyIndex = gifMetadata?.HasTransparency == true ? gifMetadata.TransparencyIndex : -1;
+ int transparencyIndex = gifMetadata.HasTransparency ? gifMetadata.TransparencyIndex : -1;
paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex);
hasPaletteQuantizer = true;
}
@@ -213,12 +269,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
stream,
previousFrame,
currentFrame,
+ nextFrame,
encodingFrame,
useLocal,
gifMetadata,
- paletteQuantizer);
+ paletteQuantizer,
+ previousDisposalMethod);
previousFrame = currentFrame;
+ previousDisposalMethod = gifMetadata.DisposalMethod;
}
if (hasPaletteQuantizer)
@@ -229,16 +288,15 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
private void EncodeFirstFrame(
Stream stream,
- GifFrameMetadata? metadata,
- IndexedImageFrame quantized,
- int transparencyIndex)
+ GifFrameMetadata metadata,
+ IndexedImageFrame quantized)
where TPixel : unmanaged, IPixel
{
- this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+ this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle();
- bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local);
+ bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata.ColorTableMode == GifColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
@@ -248,367 +306,139 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteColorTable(quantized, bitDepth, stream);
}
- this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
private void EncodeAdditionalFrame(
Stream stream,
ImageFrame previousFrame,
ImageFrame currentFrame,
+ ImageFrame? nextFrame,
ImageFrame encodingFrame,
bool useLocal,
- GifFrameMetadata? metadata,
- PaletteQuantizer globalPaletteQuantizer)
+ GifFrameMetadata metadata,
+ PaletteQuantizer globalPaletteQuantizer,
+ GifDisposalMethod previousDisposal)
where TPixel : unmanaged, IPixel
{
// Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels.
- int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1;
- Vector4 replacement = Vector4.Zero;
- if (transparencyIndex >= 0)
- {
- if (useLocal)
- {
- if (metadata?.LocalColorTable?.Length > 0)
- {
- ReadOnlySpan palette = metadata.LocalColorTable.Value.Span;
- if (transparencyIndex < palette.Length)
- {
- replacement = palette[transparencyIndex].ToScaledVector4();
- }
- }
- }
- else
- {
- ReadOnlySpan palette = globalPaletteQuantizer.Palette.Span;
- if (transparencyIndex < palette.Length)
- {
- replacement = palette[transparencyIndex].ToScaledVector4();
- }
- }
- }
-
- this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement);
+ int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1;
- IndexedImageFrame quantized;
- if (useLocal)
- {
- // Reassign using the current frame and details.
- if (metadata?.LocalColorTable?.Length > 0)
- {
- // We can use the color data from the decoded metadata here.
- // We avoid dithering by default to preserve the original colors.
- ReadOnlyMemory palette = metadata.LocalColorTable.Value;
- PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
- quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
- else
- {
- // We must quantize the frame to generate a local color table.
- IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
- using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
- quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
- }
- else
- {
- // Quantize the image using the global palette.
- // Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
- globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
- quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds());
- }
+ ImageFrame? previous = previousDisposal == GifDisposalMethod.RestoreToBackground ? null : previousFrame;
- // Recalculate the transparency index as depending on the quantizer used could have a new value.
- transparencyIndex = GetTransparentIndex(quantized, metadata);
+ // Deduplicate and quantize the frame capturing only required parts.
+ (bool difference, Rectangle bounds) =
+ AnimationUtilities.DeDuplicatePixels(
+ this.configuration,
+ previous,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ Color.Transparent,
+ true);
- // Trim down the buffer to the minimum size required.
- Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
- Rectangle interest = TrimTransparentPixels(indices, transparencyIndex);
+ using IndexedImageFrame quantized = this.QuantizeAdditionalFrameAndUpdateMetadata(
+ encodingFrame,
+ bounds,
+ metadata,
+ useLocal,
+ globalPaletteQuantizer,
+ difference,
+ transparencyIndex);
- this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream);
+ this.WriteGraphicalControlExtension(metadata, stream);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
- this.WriteImageDescriptor(interest, useLocal, bitDepth, stream);
+ this.WriteImageDescriptor(bounds, useLocal, bitDepth, stream);
if (useLocal)
{
this.WriteColorTable(quantized, bitDepth, stream);
}
- this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex);
+ Buffer2D indices = ((IPixelSource)quantized).PixelBuffer;
+ this.WriteImageData(indices, stream, quantized.Palette.Length, metadata.TransparencyIndex);
}
- private void DeDuplicatePixels(
- ImageFrame backgroundFrame,
- ImageFrame sourceFrame,
- ImageFrame resultFrame,
- Vector4 replacement)
+ private IndexedImageFrame QuantizeAdditionalFrameAndUpdateMetadata(
+ ImageFrame encodingFrame,
+ Rectangle bounds,
+ GifFrameMetadata metadata,
+ bool useLocal,
+ PaletteQuantizer globalPaletteQuantizer,
+ bool hasDuplicates,
+ int transparencyIndex)
where TPixel : unmanaged, IPixel
{
- IMemoryOwner buffers = this.memoryAllocator.Allocate(backgroundFrame.Width * 3);
- Span background = buffers.GetSpan()[..backgroundFrame.Width];
- Span source = buffers.GetSpan()[backgroundFrame.Width..];
- Span result = buffers.GetSpan()[(backgroundFrame.Width * 2)..];
-
- // TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color
- // is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better.
- // This would require a more complex algorithm.
- for (int y = 0; y < backgroundFrame.Height; y++)
- {
- PixelOperations.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale);
- PixelOperations.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale);
-
- ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background));
- ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source));
- ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result));
-
- uint x = 0;
- int remaining = background.Length;
- if (Avx2.IsSupported && remaining >= 2)
- {
- Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W);
-
- while (remaining >= 2)
- {
- Vector256 b = Unsafe.Add(ref backgroundBase, x);
- Vector256 s = Unsafe.Add(ref sourceBase, x);
-
- Vector256 m = Avx.CompareEqual(b, s).AsInt32();
-
- m = Avx2.HorizontalAdd(m, m);
- m = Avx2.HorizontalAdd(m, m);
- m = Avx2.CompareEqual(m, Vector256.Create(-4));
-
- Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle());
-
- x++;
- remaining -= 2;
- }
- }
-
- for (int i = remaining; i >= 0; i--)
- {
- x = (uint)i;
- Vector4 b = Unsafe.Add(ref Unsafe.As, Vector4>(ref backgroundBase), x);
- Vector4 s = Unsafe.Add(ref Unsafe.As, Vector4>(ref sourceBase), x);
- ref Vector4 r = ref Unsafe.Add(ref Unsafe.As, Vector4>(ref resultBase), x);
- r = (b == s) ? replacement : s;
- }
-
- PixelOperations.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale);
- }
- }
-
- private static Rectangle TrimTransparentPixels(Buffer2D buffer, int transparencyIndex)
- {
- if (transparencyIndex < 0)
- {
- return buffer.FullRectangle();
- }
-
- byte trimmableIndex = unchecked((byte)transparencyIndex);
-
- int top = int.MinValue;
- int bottom = int.MaxValue;
- int left = int.MaxValue;
- int right = int.MinValue;
- int minY = -1;
- bool isTransparentRow = true;
-
- // Run through the buffer in a single pass. Use variables to track the min/max values.
- for (int y = 0; y < buffer.Height; y++)
+ IndexedImageFrame quantized;
+ if (useLocal)
{
- isTransparentRow = true;
- Span rowSpan = buffer.DangerousGetRowSpan(y);
- ref byte rowPtr = ref MemoryMarshal.GetReference(rowSpan);
- nint rowLength = (nint)(uint)rowSpan.Length;
- nint x = 0;
-
-#if NET7_0_OR_GREATER
- if (Vector128.IsHardwareAccelerated && rowLength >= Vector128.Count)
- {
- Vector256 trimmableVec256 = Vector256.Create(trimmableIndex);
-
- if (Vector256.IsHardwareAccelerated && rowLength >= Vector256.Count)
- {
- do
- {
- Vector256 vec = Vector256.LoadUnsafe(ref rowPtr, (nuint)x);
- Vector256 notEquals = ~Vector256.Equals(vec, trimmableVec256);
- uint mask = notEquals.ExtractMostSignificantBits();
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)uint.TrailingZeroCount(mask);
- nint end = (nint)uint.LeadingZeroCount(mask);
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector256.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector256.Count;
- }
- while (x <= rowLength - Vector256.Count);
- }
-
- Vector128 trimmableVec = Vector256.IsHardwareAccelerated
- ? trimmableVec256.GetLower()
- : Vector128.Create(trimmableIndex);
-
- while (x <= rowLength - Vector128.Count)
- {
- Vector128 vec = Vector128.LoadUnsafe(ref rowPtr, (nuint)x);
- Vector128 notEquals = ~Vector128.Equals(vec, trimmableVec);
- uint mask = notEquals.ExtractMostSignificantBits();
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)uint.TrailingZeroCount(mask);
- nint end = (nint)uint.LeadingZeroCount(mask) - Vector128.Count;
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector128.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector128.Count;
- }
- }
-#else
- if (Sse41.IsSupported && rowLength >= Vector128.Count)
+ // Reassign using the current frame and details.
+ if (metadata.LocalColorTable?.Length > 0)
{
- Vector256 trimmableVec256 = Vector256.Create(trimmableIndex);
+ // We can use the color data from the decoded metadata here.
+ // We avoid dithering by default to preserve the original colors.
+ ReadOnlyMemory palette = metadata.LocalColorTable.Value;
- if (Avx2.IsSupported && rowLength >= Vector256.Count)
+ if (hasDuplicates && !metadata.HasTransparency)
{
- do
- {
- Vector256 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x));
- Vector256 notEquals = Avx2.CompareEqual(vec, trimmableVec256);
- notEquals = Avx2.Xor(notEquals, Vector256.AllBitsSet);
- int mask = Avx2.MoveMask(notEquals);
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
- nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask);
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector256.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector256.Count;
- }
- while (x <= rowLength - Vector256.Count);
+ // A difference was captured but the metadata does not have transparency.
+ metadata.HasTransparency = true;
+ transparencyIndex = palette.Length;
+ metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
- Vector128 trimmableVec = Sse41.IsSupported
- ? trimmableVec256.GetLower()
- : Vector128.Create(trimmableIndex);
-
- while (x <= rowLength - Vector128.Count)
- {
- Vector128 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x));
- Vector128 notEquals = Sse2.CompareEqual(vec, trimmableVec);
- notEquals = Sse2.Xor(notEquals, Vector128.AllBitsSet);
- int mask = Sse2.MoveMask(notEquals);
-
- if (mask != 0)
- {
- isTransparentRow = false;
- nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask);
- nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask) - Vector128.Count;
-
- // end is from the end, but we need the index from the beginning
- end = x + Vector128.Count - 1 - end;
-
- left = Math.Min(left, (int)start);
- right = Math.Max(right, (int)end);
- }
-
- x += Vector128.Count;
- }
+ PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex);
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
}
-#endif
- for (; x < rowLength; ++x)
+ else
{
- if (Unsafe.Add(ref rowPtr, x) != trimmableIndex)
- {
- isTransparentRow = false;
- left = Math.Min(left, (int)x);
- right = Math.Max(right, (int)x);
- }
- }
+ // We must quantize the frame to generate a local color table.
+ IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree;
+ using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options);
+ quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, bounds);
- if (!isTransparentRow)
- {
- if (y == 0)
+ // The transparency index derived by the quantizer might differ from the index
+ // within the metadata. We need to update the metadata to reflect this.
+ int derivedTransparencyIndex = GetTransparentIndex(quantized, null);
+ if (derivedTransparencyIndex < 0)
{
- // First row is opaque.
- // Capture to prevent over assignment when a match is found below.
- top = 0;
+ // If no index is found set to the palette length, this trick allows us to fake transparency without an explicit index.
+ derivedTransparencyIndex = quantized.Palette.Length;
}
- // The minimum top bounds have already been captured.
- // Increment the bottom to include the current opaque row.
- if (minY < 0 && top != 0)
- {
- // Increment to the first opaque row.
- top++;
- }
+ metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
- minY = top;
- bottom = y;
- }
- else
- {
- // We've yet to hit an opaque row. Capture the top position.
- if (minY < 0)
+ if (hasDuplicates)
{
- top = Math.Max(top, y);
+ metadata.HasTransparency = true;
}
-
- bottom = Math.Min(bottom, y);
}
}
-
- if (left == int.MaxValue)
- {
- left = 0;
- }
-
- if (right == int.MinValue)
+ else
{
- right = buffer.Width;
- }
+ // Quantize the image using the global palette.
+ // Individual frames, though using the shared palette, can use a different transparent index to represent transparency.
- if (top == bottom || left == right)
- {
- // The entire image is transparent.
- return buffer.FullRectangle();
- }
+ // A difference was captured but the metadata does not have transparency.
+ if (hasDuplicates && !metadata.HasTransparency)
+ {
+ metadata.HasTransparency = true;
+ transparencyIndex = globalPaletteQuantizer.Palette.Length;
+ metadata.TransparencyIndex = ClampIndex(transparencyIndex);
+ }
- if (!isTransparentRow)
- {
- // Last row is opaque.
- bottom = buffer.Height;
+ globalPaletteQuantizer.SetTransparentIndex(transparencyIndex);
+ quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, bounds);
}
- return Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height));
+ return quantized;
}
+ private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
+
///
/// Returns the index of the most transparent color in the palette.
///
@@ -800,30 +630,19 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Writes the optional graphics control extension to the stream.
///
/// The metadata of the image or frame.
- /// The index of the color in the color palette to make transparent.
/// The stream to write to.
- private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream)
+ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, Stream stream)
{
- GifFrameMetadata? data = metadata;
- bool hasTransparency;
- if (metadata is null)
- {
- data = new();
- hasTransparency = transparencyIndex >= 0;
- }
- else
- {
- hasTransparency = metadata.HasTransparency;
- }
+ bool hasTransparency = metadata.HasTransparency;
byte packedValue = GifGraphicControlExtension.GetPackedValue(
- disposalMethod: data!.DisposalMethod,
+ disposalMethod: metadata.DisposalMethod,
transparencyFlag: hasTransparency);
GifGraphicControlExtension extension = new(
packed: packedValue,
- delayTime: (ushort)data.FrameDelay,
- transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue);
+ delayTime: (ushort)metadata.FrameDelay,
+ transparencyIndex: hasTransparency ? metadata.TransparencyIndex : byte.MinValue);
this.WriteExtension(extension, stream);
}
@@ -924,14 +743,11 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// Writes the image pixel data to the stream.
///
/// The containing indexed pixels.
- /// The region of interest.
/// The stream to write to.
/// The length of the frame color palette.
/// The index of the color used to represent transparency.
- private void WriteImageData(Buffer2D indices, Rectangle interest, Stream stream, int paletteLength, int transparencyIndex)
+ private void WriteImageData(Buffer2D indices, Stream stream, int paletteLength, int transparencyIndex)
{
- Buffer2DRegion region = indices.GetRegion(interest);
-
// Pad the bit depth when required for encoding the image data.
// This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette
// as decoders skip indexes that are out of range.
@@ -940,6 +756,6 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
: 0;
using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding));
- encoder.Encode(region, stream);
+ encoder.Encode(indices, stream);
}
}
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/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index 4b40c44e4..d4050810d 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable
///
/// The 2D buffer of indexed pixels.
/// The stream to write to.
- public void Encode(Buffer2DRegion indexedPixels, Stream stream)
+ public void Encode(Buffer2D indexedPixels, Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
@@ -204,7 +204,7 @@ internal sealed class LzwEncoder : IDisposable
/// The number of bits
/// See
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static int GetMaxcode(int bitCount) => (1 << bitCount) - 1;
+ private static int GetMaxCode(int bitCount) => (1 << bitCount) - 1;
///
/// Add a character to the end of the current packet, and if it is 254 characters,
@@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable
/// The 2D buffer of indexed pixels.
/// The initial bits.
/// The stream to write to.
- private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream)
+ private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream)
{
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = initialBits;
@@ -257,7 +257,7 @@ internal sealed class LzwEncoder : IDisposable
// Set up the necessary values
this.clearFlag = false;
this.bitCount = this.globalInitialBits;
- this.maxCode = GetMaxcode(this.bitCount);
+ this.maxCode = GetMaxCode(this.bitCount);
this.clearCode = 1 << (initialBits - 1);
this.eofCode = this.clearCode + 1;
this.freeEntry = this.clearCode + 2;
@@ -383,7 +383,7 @@ internal sealed class LzwEncoder : IDisposable
{
if (this.clearFlag)
{
- this.maxCode = GetMaxcode(this.bitCount = this.globalInitialBits);
+ this.maxCode = GetMaxCode(this.bitCount = this.globalInitialBits);
this.clearFlag = false;
}
else
@@ -391,7 +391,7 @@ internal sealed class LzwEncoder : IDisposable
++this.bitCount;
this.maxCode = this.bitCount == MaxBits
? MaxMaxCode
- : GetMaxcode(this.bitCount);
+ : GetMaxCode(this.bitCount);
}
}
diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs
index 9ba95952e..16f788e3d 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.
///
@@ -42,4 +58,39 @@ public static partial class MetadataExtensions
///
public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata)
=> source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
+
+ internal static AnimatedImageMetadata ToAnimatedImageMetadata(this GifMetadata source)
+ {
+ Color background = Color.Transparent;
+ if (source.GlobalColorTable != null)
+ {
+ background = source.GlobalColorTable.Value.Span[source.BackgroundColorIndex];
+ }
+
+ return new()
+ {
+ ColorTable = source.GlobalColorTable,
+ ColorTableMode = source.ColorTableMode == GifColorTableMode.Global ? FrameColorTableMode.Global : FrameColorTableMode.Local,
+ RepeatCount = source.RepeatCount,
+ BackgroundColor = background,
+ };
+ }
+
+ 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 = source.DisposalMethod == GifDisposalMethod.RestoreToBackground ? FrameBlendMode.Source : FrameBlendMode.Over,
+ };
+
+ 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/Chunks/PngPhysical.cs b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
index 784788248..8af0ac8ca 100644
--- a/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
+++ b/src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
@@ -61,10 +61,10 @@ internal readonly struct PngPhysical
/// The constructed PngPhysicalChunkData instance.
public static PngPhysical FromMetadata(ImageMetadata meta)
{
- byte unitSpecifier = 0;
uint x;
uint y;
+ byte unitSpecifier;
switch (meta.ResolutionUnits)
{
case PixelResolutionUnit.AspectRatio:
diff --git a/src/ImageSharp/Formats/Png/MetadataExtensions.cs b/src/ImageSharp/Formats/Png/MetadataExtensions.cs
index f24b8d1b5..b6313bffe 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,64 @@ 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);
+ public static PngFrameMetadata GetPngMetadata(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 TryGetPngMetadata(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)
+ {
+ double delay = source.FrameDelay.ToDouble();
+ if (double.IsNaN(delay))
+ {
+ delay = 0;
+ }
+
+ return new()
+ {
+ ColorTableMode = FrameColorTableMode.Global,
+ Duration = TimeSpan.FromMilliseconds(delay * 1000),
+ DisposalMode = GetMode(source.DisposalMethod),
+ BlendMode = source.BlendMethod == PngBlendMethod.Source ? FrameBlendMode.Source : FrameBlendMode.Over,
+ };
+ }
+
+ private static FrameDisposalMode GetMode(PngDisposalMethod method) => method switch
+ {
+ PngDisposalMethod.DoNotDispose => 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 64f1e00ff..7fb96241a 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -225,7 +225,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
chunk.Length - 4,
currentFrame,
pngMetadata,
- this.ReadNextDataChunkAndSkipSeq,
+ this.ReadNextFrameDataChunk,
currentFrameControl.Value,
cancellationToken);
@@ -601,7 +601,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
metadata);
}
- PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngFrameMetadata();
+ PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata();
frameMetadata.FromChunk(in frameControl);
this.bytesPerPixel = this.CalculateBytesPerPixel();
@@ -641,8 +641,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);
@@ -650,7 +650,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
pixelRegion.Clear();
}
- PngFrameMetadata frameMetadata = frame.Metadata.GetPngFrameMetadata();
+ PngFrameMetadata frameMetadata = frame.Metadata.GetPngMetadata();
frameMetadata.FromChunk(currentFrameControl);
this.previousScanline?.Dispose();
@@ -784,10 +784,12 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
cancellationToken.ThrowIfCancellationRequested();
int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1;
- Span scanlineSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
+ Span scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
+ Span prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline];
+
while (currentRowBytesRead < bytesPerFrameScanline)
{
- int bytesRead = compressedStream.Read(scanlineSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead);
+ int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead);
if (bytesRead <= 0)
{
return;
@@ -798,25 +800,25 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
currentRowBytesRead = 0;
- switch ((FilterType)scanlineSpan[0])
+ switch ((FilterType)scanSpan[0])
{
case FilterType.None:
break;
case FilterType.Sub:
- SubFilter.Decode(scanlineSpan, this.bytesPerPixel);
+ SubFilter.Decode(scanSpan, this.bytesPerPixel);
break;
case FilterType.Up:
- UpFilter.Decode(scanlineSpan, this.previousScanline.GetSpan());
+ UpFilter.Decode(scanSpan, prevSpan);
break;
case FilterType.Average:
- AverageFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel);
+ AverageFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel);
break;
case FilterType.Paeth:
- PaethFilter.Decode(scanlineSpan, this.previousScanline.GetSpan(), this.bytesPerPixel);
+ PaethFilter.Decode(scanSpan, prevSpan, this.bytesPerPixel);
break;
default:
@@ -829,7 +831,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, imageFrame, pngMetadata, blendRowBuffer);
+ this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer);
this.SwapScanlineBuffers();
currentRow++;
}
@@ -1751,19 +1753,34 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
}
///
- /// Reads the next data chunk and skip sequence number.
+ /// Reads the next animated frame data chunk.
///
/// Count of bytes in the next data chunk, or 0 if there are no more data chunks left.
- private int ReadNextDataChunkAndSkipSeq()
+ private int ReadNextFrameDataChunk()
{
- int length = this.ReadNextDataChunk();
- if (this.ReadNextDataChunk() is 0)
+ if (this.nextChunk != null)
+ {
+ return 0;
+ }
+
+ Span buffer = stackalloc byte[20];
+
+ _ = this.currentStream.Read(buffer, 0, 4);
+
+ if (this.TryReadChunk(buffer, out PngChunk chunk))
{
- return length;
+ if (chunk.Type is PngChunkType.FrameData)
+ {
+ chunk.Data?.Dispose();
+
+ this.currentStream.Position += 4; // Skip sequence number
+ return chunk.Length - 4;
+ }
+
+ this.nextChunk = chunk;
}
- this.currentStream.Position += 4; // Skip sequence number
- return length - 4;
+ return 0;
}
///
diff --git a/src/ImageSharp/Formats/Png/PngDisposalMethod.cs b/src/ImageSharp/Formats/Png/PngDisposalMethod.cs
index 17391de95..1537c5ced 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;
@@ -11,15 +11,15 @@ public enum PngDisposalMethod
///
/// No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
///
- None,
+ DoNotDispose,
///
/// 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..932916dec 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -7,8 +7,10 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
+using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
+using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@@ -116,6 +118,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
private IQuantizer? quantizer;
+ ///
+ /// Any explicit quantized transparent index provided by the background color.
+ ///
+ private int derivedTransparencyIndex = -1;
+
///
/// Initializes a new instance of the class.
///
@@ -137,7 +144,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The to encode the image data to.
/// The token to request cancellation.
public void Encode(Image image, Stream stream, CancellationToken cancellationToken)
- where TPixel : unmanaged, IPixel
+ where TPixel : unmanaged, IPixel
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
@@ -146,7 +153,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
- PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
+ PngMetadata pngMetadata = GetPngMetadata(image);
this.SanitizeAndSetEncoderOptions(this.encoder, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
stream.Write(PngConstants.HeaderBytes);
@@ -162,7 +169,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
}
// Do not move this. We require an accurate bit depth for the header chunk.
- IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, null);
+ IndexedImageFrame? quantized = this.CreateQuantizedImageAndUpdateBitDepth(
+ pngMetadata,
+ currentFrame,
+ currentFrame.Bounds(),
+ null);
this.WriteHeaderChunk(stream);
this.WriteGammaChunk(stream);
@@ -176,46 +187,64 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (image.Frames.Count > 1)
{
- this.WriteAnimationControlChunk(stream, 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
- // less data. See GifEncoder for the implementation there.
+ this.WriteAnimationControlChunk(stream, (uint)image.Frames.Count, pngMetadata.RepeatCount);
// Write the first frame.
- FrameControl frameControl = this.WriteFrameControlChunk(stream, currentFrame, 0);
- this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+ PngFrameMetadata frameMetadata = GetPngFrameMetadata(currentFrame);
+ PngDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
+ FrameControl frameControl = this.WriteFrameControlChunk(stream, frameMetadata, currentFrame.Bounds(), 0);
+ this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
// Capture the global palette for reuse on subsequent frames.
ReadOnlyMemory? previousPalette = quantized?.Palette.ToArray();
// Write following frames.
uint increment = 0;
+ ImageFrame previousFrame = image.Frames.RootFrame;
+
+ // This frame is reused to store de-duplicated pixel buffers.
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+
for (int i = 1; i < image.Frames.Count; i++)
{
+ ImageFrame? prev = previousDisposal == PngDisposalMethod.RestoreToBackground ? null : previousFrame;
currentFrame = image.Frames[i];
+ ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
+
+ frameMetadata = GetPngFrameMetadata(currentFrame);
+ bool blend = frameMetadata.BlendMethod == PngBlendMethod.Over;
+
+ (bool difference, Rectangle bounds) =
+ AnimationUtilities.DeDuplicatePixels(
+ image.Configuration,
+ prev,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ Color.Transparent,
+ blend);
+
if (clearTransparency)
{
- // Dispose of previous clone and reassign.
- clonedFrame?.Dispose();
- currentFrame = clonedFrame = currentFrame.Clone();
- ClearTransparentPixels(currentFrame);
+ ClearTransparentPixels(encodingFrame);
}
- // Each frame control sequence number must be incremented by the
- // number of frame data chunks that follow.
- frameControl = this.WriteFrameControlChunk(stream, currentFrame, (uint)i + increment);
+ // Each frame control sequence number must be incremented by the number of frame data chunks that follow.
+ frameControl = this.WriteFrameControlChunk(stream, frameMetadata, bounds, (uint)i + increment);
// Dispose of previous quantized frame and reassign.
quantized?.Dispose();
- quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, currentFrame, previousPalette);
- increment += this.WriteDataChunks(frameControl, currentFrame, quantized, stream, true);
+ quantized = this.CreateQuantizedImageAndUpdateBitDepth(pngMetadata, encodingFrame, bounds, previousPalette);
+ increment += this.WriteDataChunks(frameControl, encodingFrame.PixelBuffer.GetRegion(bounds), quantized, stream, true);
+
+ previousFrame = currentFrame;
+ previousDisposal = frameMetadata.DisposalMethod;
}
}
else
{
FrameControl frameControl = new((uint)this.width, (uint)this.height);
- this.WriteDataChunks(frameControl, currentFrame, quantized, stream, false);
+ this.WriteDataChunks(frameControl, currentFrame.PixelBuffer.GetRegion(), quantized, stream, false);
}
this.WriteEndChunk(stream);
@@ -234,6 +263,54 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
this.currentScanline?.Dispose();
}
+ private static PngMetadata GetPngMetadata(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
+ {
+ return (PngMetadata)png.DeepClone();
+ }
+
+ if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
+ {
+ AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
+ return PngMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
+ {
+ AnimatedImageMetadata ani = webp.ToAnimatedImageMetadata();
+ return PngMetadata.FromAnimatedMetadata(ani);
+ }
+
+ // Return explicit new instance so we do not mutate the original metadata.
+ return new();
+ }
+
+ private static PngFrameMetadata GetPngFrameMetadata(ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
+ {
+ return (PngFrameMetadata)png.DeepClone();
+ }
+
+ if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
+ {
+ AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
+ return PngFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
+ {
+ AnimatedImageFrameMetadata ani = webp.ToAnimatedImageFrameMetadata();
+ return PngFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ // Return explicit new instance so we do not mutate the original metadata.
+ return new();
+ }
+
///
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
///
@@ -267,15 +344,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The type of the pixel.
/// The image metadata.
/// The frame to quantize.
+ /// The area of interest within the frame.
/// Any previously derived palette.
/// The quantized image.
private IndexedImageFrame? CreateQuantizedImageAndUpdateBitDepth(
PngMetadata metadata,
ImageFrame frame,
+ Rectangle bounds,
ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
- IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, previousPalette);
+ IndexedImageFrame? quantized = this.CreateQuantizedFrame(this.encoder, this.colorType, this.bitDepth, metadata, frame, bounds, previousPalette);
this.bitDepth = CalculateBitDepth(this.colorType, this.bitDepth, quantized);
return quantized;
}
@@ -621,7 +700,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);
@@ -983,19 +1062,17 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Writes the animation control chunk to the stream.
///
/// The containing image data.
- /// The image frame.
+ /// The frame metadata.
+ /// The frame area of interest.
/// The frame sequence number.
- private FrameControl WriteFrameControlChunk(Stream stream, ImageFrame imageFrame, uint sequenceNumber)
+ private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata frameMetadata, Rectangle bounds, uint sequenceNumber)
{
- PngFrameMetadata frameMetadata = imageFrame.Metadata.GetPngFrameMetadata();
-
- // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
FrameControl fcTL = new(
sequenceNumber: sequenceNumber,
- width: (uint)imageFrame.Width,
- height: (uint)imageFrame.Height,
- xOffset: 0,
- yOffset: 0,
+ width: (uint)bounds.Width,
+ height: (uint)bounds.Height,
+ xOffset: (uint)bounds.Left,
+ yOffset: (uint)bounds.Top,
delayNumerator: (ushort)frameMetadata.FrameDelay.Numerator,
delayDenominator: (ushort)frameMetadata.FrameDelay.Denominator,
disposeOperation: frameMetadata.DisposalMethod,
@@ -1013,11 +1090,11 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
///
/// The pixel format.
/// The frame control
- /// The frame.
+ /// The image frame.
/// The quantized pixel data. Can be null.
/// The stream.
/// Is writing fdAT or IDAT.
- private uint WriteDataChunks(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, Stream stream, bool isFrame)
+ private uint WriteDataChunks(FrameControl frameControl, Buffer2DRegion frame, IndexedImageFrame? quantized, Stream stream, bool isFrame)
where TPixel : unmanaged, IPixel
{
byte[] buffer;
@@ -1031,16 +1108,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
if (quantized is not null)
{
- this.EncodeAdam7IndexedPixels(frameControl, quantized, deflateStream);
+ this.EncodeAdam7IndexedPixels(quantized, deflateStream);
}
else
{
- this.EncodeAdam7Pixels(frameControl, pixels, deflateStream);
+ this.EncodeAdam7Pixels(frame, deflateStream);
}
}
else
{
- this.EncodePixels(frameControl, pixels, quantized, deflateStream);
+ this.EncodePixels(frame, quantized, deflateStream);
}
}
@@ -1105,54 +1182,43 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Encodes the pixels.
///
/// The type of the pixel.
- /// The frame control
- /// The pixels.
- /// The quantized pixels span.
+ /// The image frame pixel buffer.
+ /// The quantized pixels.
/// The deflate stream.
- private void EncodePixels(FrameControl frameControl, ImageFrame pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
+ private void EncodePixels(Buffer2DRegion pixels, IndexedImageFrame? quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = (int)frameControl.Width;
- int height = (int)frameControl.Height;
-
- int bytesPerScanline = this.CalculateScanlineLength(width);
+ int bytesPerScanline = this.CalculateScanlineLength(pixels.Width);
int filterLength = bytesPerScanline + 1;
this.AllocateScanlineBuffers(bytesPerScanline);
using IMemoryOwner filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean);
- pixels.ProcessPixelRows(accessor =>
+ Span filter = filterBuffer.GetSpan();
+ Span attempt = attemptBuffer.GetSpan();
+ for (int y = 0; y < pixels.Height; y++)
{
- Span filter = filterBuffer.GetSpan();
- Span attempt = attemptBuffer.GetSpan();
- for (int y = (int)frameControl.YOffset; y < frameControl.YMax; y++)
- {
- this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y);
- deflateStream.Write(filter);
- this.SwapScanlineBuffers();
- }
- });
+ this.CollectAndFilterPixelRow(pixels.DangerousGetRowSpan(y), ref filter, ref attempt, quantized, y);
+ deflateStream.Write(filter);
+ this.SwapScanlineBuffers();
+ }
}
///
/// Interlaced encoding the pixels.
///
/// The type of the pixel.
- /// The frame control
- /// The image frame.
+ /// The image frame pixel buffer.
/// The deflate stream.
- private void EncodeAdam7Pixels(FrameControl frameControl, ImageFrame frame, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7Pixels(Buffer2DRegion pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = (int)frameControl.XMax;
- int height = (int)frameControl.YMax;
- Buffer2D pixelBuffer = frame.PixelBuffer;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
- int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
- int blockWidth = Adam7.ComputeBlockWidth(width, pass);
+ int startRow = Adam7.FirstRow[pass];
+ int startCol = Adam7.FirstColumn[pass];
+ int blockWidth = Adam7.ComputeBlockWidth(pixels.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8
@@ -1169,13 +1235,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
- for (int row = startRow; row < height; row += Adam7.RowIncrement[pass])
+ for (int row = startRow; row < pixels.Height; row += Adam7.RowIncrement[pass])
{
// Collect pixel data
- Span srcRow = pixelBuffer.DangerousGetRowSpan(row);
- for (int col = startCol, i = 0; col < frameControl.XMax; col += Adam7.ColumnIncrement[pass])
+ Span srcRow = pixels.DangerousGetRowSpan(row);
+ for (int col = startCol, i = 0; col < pixels.Width; col += Adam7.ColumnIncrement[pass], i++)
{
- block[i++] = srcRow[col];
+ block[i] = srcRow[col];
}
// Encode data
@@ -1193,19 +1259,16 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// Interlaced encoding the quantized (indexed, with palette) pixels.
///
/// The type of the pixel.
- /// The frame control
/// The quantized.
/// The deflate stream.
- private void EncodeAdam7IndexedPixels(FrameControl frameControl, IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
+ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel
{
- int width = (int)frameControl.Width;
- int endRow = (int)frameControl.YMax;
for (int pass = 0; pass < 7; pass++)
{
- int startRow = Adam7.FirstRow[pass] + (int)frameControl.YOffset;
- int startCol = Adam7.FirstColumn[pass] + (int)frameControl.XOffset;
- int blockWidth = Adam7.ComputeBlockWidth(width, pass);
+ int startRow = Adam7.FirstRow[pass];
+ int startCol = Adam7.FirstColumn[pass];
+ int blockWidth = Adam7.ComputeBlockWidth(quantized.Width, pass);
int bytesPerScanline = this.bytesPerPixel <= 1
? ((blockWidth * this.bitDepth) + 7) / 8
@@ -1223,16 +1286,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
Span filter = filterBuffer.GetSpan();
Span attempt = attemptBuffer.GetSpan();
- for (int row = startRow; row < endRow; row += Adam7.RowIncrement[pass])
+ for (int row = startRow; row < quantized.Height; row += Adam7.RowIncrement[pass])
{
// Collect data
ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row);
- for (int col = startCol, i = 0;
- col < frameControl.XMax;
- col += Adam7.ColumnIncrement[pass])
+ for (int col = startCol, i = 0; col < quantized.Width; col += Adam7.ColumnIncrement[pass], i++)
{
block[i] = srcRow[col];
- i++;
}
// Encode data
@@ -1404,6 +1464,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
/// The bits per component.
/// The image metadata.
/// The frame to quantize.
+ /// The frame area of interest.
/// Any previously derived palette.
private IndexedImageFrame? CreateQuantizedFrame(
QuantizingImageEncoder encoder,
@@ -1411,6 +1472,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
byte bitDepth,
PngMetadata metadata,
ImageFrame frame,
+ Rectangle bounds,
ReadOnlyMemory? previousPalette)
where TPixel : unmanaged, IPixel
{
@@ -1422,9 +1484,13 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
if (previousPalette is not null)
{
// Use the previously derived palette created by quantizing the root frame to quantize the current frame.
- using PaletteQuantizer paletteQuantizer = new(this.configuration, this.quantizer!.Options, previousPalette.Value, -1);
+ using PaletteQuantizer paletteQuantizer = new(
+ this.configuration,
+ this.quantizer!.Options,
+ previousPalette.Value,
+ this.derivedTransparencyIndex);
paletteQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
- return paletteQuantizer.QuantizeFrame(frame, frame.Bounds());
+ return paletteQuantizer.QuantizeFrame(frame, bounds);
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
@@ -1432,8 +1498,10 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
{
if (metadata.ColorTable is not null)
{
- // Use the provided palette. The caller is responsible for setting values.
- this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value);
+ // We can use the color data from the decoded metadata here.
+ // We avoid dithering by default to preserve the original colors.
+ this.derivedTransparencyIndex = metadata.ColorTable.Value.Span.IndexOf(Color.Transparent);
+ this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }, this.derivedTransparencyIndex);
}
else
{
@@ -1445,7 +1513,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(frame.Configuration);
frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame);
- return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
+ return frameQuantizer.QuantizeFrame(frame, bounds);
}
///
diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
index ca4d8c1f4..dbda4d73c 100644
--- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -34,7 +34,7 @@ public class PngFrameMetadata : IDeepCloneable
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
///
- public Rational FrameDelay { get; set; }
+ public Rational FrameDelay { get; set; } = new(0);
///
/// Gets or sets the type of frame area disposal to be done after rendering this frame
@@ -59,4 +59,20 @@ public class PngFrameMetadata : IDeepCloneable
///
public IDeepCloneable DeepClone() => new PngFrameMetadata(this);
+
+ internal static PngFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
+ => new()
+ {
+ FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
+ DisposalMethod = GetMode(metadata.DisposalMode),
+ BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? PngBlendMethod.Source : PngBlendMethod.Over,
+ };
+
+ private static PngDisposalMethod GetMode(FrameDisposalMode mode) => mode switch
+ {
+ FrameDisposalMode.RestoreToBackground => PngDisposalMethod.RestoreToBackground,
+ FrameDisposalMode.RestoreToPrevious => PngDisposalMethod.RestoreToPrevious,
+ FrameDisposalMode.DoNotDispose => PngDisposalMethod.DoNotDispose,
+ _ => PngDisposalMethod.DoNotDispose,
+ };
}
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index b113dbfc1..93ddcf263 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,8 +81,38 @@ 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; } = 1;
///
public IDeepCloneable DeepClone() => new PngMetadata(this);
+
+ internal static PngMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
+ {
+ // Should the conversion be from a format that uses a 24bit palette entries (gif)
+ // we need to clone and adjust the color table to allow for transparency.
+ Color[]? colorTable = metadata.ColorTable.HasValue ? metadata.ColorTable.Value.ToArray() : null;
+ if (colorTable != null)
+ {
+ for (int i = 0; i < colorTable.Length; i++)
+ {
+ ref Color c = ref colorTable[i];
+ if (c == metadata.BackgroundColor)
+ {
+ // Png treats background as fully empty
+ c = Color.Transparent;
+ break;
+ }
+ }
+ }
+
+ return new()
+ {
+ ColorType = colorTable != null ? PngColorType.Palette : null,
+ BitDepth = colorTable != null
+ ? (PngBitDepth)Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(colorTable.Length), 1, 8)
+ : null,
+ ColorTable = colorTable,
+ RepeatCount = metadata.RepeatCount,
+ };
+ }
}
diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
index cbd2aa8e7..46030dde3 100644
--- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
@@ -27,7 +27,7 @@ internal static class AlphaEncoder
/// The size in bytes of the alpha data.
/// The encoded alpha data.
public static IMemoryOwner EncodeAlpha(
- ImageFrame frame,
+ Buffer2DRegion frame,
Configuration configuration,
MemoryAllocator memoryAllocator,
bool skipMetadata,
@@ -35,8 +35,6 @@ internal static class AlphaEncoder
out int size)
where TPixel : unmanaged, IPixel
{
- int width = frame.Width;
- int height = frame.Height;
IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator);
if (compress)
@@ -46,8 +44,8 @@ internal static class AlphaEncoder
using Vp8LEncoder lossLessEncoder = new(
memoryAllocator,
configuration,
- width,
- height,
+ frame.Width,
+ frame.Height,
quality,
skipMetadata,
effort,
@@ -58,14 +56,14 @@ internal static class AlphaEncoder
// The transparency information will be stored in the green channel of the ARGB quadruplet.
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression.
- using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan());
+ using ImageFrame alphaAsFrame = DispatchAlphaToGreen(configuration, frame, alphaData.GetSpan());
- size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData);
+ size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame.PixelBuffer.GetRegion(), alphaData);
return alphaData;
}
- size = width * height;
+ size = frame.Width * frame.Height;
return alphaData;
}
@@ -73,25 +71,28 @@ internal static class AlphaEncoder
/// Store the transparency in the green channel.
///
/// The pixel format.
- /// The to encode from.
+ /// The configuration.
+ /// The pixel buffer to encode from.
/// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.
/// The transparency frame.
- private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData)
+ private static ImageFrame DispatchAlphaToGreen(Configuration configuration, Buffer2DRegion frame, Span alphaData)
where TPixel : unmanaged, IPixel
{
int width = frame.Width;
int height = frame.Height;
- ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height);
+ ImageFrame alphaAsFrame = new(configuration, width, height);
for (int y = 0; y < height; y++)
{
- Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y);
- Span pixelRow = rowBuffer.Span;
+ Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y);
+ Span pixelRow = rowBuffer.Span;
Span alphaRow = alphaData.Slice(y * width, width);
+
+ // TODO: This can be probably simd optimized.
for (int x = 0; x < width; x++)
{
// Leave A/R/B channels zero'd.
- pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0);
+ pixelRow[x] = new Bgra32(0, alphaRow[x], 0, 0);
}
}
@@ -106,12 +107,12 @@ internal static class AlphaEncoder
/// The global configuration.
/// The memory manager.
/// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.
- private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator)
+ private static IMemoryOwner ExtractAlphaChannel(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = frame.PixelBuffer;
- int height = frame.Height;
int width = frame.Width;
+ int height = frame.Height;
+
IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height);
Span alphaData = alphaDataBuffer.GetSpan();
@@ -120,7 +121,7 @@ internal static class AlphaEncoder
for (int y = 0; y < height; y++)
{
- Span rowSpan = imageBuffer.DangerousGetRowSpan(y);
+ Span rowSpan = frame.DangerousGetRowSpan(y);
PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow);
int offset = y * width;
for (int x = 0; x < width; x++)
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index d502fd606..9ffda0f51 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Diagnostics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -100,9 +99,7 @@ internal abstract class BitWriterBase
bool hasAnimation)
{
// Write file size later
- long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
-
- Debug.Assert(pos is 4, "Stream should be written from position 0.");
+ RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
// Write VP8X, header if necessary.
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
@@ -160,7 +157,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..5ed7aab1e 100644
--- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
@@ -12,7 +12,7 @@ internal readonly struct WebpFrameData
///
public const uint HeaderSize = 16;
- public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod)
+ public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod)
{
this.DataSize = dataSize;
this.X = x;
@@ -32,12 +32,12 @@ internal readonly struct WebpFrameData
width,
height,
duration,
- (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending,
- (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose)
+ (flags & 2) == 0 ? WebpBlendMethod.Over : WebpBlendMethod.Source,
+ (flags & 1) == 1 ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose)
{
}
- public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod)
+ public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendMethod blendingMethod, WebpDisposalMethod disposalMethod)
: this(0, x, y, width, height, duration, blendingMethod, disposalMethod)
{
}
@@ -76,14 +76,14 @@ internal readonly struct WebpFrameData
///
/// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
///
- public WebpBlendingMethod BlendingMethod { get; }
+ public WebpBlendMethod BlendingMethod { get; }
///
/// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
///
public WebpDisposalMethod DisposalMethod { get; }
- public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height);
+ public Rectangle Bounds => new((int)this.X, (int)this.Y, (int)this.Width, (int)this.Height);
///
/// Writes the animation frame() to the stream.
@@ -93,13 +93,13 @@ internal readonly struct WebpFrameData
{
byte flags = 0;
- if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend)
+ if (this.BlendingMethod is WebpBlendMethod.Source)
{
// Set blending flag.
flags |= 2;
}
- if (this.DisposalMethod is WebpDisposalMethod.Dispose)
+ if (this.DisposalMethod is WebpDisposalMethod.RestoreToBackground)
{
// Set disposal flag.
flags |= 1;
@@ -107,8 +107,8 @@ internal readonly struct WebpFrameData
long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData);
- WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X);
- WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, (uint)Math.Round(this.X / 2f));
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, (uint)Math.Round(this.Y / 2f));
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration);
@@ -128,8 +128,8 @@ internal readonly struct WebpFrameData
WebpFrameData data = new(
dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
- x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
- y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
+ x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2,
+ y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) * 2,
width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 4fdbb31d3..518c09ff4 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -240,7 +240,7 @@ internal class Vp8LEncoder : IDisposable
public void EncodeHeader(Image image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
- // Write bytes from the bitwriter buffer to the stream.
+ // Write bytes from the bit-writer buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
@@ -259,15 +259,15 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
- WebpMetadata webpMetadata = metadata.GetWebpMetadata();
- BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
public void EncodeFooter(Image image, Stream stream)
where TPixel : unmanaged, IPixel
{
- // Write bytes from the bitwriter buffer to the stream.
+ // Write bytes from the bit-writer buffer to the stream.
ImageMetadata metadata = image.Metadata;
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
@@ -280,26 +280,25 @@ internal class Vp8LEncoder : IDisposable
/// Encodes the image as lossless webp to the specified stream.
///
/// The pixel format.
- /// The to encode from.
+ /// The image frame to encode from.
+ /// The region of interest within the frame to encode.
+ /// The frame metadata.
/// The to encode the image data to.
/// Flag indicating, if an animation parameter is present.
- public void Encode(ImageFrame frame, Stream stream, bool hasAnimation)
+ public void Encode(ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
- int width = frame.Width;
- int height = frame.Height;
-
// Convert image pixels to bgra array.
- bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height);
+ bool hasAlpha = this.ConvertPixelsToBgra(frame.PixelBuffer.GetRegion(bounds));
// Write the image size.
- this.WriteImageSize(width, height);
+ this.WriteImageSize(bounds.Width, bounds.Height);
// Write the non-trivial Alpha flag and lossless version.
this.WriteAlphaAndVersion(hasAlpha);
// Encode the main image stream.
- this.EncodeStream(frame);
+ this.EncodeStream(bounds.Width, bounds.Height);
this.bitWriter.Finish();
@@ -307,21 +306,18 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
- WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
-
- // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(
- 0,
- 0,
- (uint)frame.Width,
- (uint)frame.Height,
+ (uint)bounds.Left,
+ (uint)bounds.Top,
+ (uint)bounds.Width,
+ (uint)bounds.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
.WriteHeaderTo(stream);
}
- // Write bytes from the bitwriter buffer to the stream.
+ // Write bytes from the bit-writer buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream);
if (hasAnimation)
@@ -334,12 +330,12 @@ internal class Vp8LEncoder : IDisposable
/// Encodes the alpha image data using the webp lossless compression.
///
/// The type of the pixel.
- /// The to encode from.
+ /// The alpha-pixel data to encode from.
/// The destination buffer to write the encoded alpha data to.
/// The size of the compressed data in bytes.
/// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed.
///
- public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData)
+ public int EncodeAlphaImageData(Buffer2DRegion frame, IMemoryOwner alphaData)
where TPixel : unmanaged, IPixel
{
int width = frame.Width;
@@ -347,10 +343,10 @@ internal class Vp8LEncoder : IDisposable
int pixelCount = width * height;
// Convert image pixels to bgra array.
- this.ConvertPixelsToBgra(frame, width, height);
+ this.ConvertPixelsToBgra(frame);
// The image-stream will NOT contain any headers describing the image dimension, the dimension is already known.
- this.EncodeStream(frame);
+ this.EncodeStream(width, height);
this.bitWriter.Finish();
int size = this.bitWriter.NumBytes;
if (size >= pixelCount)
@@ -364,7 +360,7 @@ internal class Vp8LEncoder : IDisposable
}
///
- /// Writes the image size to the bitwriter buffer.
+ /// Writes the image size to the bit writer buffer.
///
/// The input image width.
/// The input image height.
@@ -381,7 +377,7 @@ internal class Vp8LEncoder : IDisposable
}
///
- /// Writes a flag indicating if alpha channel is used and the VP8L version to the bitwriter buffer.
+ /// Writes a flag indicating if alpha channel is used and the VP8L version to the bit-writer buffer.
///
/// Indicates if a alpha channel is present.
private void WriteAlphaAndVersion(bool hasAlpha)
@@ -393,14 +389,10 @@ internal class Vp8LEncoder : IDisposable
///
/// Encodes the image stream using lossless webp format.
///
- /// The pixel type.
- /// The frame to encode.
- private void EncodeStream(ImageFrame frame)
- where TPixel : unmanaged, IPixel
+ /// The image frame width.
+ /// The image frame height.
+ private void EncodeStream(int width, int height)
{
- int width = frame.Width;
- int height = frame.Height;
-
Span bgra = this.Bgra.GetSpan();
Span encodedData = this.EncodedData.GetSpan();
bool lowEffort = this.method == 0;
@@ -508,23 +500,20 @@ internal class Vp8LEncoder : IDisposable
/// Converts the pixels of the image to bgra.
///
/// The type of the pixels.
- /// The frame to convert.
- /// The width of the image.
- /// The height of the image.
+ /// The frame pixel buffer to convert.
/// true, if the image is non opaque.
- private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height)
+ private bool ConvertPixelsToBgra(Buffer2DRegion pixels)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = frame.PixelBuffer;
bool nonOpaque = false;
Span bgra = this.Bgra.GetSpan();
Span bgraBytes = MemoryMarshal.Cast(bgra);
- int widthBytes = width * 4;
- for (int y = 0; y < height; y++)
+ int widthBytes = pixels.Width * 4;
+ for (int y = 0; y < pixels.Height; y++)
{
- Span rowSpan = imageBuffer.DangerousGetRowSpan(y);
+ Span rowSpan = pixels.DangerousGetRowSpan(y);
Span rowBytes = bgraBytes.Slice(y * widthBytes, widthBytes);
- PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, width);
+ PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, pixels.Width);
if (!nonOpaque)
{
Span rowBgra = MemoryMarshal.Cast(rowBytes);
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 98e50bb9c..2b74c300a 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -333,8 +333,8 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
- WebpMetadata webpMetadata = metadata.GetWebpMetadata();
- BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.BackgroundColor, webpMetadata.RepeatCount);
}
}
@@ -351,44 +351,53 @@ internal class Vp8Encoder : IDisposable
}
///
- /// Encodes the image to the specified stream from the .
+ /// Encodes the animated image frame to the specified stream.
///
/// The pixel format.
- /// The to encode from.
- /// The to encode the image data to.
- public void EncodeAnimation(ImageFrame frame, Stream stream)
+ /// The image frame to encode from.
+ /// The stream to encode the image data to.
+ /// The region of interest within the frame to encode.
+ /// The frame metadata.
+ public void EncodeAnimation(ImageFrame frame, Stream stream, Rectangle bounds, WebpFrameMetadata frameMetadata)
where TPixel : unmanaged, IPixel =>
- this.Encode(frame, stream, true, null);
+ this.Encode(stream, frame, bounds, frameMetadata, true, null);
///
- /// Encodes the image to the specified stream from the .
+ /// Encodes the static image frame to the specified stream.
///
/// The pixel format.
- /// The to encode from.
- /// The to encode the image data to.
- public void EncodeStatic(Image image, Stream stream)
- where TPixel : unmanaged, IPixel =>
- this.Encode(image.Frames.RootFrame, stream, false, image);
+ /// The stream to encode the image data to.
+ /// The image to encode from.
+ public void EncodeStatic(Stream stream, Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ ImageFrame frame = image.Frames.RootFrame;
+ this.Encode(stream, frame, image.Bounds, WebpCommonUtils.GetWebpFrameMetadata(frame), false, image);
+ }
///
- /// Encodes the image to the specified stream from the .
+ /// Encodes the image to the specified stream.
///
/// The pixel format.
- /// The to encode from.
- /// The to encode the image data to.
+ /// The stream to encode the image data to.
+ /// The image frame to encode from.
+ /// The region of interest within the frame to encode.
+ /// The frame metadata.
/// Flag indicating, if an animation parameter is present.
- /// The to encode from.
- private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image)
+ /// The image to encode from.
+ private void Encode(Stream stream, ImageFrame frame, Rectangle bounds, WebpFrameMetadata frameMetadata, bool hasAnimation, Image image)
where TPixel : unmanaged, IPixel
{
- int width = frame.Width;
- int height = frame.Height;
+ int width = bounds.Width;
+ int height = bounds.Height;
int pixelCount = width * height;
Span y = this.Y.GetSpan();
Span u = this.U.GetSpan();
Span v = this.V.GetSpan();
- bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v);
+
+ Buffer2DRegion pixels = frame.PixelBuffer.GetRegion(bounds);
+ bool hasAlpha = YuvConversion.ConvertRgbToYuv(pixels, this.configuration, this.memoryAllocator, y, u, v);
if (!hasAnimation)
{
@@ -456,7 +465,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
- frame,
+ pixels,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@@ -477,14 +486,11 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
- WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
-
- // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(
- 0,
- 0,
- (uint)frame.Width,
- (uint)frame.Height,
+ (uint)bounds.X,
+ (uint)bounds.Y,
+ (uint)bounds.Width,
+ (uint)bounds.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
index d669a37b7..f8e664ed0 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
@@ -259,7 +259,7 @@ internal static class YuvConversion
}
///
- /// Converts the RGB values of the image to YUV.
+ /// Converts the pixel values of the image to YUV.
///
/// The pixel type of the image.
/// The frame to convert.
@@ -269,12 +269,11 @@ internal static class YuvConversion
/// Span to store the u component of the image.
/// Span to store the v component of the image.
/// true, if the image contains alpha data.
- public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v)
+ public static bool ConvertRgbToYuv(Buffer2DRegion frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = frame.PixelBuffer;
- int width = imageBuffer.Width;
- int height = imageBuffer.Height;
+ int width = frame.Width;
+ int height = frame.Height;
int uvWidth = (width + 1) >> 1;
// Temporary storage for accumulated R/G/B values during conversion to U/V.
@@ -289,8 +288,8 @@ internal static class YuvConversion
bool hasAlpha = false;
for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2)
{
- Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex);
- Span nextRowSpan = imageBuffer.DangerousGetRowSpan(rowIndex + 1);
+ Span rowSpan = frame.DangerousGetRowSpan(rowIndex);
+ Span nextRowSpan = frame.DangerousGetRowSpan(rowIndex + 1);
PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0);
PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1);
@@ -320,7 +319,7 @@ internal static class YuvConversion
// Extra last row.
if ((height & 1) != 0)
{
- Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex);
+ Span rowSpan = frame.DangerousGetRowSpan(rowIndex);
PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0);
ConvertRgbaToY(bgraRow0, y[(rowIndex * width)..], width);
diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
index 7f0920f2d..731d3f1ff 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 == WebpBlendMethod.Over ? FrameBlendMode.Over : FrameBlendMode.Source,
+ };
+
+ private static FrameDisposalMode GetMode(WebpDisposalMethod method) => method switch
+ {
+ WebpDisposalMethod.RestoreToBackground => FrameDisposalMode.RestoreToBackground,
+ WebpDisposalMethod.DoNotDispose => FrameDisposalMode.DoNotDispose,
+ _ => FrameDisposalMode.DoNotDispose,
+ };
}
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index 66e69d9a4..65f1a4da4 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 == WebpBlendMethod.Over;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
@@ -253,7 +253,7 @@ internal class WebpAnimationDecoder : IDisposable
private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo)
where TPixel : unmanaged, IPixel
{
- ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height);
+ ImageFrame decodedFrame = new(this.configuration, (int)frameData.Width, (int)frameData.Height);
try
{
diff --git a/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs
similarity index 88%
rename from src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
rename to src/ImageSharp/Formats/Webp/WebpBlendMethod.cs
index cbd0e9a8c..f16f7650c 100644
--- a/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpBlendMethod.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -6,17 +6,17 @@ namespace SixLabors.ImageSharp.Formats.Webp;
///
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
///
-public enum WebpBlendingMethod
+public enum WebpBlendMethod
{
///
- /// 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/WebpCommonUtils.cs b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
index 1a8fcbafc..49482260b 100644
--- a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
+++ b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs
@@ -4,6 +4,8 @@
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
+using SixLabors.ImageSharp.Formats.Gif;
+using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -13,6 +15,54 @@ namespace SixLabors.ImageSharp.Formats.Webp;
///
internal static class WebpCommonUtils
{
+ public static WebpMetadata GetWebpMetadata(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ if (image.Metadata.TryGetWebpMetadata(out WebpMetadata? webp))
+ {
+ return (WebpMetadata)webp.DeepClone();
+ }
+
+ if (image.Metadata.TryGetGifMetadata(out GifMetadata? gif))
+ {
+ AnimatedImageMetadata ani = gif.ToAnimatedImageMetadata();
+ return WebpMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (image.Metadata.TryGetPngMetadata(out PngMetadata? png))
+ {
+ AnimatedImageMetadata ani = png.ToAnimatedImageMetadata();
+ return WebpMetadata.FromAnimatedMetadata(ani);
+ }
+
+ // Return explicit new instance so we do not mutate the original metadata.
+ return new();
+ }
+
+ public static WebpFrameMetadata GetWebpFrameMetadata(ImageFrame frame)
+ where TPixel : unmanaged, IPixel
+ {
+ if (frame.Metadata.TryGetWebpFrameMetadata(out WebpFrameMetadata? webp))
+ {
+ return (WebpFrameMetadata)webp.DeepClone();
+ }
+
+ if (frame.Metadata.TryGetGifMetadata(out GifFrameMetadata? gif))
+ {
+ AnimatedImageFrameMetadata ani = gif.ToAnimatedImageFrameMetadata();
+ return WebpFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ if (frame.Metadata.TryGetPngMetadata(out PngFrameMetadata? png))
+ {
+ AnimatedImageFrameMetadata ani = png.ToAnimatedImageFrameMetadata();
+ return WebpFrameMetadata.FromAnimatedMetadata(ani);
+ }
+
+ // Return explicit new instance so we do not mutate the original metadata.
+ return new();
+ }
+
///
/// Checks if the pixel row is not opaque.
///
@@ -27,7 +77,7 @@ internal static class WebpCommonUtils
int length = (row.Length * 4) - 3;
fixed (byte* src = rowBytes)
{
- var alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
+ Vector256 alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector256 all0x80Vector256 = Vector256.Create((byte)0x80).AsByte();
for (; i + 128 <= length; i += 128)
@@ -124,7 +174,7 @@ internal static class WebpCommonUtils
private static unsafe bool IsNoneOpaque64Bytes(byte* src, int i)
{
- var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
+ Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128 a0 = Sse2.LoadVector128(src + i).AsByte();
Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte();
@@ -144,7 +194,7 @@ internal static class WebpCommonUtils
private static unsafe bool IsNoneOpaque32Bytes(byte* src, int i)
{
- var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
+ Vector128 alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255);
Vector128 a0 = Sse2.LoadVector128(src + i).AsByte();
Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte();
diff --git a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
index d409973a9..47cc83951 100644
--- a/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
@@ -16,5 +16,5 @@ public enum WebpDisposalMethod
///
/// 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/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index 47712071b..e37c1d179 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -123,12 +123,14 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
}
else
{
- WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata();
+ WebpMetadata webpMetadata = WebpCommonUtils.GetWebpMetadata(image);
lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless;
}
if (lossless)
{
+ bool hasAnimation = image.Frames.Count > 1;
+
using Vp8LEncoder encoder = new(
this.memoryAllocator,
this.configuration,
@@ -141,17 +143,46 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.nearLossless,
this.nearLosslessQuality);
- bool hasAnimation = image.Frames.Count > 1;
encoder.EncodeHeader(image, stream, hasAnimation);
+
+ // Encode the first frame.
+ ImageFrame previousFrame = image.Frames.RootFrame;
+ WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame);
+ encoder.Encode(previousFrame, previousFrame.Bounds(), frameMetadata, stream, hasAnimation);
+
if (hasAnimation)
{
- foreach (ImageFrame imageFrame in image.Frames)
+ WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
+
+ // Encode additional frames
+ // This frame is reused to store de-duplicated pixel buffers.
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+
+ for (int i = 1; i < image.Frames.Count; i++)
{
- using Vp8LEncoder enc = new(
+ ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
+ ImageFrame currentFrame = image.Frames[i];
+ ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
+
+ frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
+ bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
+
+ (bool difference, Rectangle bounds) =
+ AnimationUtilities.DeDuplicatePixels(
+ image.Configuration,
+ prev,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ Color.Transparent,
+ blend,
+ ClampingMode.Even);
+
+ using Vp8LEncoder animatedEncoder = new(
this.memoryAllocator,
this.configuration,
- image.Width,
- image.Height,
+ bounds.Width,
+ bounds.Height,
this.quality,
this.skipMetadata,
this.method,
@@ -159,13 +190,12 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.nearLossless,
this.nearLosslessQuality);
- enc.Encode(imageFrame, stream, true);
+ animatedEncoder.Encode(encodingFrame, bounds, frameMetadata, stream, hasAnimation);
+
+ previousFrame = currentFrame;
+ previousDisposal = frameMetadata.DisposalMethod;
}
}
- else
- {
- encoder.Encode(image.Frames.RootFrame, stream, false);
- }
encoder.EncodeFooter(image, stream);
}
@@ -183,17 +213,48 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
+
if (image.Frames.Count > 1)
{
+ // TODO: What about alpha here?
encoder.EncodeHeader(image, stream, false, true);
- foreach (ImageFrame imageFrame in image.Frames)
+ // Encode the first frame.
+ ImageFrame previousFrame = image.Frames.RootFrame;
+ WebpFrameMetadata frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(previousFrame);
+ WebpDisposalMethod previousDisposal = frameMetadata.DisposalMethod;
+
+ encoder.EncodeAnimation(previousFrame, stream, previousFrame.Bounds(), frameMetadata);
+
+ // Encode additional frames
+ // This frame is reused to store de-duplicated pixel buffers.
+ using ImageFrame encodingFrame = new(image.Configuration, previousFrame.Size());
+
+ for (int i = 1; i < image.Frames.Count; i++)
{
- using Vp8Encoder enc = new(
+ ImageFrame? prev = previousDisposal == WebpDisposalMethod.RestoreToBackground ? null : previousFrame;
+ ImageFrame currentFrame = image.Frames[i];
+ ImageFrame? nextFrame = i < image.Frames.Count - 1 ? image.Frames[i + 1] : null;
+
+ frameMetadata = WebpCommonUtils.GetWebpFrameMetadata(currentFrame);
+ bool blend = frameMetadata.BlendMethod == WebpBlendMethod.Over;
+
+ (bool difference, Rectangle bounds) =
+ AnimationUtilities.DeDuplicatePixels(
+ image.Configuration,
+ prev,
+ currentFrame,
+ nextFrame,
+ encodingFrame,
+ Color.Transparent,
+ blend,
+ ClampingMode.Even);
+
+ using Vp8Encoder animatedEncoder = new(
this.memoryAllocator,
this.configuration,
- image.Width,
- image.Height,
+ bounds.Width,
+ bounds.Height,
this.quality,
this.skipMetadata,
this.method,
@@ -202,12 +263,15 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.spatialNoiseShaping,
this.alphaCompression);
- enc.EncodeAnimation(imageFrame, stream);
+ animatedEncoder.EncodeAnimation(encodingFrame, stream, bounds, frameMetadata);
+
+ previousFrame = currentFrame;
+ previousDisposal = frameMetadata.DisposalMethod;
}
}
else
{
- encoder.EncodeStatic(image, stream);
+ encoder.EncodeStatic(stream, image);
}
encoder.EncodeFooter(image, stream);
diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
index ef21d8b6f..422ad6bc7 100644
--- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
@@ -29,7 +29,7 @@ public class WebpFrameMetadata : IDeepCloneable
///
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
///
- public WebpBlendingMethod BlendMethod { get; set; }
+ public WebpBlendMethod BlendMethod { get; set; }
///
/// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
@@ -44,4 +44,12 @@ public class WebpFrameMetadata : IDeepCloneable
///
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);
+
+ internal static WebpFrameMetadata FromAnimatedMetadata(AnimatedImageFrameMetadata metadata)
+ => new()
+ {
+ FrameDelay = (uint)metadata.Duration.Milliseconds,
+ BlendMethod = metadata.BlendMode == FrameBlendMode.Source ? WebpBlendMethod.Source : WebpBlendMethod.Over,
+ DisposalMethod = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground ? WebpDisposalMethod.RestoreToBackground : WebpDisposalMethod.DoNotDispose
+ };
}
diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
index a6bb0a7b8..536ea0929 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,16 +34,24 @@ 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);
+
+ internal static WebpMetadata FromAnimatedMetadata(AnimatedImageMetadata metadata)
+ => new()
+ {
+ FileFormat = WebpFileFormatType.Lossless,
+ BackgroundColor = metadata.BackgroundColor,
+ RepeatCount = metadata.RepeatCount
+ };
}
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/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
index 754aac90e..3c56d0243 100644
--- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
+++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
@@ -107,15 +107,15 @@ public readonly partial struct ErrorDither : IDither, IEquatable, I
float scale = quantizer.Options.DitherScale;
Buffer2D sourceBuffer = source.PixelBuffer;
- for (int y = bounds.Top; y < bounds.Bottom; y++)
+ for (int y = 0; y < destination.Height; y++)
{
- ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(sourceBuffer.DangerousGetRowSpan(y));
- ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
+ ReadOnlySpan