diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index d17e89cd45..2932cafe24 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -94,7 +94,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
///
/// Gets the dimensions of the image.
///
- public Size Dimensions => new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
+ public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height);
private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator;
diff --git a/src/ImageSharp/Formats/Gif/GifFormat.cs b/src/ImageSharp/Formats/Gif/GifFormat.cs
index 459f0068be..fcb0fe5b3f 100644
--- a/src/ImageSharp/Formats/Gif/GifFormat.cs
+++ b/src/ImageSharp/Formats/Gif/GifFormat.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
@@ -17,7 +17,7 @@ private GifFormat()
///
/// Gets the current instance.
///
- public static GifFormat Instance { get; } = new GifFormat();
+ public static GifFormat Instance { get; } = new();
///
public string Name => "GIF";
@@ -32,9 +32,9 @@ private GifFormat()
public IEnumerable FileExtensions => GifConstants.FileExtensions;
///
- public GifMetadata CreateDefaultFormatMetadata() => new GifMetadata();
+ public GifMetadata CreateDefaultFormatMetadata() => new();
///
- public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new GifFrameMetadata();
+ public GifFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
}
diff --git a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
new file mode 100644
index 0000000000..643c1959ae
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ ///
+ /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
+ ///
+ internal enum AnimationBlendingMethod
+ {
+ ///
+ /// 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.
+ ///
+ AlphaBlending = 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.
+ ///
+ DoNotBlend = 1
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
new file mode 100644
index 0000000000..f6beebf757
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ ///
+ /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
+ ///
+ internal enum AnimationDisposalMethod
+ {
+ ///
+ /// Do not dispose. Leave the canvas as is.
+ ///
+ DoNotDispose = 0,
+
+ ///
+ /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.
+ ///
+ Dispose = 1
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs
new file mode 100644
index 0000000000..ffb1ddc1f6
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ internal struct AnimationFrameData
+ {
+ ///
+ /// The animation chunk size.
+ ///
+ public uint DataSize;
+
+ ///
+ /// The X coordinate of the upper left corner of the frame is Frame X * 2.
+ ///
+ public uint X;
+
+ ///
+ /// The Y coordinate of the upper left corner of the frame is Frame Y * 2.
+ ///
+ public uint Y;
+
+ ///
+ /// The width of the frame.
+ ///
+ public uint Width;
+
+ ///
+ /// The height of the frame.
+ ///
+ public uint Height;
+
+ ///
+ /// The time to wait before displaying the next frame, in 1 millisecond units.
+ /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined.
+ ///
+ public uint Duration;
+
+ ///
+ /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
+ ///
+ public AnimationBlendingMethod BlendingMethod;
+
+ ///
+ /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
+ ///
+ public AnimationDisposalMethod DisposalMethod;
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs
index 7bd78da3da..cf607ef69f 100644
--- a/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Metadata;
+
namespace SixLabors.ImageSharp.Formats.Webp
{
///
@@ -12,5 +14,10 @@ internal interface IWebpDecoderOptions
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
///
bool IgnoreMetadata { get; }
+
+ ///
+ /// Gets the decoding mode for multi-frame images.
+ ///
+ FrameDecodingMode DecodingMode { get; }
}
}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
index f517ad520f..2d2396f1a5 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
@@ -87,7 +87,7 @@ public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAlloca
private static ReadOnlySpan LiteralMap => new byte[] { 0, 1, 1, 1, 0 };
///
- /// Decodes the image from the stream using the bitreader.
+ /// Decodes the lossless webp image from the stream.
///
/// The pixel format.
/// The pixel buffer to store the decoded data.
diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
index b74f6969e1..d374393e9a 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
@@ -57,7 +57,16 @@ public WebpLossyDecoder(Vp8BitReader bitReader, MemoryAllocator memoryAllocator,
this.configuration = configuration;
}
- public void Decode(Buffer2D pixels, int width, int height, WebpImageInfo info)
+ ///
+ /// Decodes the lossless webp image from the stream.
+ ///
+ /// The pixel format.
+ /// The pixel buffer to store the decoded data.
+ /// The width of the image.
+ /// The height of the image.
+ /// Information about the image.
+ /// The ALPH chunk data.
+ public void Decode(Buffer2D pixels, int width, int height, WebpImageInfo info, IMemoryOwner alphaData)
where TPixel : unmanaged, IPixel
{
// Paragraph 9.2: color space and clamp type follow.
@@ -105,7 +114,7 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI
using (var alphaDecoder = new AlphaDecoder(
width,
height,
- info.Features.AlphaData,
+ alphaData,
info.Features.AlphaChunkHeader,
this.memoryAllocator,
this.configuration))
diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
index 63f8e3427e..3a85b5441f 100644
--- a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs
@@ -17,5 +17,12 @@ public static partial class MetadataExtensions
/// The metadata this method extends.
/// The .
public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance);
+
+ ///
+ /// 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);
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
new file mode 100644
index 0000000000..09653fd4cd
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -0,0 +1,386 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Webp.Lossless;
+using SixLabors.ImageSharp.Formats.Webp.Lossy;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ ///
+ /// Decoder for animated webp images.
+ ///
+ internal class WebpAnimationDecoder : IDisposable
+ {
+ ///
+ /// Reusable buffer.
+ ///
+ private readonly byte[] buffer = new byte[4];
+
+ ///
+ /// Used for allocating memory during the decoding operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// The area to restore.
+ ///
+ private Rectangle? restoreArea;
+
+ ///
+ /// The abstract metadata.
+ ///
+ private ImageMetadata metadata;
+
+ ///
+ /// The gif specific metadata.
+ ///
+ private WebpMetadata webpMetadata;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The memory allocator.
+ /// The global configuration.
+ /// The frame decoding mode.
+ public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, FrameDecodingMode decodingMode)
+ {
+ this.memoryAllocator = memoryAllocator;
+ this.configuration = configuration;
+ this.DecodingMode = decodingMode;
+ }
+
+ ///
+ /// Gets or sets the alpha data, if an ALPH chunk is present.
+ ///
+ public IMemoryOwner AlphaData { get; set; }
+
+ ///
+ /// Gets the decoding mode for multi-frame images.
+ ///
+ public FrameDecodingMode DecodingMode { get; }
+
+ ///
+ /// Decodes the animated webp image from the specified stream.
+ ///
+ /// The pixel format.
+ /// The stream, where the image should be decoded from. Cannot be null.
+ /// The webp features.
+ /// The width of the image.
+ /// The height of the image.
+ /// The size of the image data in bytes.
+ public Image Decode(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize)
+ where TPixel : unmanaged, IPixel
+ {
+ Image image = null;
+ ImageFrame previousFrame = null;
+
+ this.metadata = new ImageMetadata();
+ this.webpMetadata = this.metadata.GetWebpMetadata();
+ this.webpMetadata.AnimationLoopCount = features.AnimationLoopCount;
+
+ int remainingBytes = (int)completeDataSize;
+ while (remainingBytes > 0)
+ {
+ WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
+ remainingBytes -= 4;
+ switch (chunkType)
+ {
+ case WebpChunkType.Animation:
+ uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, features.AnimationBackgroundColor.Value);
+ remainingBytes -= (int)dataSize;
+ break;
+ case WebpChunkType.Xmp:
+ case WebpChunkType.Exif:
+ WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image.Metadata, false, this.buffer);
+ break;
+ default:
+ WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
+ break;
+ }
+
+ if (stream.Position == stream.Length || this.DecodingMode is FrameDecodingMode.First)
+ {
+ break;
+ }
+ }
+
+ return image;
+ }
+
+ ///
+ /// Reads an individual webp frame.
+ ///
+ /// The pixel format.
+ /// The stream, where the image should be decoded from. Cannot be null.
+ /// The image to decode the information to.
+ /// The previous frame.
+ /// The width of the image.
+ /// The height of the image.
+ /// The default background color of the canvas in.
+ private uint ReadFrame(BufferedReadStream stream, ref Image image, ref ImageFrame previousFrame, uint width, uint height, Color backgroundColor)
+ where TPixel : unmanaged, IPixel
+ {
+ AnimationFrameData frameData = this.ReadFrameHeader(stream);
+ long streamStartPosition = stream.Position;
+
+ WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
+ bool hasAlpha = false;
+ byte alphaChunkHeader = 0;
+ if (chunkType is WebpChunkType.Alpha)
+ {
+ alphaChunkHeader = this.ReadAlphaData(stream);
+ hasAlpha = true;
+ chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
+ }
+
+ WebpImageInfo webpInfo = null;
+ var features = new WebpFeatures();
+ switch (chunkType)
+ {
+ case WebpChunkType.Vp8:
+ webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features);
+ features.Alpha = hasAlpha;
+ features.AlphaChunkHeader = alphaChunkHeader;
+ break;
+ case WebpChunkType.Vp8L:
+ webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features);
+ break;
+ default:
+ WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L");
+ break;
+ }
+
+ ImageFrame currentFrame = null;
+ ImageFrame imageFrame;
+ if (previousFrame is null)
+ {
+ image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata);
+
+ this.SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration);
+
+ imageFrame = image.Frames.RootFrame;
+ }
+ else
+ {
+ currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
+
+ this.SetFrameMetadata(currentFrame.Metadata, frameData.Duration);
+
+ imageFrame = currentFrame;
+ }
+
+ int frameX = (int)(frameData.X * 2);
+ int frameY = (int)(frameData.Y * 2);
+ int frameWidth = (int)frameData.Width;
+ int frameHeight = (int)frameData.Height;
+ var regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight);
+
+ if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose)
+ {
+ this.RestoreToBackground(imageFrame, backgroundColor);
+ }
+
+ using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo);
+ this.DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
+
+ if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending)
+ {
+ this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight);
+ }
+
+ previousFrame = currentFrame ?? image.Frames.RootFrame;
+ this.restoreArea = regionRectangle;
+
+ return (uint)(stream.Position - streamStartPosition);
+ }
+
+ ///
+ /// Sets the frames metadata.
+ ///
+ /// The metadata.
+ /// The frame duration.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SetFrameMetadata(ImageFrameMetadata meta, uint duration)
+ {
+ WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
+ frameMetadata.FrameDuration = duration;
+ }
+
+ ///
+ /// Reads the ALPH chunk data.
+ ///
+ /// The stream to read from.
+ private byte ReadAlphaData(BufferedReadStream stream)
+ {
+ this.AlphaData?.Dispose();
+
+ uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer);
+ int alphaDataSize = (int)(alphaChunkSize - 1);
+ this.AlphaData = this.memoryAllocator.Allocate(alphaDataSize);
+
+ byte alphaChunkHeader = (byte)stream.ReadByte();
+ Span alphaData = this.AlphaData.GetSpan();
+ stream.Read(alphaData, 0, alphaDataSize);
+
+ return alphaChunkHeader;
+ }
+
+ ///
+ /// Decodes the either lossy or lossless webp image data.
+ ///
+ /// The pixel format.
+ /// The frame data.
+ /// The webp information.
+ /// A decoded image.
+ private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo)
+ where TPixel : unmanaged, IPixel
+ {
+ var decodedImage = new Image((int)frameData.Width, (int)frameData.Height);
+
+ try
+ {
+ Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer;
+ if (webpInfo.IsLossless)
+ {
+ var losslessDecoder = new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
+ losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
+ }
+ else
+ {
+ var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
+ lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.AlphaData);
+ }
+
+ return pixelBufferDecoded;
+ }
+ catch
+ {
+ decodedImage?.Dispose();
+ throw;
+ }
+ finally
+ {
+ webpInfo.Dispose();
+ }
+ }
+
+ ///
+ /// Draws the decoded image on canvas. The decoded image can be smaller the the canvas.
+ ///
+ /// The type of the pixel.
+ /// The decoded image.
+ /// The image frame to draw into.
+ /// The frame x coordinate.
+ /// The frame y coordinate.
+ /// The width of the frame.
+ /// The height of the frame.
+ private void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight)
+ where TPixel : unmanaged, IPixel
+ {
+ Buffer2D imageFramePixels = imageFrame.PixelBuffer;
+ int decodedRowIdx = 0;
+ for (int y = frameY; y < frameY + frameHeight; y++)
+ {
+ Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
+ Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++).Slice(0, frameWidth);
+ decodedPixelRow.TryCopyTo(framePixelRow.Slice(frameX));
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The pixel format.
+ /// The source image.
+ /// The destination image.
+ /// The frame x coordinate.
+ /// The frame y coordinate.
+ /// The width of the frame.
+ /// The height of the frame.
+ private void AlphaBlend(ImageFrame src, ImageFrame dst, int frameX, int frameY, int frameWidth, int frameHeight)
+ where TPixel : unmanaged, IPixel
+ {
+ Buffer2D srcPixels = src.PixelBuffer;
+ Buffer2D dstPixels = dst.PixelBuffer;
+ PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
+ for (int y = frameY; y < frameY + frameHeight; y++)
+ {
+ Span srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
+ Span dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
+
+ blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f);
+ }
+ }
+
+ ///
+ /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame
+ /// with background color specified in the ANIM chunk.
+ ///
+ /// The pixel format.
+ /// The image frame.
+ /// Color of the background.
+ private void RestoreToBackground(ImageFrame imageFrame, Color backgroundColor)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!this.restoreArea.HasValue)
+ {
+ return;
+ }
+
+ var interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
+ Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
+ TPixel backgroundPixel = backgroundColor.ToPixel();
+ pixelRegion.Fill(backgroundPixel);
+ }
+
+ ///
+ /// Reads the animation frame header.
+ ///
+ /// The stream to read from.
+ /// Animation frame data.
+ private AnimationFrameData ReadFrameHeader(BufferedReadStream stream)
+ {
+ var data = new AnimationFrameData
+ {
+ DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer)
+ };
+
+ // 3 bytes for the X coordinate of the upper left corner of the frame.
+ data.X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer);
+
+ // 3 bytes for the Y coordinate of the upper left corner of the frame.
+ data.Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer);
+
+ // Frame width Minus One.
+ data.Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1;
+
+ // Frame height Minus One.
+ data.Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1;
+
+ // Frame duration.
+ data.Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer);
+
+ byte flags = (byte)stream.ReadByte();
+ data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose;
+ data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending;
+
+ return data;
+ }
+
+ ///
+ public void Dispose() => this.AlphaData?.Dispose();
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
new file mode 100644
index 0000000000..26d82a8929
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
@@ -0,0 +1,375 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers.Binary;
+using SixLabors.ImageSharp.Formats.Webp.BitReader;
+using SixLabors.ImageSharp.Formats.Webp.Lossy;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ internal static class WebpChunkParsingUtils
+ {
+ ///
+ /// Reads the header of a lossy webp image.
+ ///
+ /// Information about this webp image.
+ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features)
+ {
+ // VP8 data size (not including this 4 bytes).
+ int bytesRead = stream.Read(buffer, 0, 4);
+ if (bytesRead != 4)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header");
+ }
+
+ uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
+
+ // Remaining counts the available image data payload.
+ uint remaining = dataSize;
+
+ // Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30
+ // Frame tag that contains four fields:
+ // - A 1-bit frame type (0 for key frames, 1 for interframes).
+ // - A 3-bit version number.
+ // - A 1-bit show_frame flag.
+ // - A 19-bit field containing the size of the first data partition in bytes.
+ bytesRead = stream.Read(buffer, 0, 3);
+ if (bytesRead != 3)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header");
+ }
+
+ uint frameTag = (uint)(buffer[0] | (buffer[1] << 8) | (buffer[2] << 16));
+ remaining -= 3;
+ bool isNoKeyFrame = (frameTag & 0x1) == 1;
+ if (isNoKeyFrame)
+ {
+ WebpThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame");
+ }
+
+ uint version = (frameTag >> 1) & 0x7;
+ if (version > 3)
+ {
+ WebpThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}");
+ }
+
+ bool invisibleFrame = ((frameTag >> 4) & 0x1) == 0;
+ if (invisibleFrame)
+ {
+ WebpThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible");
+ }
+
+ uint partitionLength = frameTag >> 5;
+ if (partitionLength > dataSize)
+ {
+ WebpThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information");
+ }
+
+ // Check for VP8 magic bytes.
+ bytesRead = stream.Read(buffer, 0, 3);
+ if (bytesRead != 3)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
+ }
+
+ if (!buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
+ {
+ WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
+ }
+
+ bytesRead = stream.Read(buffer, 0, 4);
+ if (bytesRead != 4)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 header, could not read width and height");
+ }
+
+ uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
+ uint width = tmp & 0x3fff;
+ sbyte xScale = (sbyte)(tmp >> 6);
+ tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2));
+ uint height = tmp & 0x3fff;
+ sbyte yScale = (sbyte)(tmp >> 6);
+ remaining -= 7;
+ if (width == 0 || height == 0)
+ {
+ WebpThrowHelper.ThrowImageFormatException("width or height can not be zero");
+ }
+
+ if (partitionLength > remaining)
+ {
+ WebpThrowHelper.ThrowImageFormatException("bad partition length");
+ }
+
+ var vp8FrameHeader = new Vp8FrameHeader()
+ {
+ KeyFrame = true,
+ Profile = (sbyte)version,
+ PartitionLength = partitionLength
+ };
+
+ var bitReader = new Vp8BitReader(
+ stream,
+ remaining,
+ memoryAllocator,
+ partitionLength)
+ {
+ Remaining = remaining
+ };
+
+ return new WebpImageInfo()
+ {
+ Width = width,
+ Height = height,
+ XScale = xScale,
+ YScale = yScale,
+ BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
+ IsLossless = false,
+ Features = features,
+ Vp8Profile = (sbyte)version,
+ Vp8FrameHeader = vp8FrameHeader,
+ Vp8BitReader = bitReader
+ };
+ }
+
+ ///
+ /// Reads the header of a lossless webp image.
+ ///
+ /// Information about this image.
+ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, BufferedReadStream stream, byte[] buffer, WebpFeatures features)
+ {
+ // VP8 data size.
+ uint imageDataSize = ReadChunkSize(stream, buffer);
+
+ var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
+
+ // One byte signature, should be 0x2f.
+ uint signature = bitReader.ReadValue(8);
+ if (signature != WebpConstants.Vp8LHeaderMagicByte)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
+ }
+
+ // The first 28 bits of the bitstream specify the width and height of the image.
+ uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
+ uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
+ if (width == 0 || height == 0)
+ {
+ WebpThrowHelper.ThrowImageFormatException("invalid width or height read");
+ }
+
+ // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
+ // TODO: this flag value is not used yet
+ bool alphaIsUsed = bitReader.ReadBit();
+
+ // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0.
+ // Any other value should be treated as an error.
+ uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits);
+ if (version != 0)
+ {
+ WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
+ }
+
+ return new WebpImageInfo()
+ {
+ Width = width,
+ Height = height,
+ BitsPerPixel = WebpBitsPerPixel.Pixel32,
+ IsLossless = true,
+ Features = features,
+ Vp8LBitReader = bitReader
+ };
+ }
+
+ ///
+ /// Reads an the extended webp file header. An extended file header consists of:
+ /// - A 'VP8X' chunk with information about features used in the file.
+ /// - An optional 'ICCP' chunk with color profile.
+ /// - An optional 'XMP' chunk with metadata.
+ /// - An optional 'ANIM' chunk with animation control data.
+ /// - An optional 'ALPH' chunk with alpha channel data.
+ /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
+ ///
+ /// Information about this webp image.
+ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, byte[] buffer, WebpFeatures features)
+ {
+ uint fileSize = ReadChunkSize(stream, buffer);
+
+ // The first byte contains information about the image features used.
+ byte imageFeatures = (byte)stream.ReadByte();
+
+ // The first two bit of it are reserved and should be 0.
+ if (imageFeatures >> 6 != 0)
+ {
+ WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero");
+ }
+
+ // If bit 3 is set, a ICC Profile Chunk should be present.
+ features.IccProfile = (imageFeatures & (1 << 5)) != 0;
+
+ // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk).
+ features.Alpha = (imageFeatures & (1 << 4)) != 0;
+
+ // If bit 5 is set, a EXIF metadata should be present.
+ features.ExifProfile = (imageFeatures & (1 << 3)) != 0;
+
+ // If bit 6 is set, XMP metadata should be present.
+ features.XmpMetaData = (imageFeatures & (1 << 2)) != 0;
+
+ // If bit 7 is set, animation should be present.
+ features.Animation = (imageFeatures & (1 << 1)) != 0;
+
+ // 3 reserved bytes should follow which are supposed to be zero.
+ stream.Read(buffer, 0, 3);
+ if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0)
+ {
+ WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
+ }
+
+ // 3 bytes for the width.
+ uint width = ReadUnsignedInt24Bit(stream, buffer) + 1;
+
+ // 3 bytes for the height.
+ uint height = ReadUnsignedInt24Bit(stream, buffer) + 1;
+
+ // Read all the chunks in the order they occur.
+ var info = new WebpImageInfo()
+ {
+ Width = width,
+ Height = height,
+ Features = features
+ };
+
+ return info;
+ }
+
+ ///
+ /// Reads a unsigned 24 bit integer.
+ ///
+ /// The stream to read from.
+ /// The buffer to store the read data into.
+ /// A unsigned 24 bit integer.
+ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, byte[] buffer)
+ {
+ if (stream.Read(buffer, 0, 3) == 3)
+ {
+ buffer[3] = 0;
+ return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
+ }
+
+ throw new ImageFormatException("Invalid Webp data, could not read unsigned integer.");
+ }
+
+ ///
+ /// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload,
+ /// so the chunk size will be increased by 1 in those cases.
+ ///
+ /// The stream to read the data from.
+ /// Buffer to store the data read from the stream.
+ /// The chunk size in bytes.
+ public static uint ReadChunkSize(BufferedReadStream stream, byte[] buffer)
+ {
+ if (stream.Read(buffer, 0, 4) == 4)
+ {
+ uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
+ return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
+ }
+
+ throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
+ }
+
+ ///
+ /// Identifies the chunk type from the chunk.
+ ///
+ /// The stream to read the data from.
+ /// Buffer to store the data read from the stream.
+ ///
+ /// Thrown if the input stream is not valid.
+ ///
+ public static WebpChunkType ReadChunkType(BufferedReadStream stream, byte[] buffer)
+ {
+ if (stream.Read(buffer, 0, 4) == 4)
+ {
+ var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ return chunkType;
+ }
+
+ throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
+ }
+
+ ///
+ /// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
+ /// If there are more such chunks, readers MAY ignore all except the first one.
+ /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
+ ///
+ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, byte[] buffer)
+ {
+ long streamLength = stream.Length;
+ while (stream.Position < streamLength)
+ {
+ uint chunkLength = ReadChunkSize(stream, buffer);
+
+ if (ignoreMetaData)
+ {
+ stream.Skip((int)chunkLength);
+ }
+
+ int bytesRead;
+ switch (chunkType)
+ {
+ case WebpChunkType.Exif:
+ byte[] exifData = new byte[chunkLength];
+ bytesRead = stream.Read(exifData, 0, (int)chunkLength);
+ if (bytesRead != chunkLength)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
+ }
+
+ if (metadata.ExifProfile != null)
+ {
+ metadata.ExifProfile = new ExifProfile(exifData);
+ }
+
+ break;
+ case WebpChunkType.Xmp:
+ byte[] xmpData = new byte[chunkLength];
+ bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
+ if (bytesRead != chunkLength)
+ {
+ WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
+ }
+
+ if (metadata.XmpProfile != null)
+ {
+ metadata.XmpProfile = new XmpProfile(xmpData);
+ }
+
+ break;
+ default:
+ stream.Skip((int)chunkLength);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Determines if the chunk type is an optional VP8X chunk.
+ ///
+ /// The chunk type.
+ /// True, if its an optional chunk type.
+ public static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch
+ {
+ WebpChunkType.Alpha => true,
+ WebpChunkType.AnimationParameter => true,
+ WebpChunkType.Exif => true,
+ WebpChunkType.Iccp => true,
+ WebpChunkType.Xmp => true,
+ _ => false
+ };
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs
index c9470a66f2..2f6b593eef 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs
@@ -4,6 +4,7 @@
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
@@ -18,13 +19,19 @@ public sealed class WebpDecoder : IImageDecoder, IWebpDecoderOptions, IImageInfo
///
public bool IgnoreMetadata { get; set; }
+ ///
+ /// Gets or sets the decoding mode for multi-frame images.
+ /// Defaults to All.
+ ///
+ public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All;
+
///
public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
Guard.NotNull(stream, nameof(stream));
- var decoder = new WebpDecoderCore(configuration, this);
+ using var decoder = new WebpDecoderCore(configuration, this);
try
{
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 0e00f037ca..979ac55825 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
-using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
///
/// Performs the webp decoding operation.
///
- internal sealed class WebpDecoderCore : IImageDecoderInternals
+ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
///
/// Reusable buffer.
@@ -29,14 +29,14 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals
private readonly byte[] buffer = new byte[4];
///
- /// Used for allocating memory during processing operations.
+ /// Used for allocating memory during the decoding operations.
///
private readonly MemoryAllocator memoryAllocator;
///
/// The stream to decode from.
///
- private Stream currentStream;
+ private BufferedReadStream currentStream;
///
/// The webp specific metadata.
@@ -56,10 +56,19 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals
public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options)
{
this.Configuration = configuration;
+ this.DecodingMode = options.DecodingMode;
this.memoryAllocator = configuration.MemoryAllocator;
this.IgnoreMetadata = options.IgnoreMetadata;
}
+ ///
+ public Configuration Configuration { get; }
+
+ ///
+ /// Gets the decoding mode for multi-frame images.
+ ///
+ public FrameDecodingMode DecodingMode { get; }
+
///
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
///
@@ -70,14 +79,16 @@ public WebpDecoderCore(Configuration configuration, IWebpDecoderOptions options)
///
public ImageMetadata Metadata { get; private set; }
- ///
- public Configuration Configuration { get; }
-
///
/// Gets the dimensions of the image.
///
public Size Dimensions => new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height);
+ ///
+ /// Gets or sets the alpha data, if an ALPH chunk is present.
+ ///
+ public IMemoryOwner AlphaData { get; set; }
+
///
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
@@ -92,6 +103,12 @@ public Image Decode(BufferedReadStream stream, CancellationToken
using (this.webImageInfo = this.ReadVp8Info())
{
+ if (this.webImageInfo.Features is { Animation: true })
+ {
+ using var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration, this.DecodingMode);
+ return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
+ }
+
if (this.webImageInfo.Features is { Animation: true })
{
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
@@ -107,7 +124,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken
else
{
var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration);
- lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo);
+ lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.AlphaData);
}
// There can be optional chunks after the image data, like EXIF and XMP.
@@ -132,7 +149,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
this.currentStream = stream;
this.ReadImageHeader();
- using (this.webImageInfo = this.ReadVp8Info())
+ using (this.webImageInfo = this.ReadVp8Info(true))
{
return new ImageInfo(new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
}
@@ -150,7 +167,7 @@ private uint ReadImageHeader()
// Read file size.
// The size of the file in bytes starting at offset 8.
// The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC.
- uint fileSize = this.ReadChunkSize();
+ uint fileSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
// Skip 'WEBP' from the header.
this.currentStream.Skip(4);
@@ -161,310 +178,59 @@ private uint ReadImageHeader()
///
/// Reads information present in the image header, about the image content and how to decode the image.
///
+ /// For identify, the alpha data should not be read.
/// Information about the webp image.
- private WebpImageInfo ReadVp8Info()
+ private WebpImageInfo ReadVp8Info(bool ignoreAlpha = false)
{
this.Metadata = new ImageMetadata();
this.webpMetadata = this.Metadata.GetFormatMetadata(WebpFormat.Instance);
- WebpChunkType chunkType = this.ReadChunkType();
+ WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
+ var features = new WebpFeatures();
switch (chunkType)
{
case WebpChunkType.Vp8:
- return this.ReadVp8Header();
+ this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
+ return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8L:
- return this.ReadVp8LHeader();
+ this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
+ return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8X:
- return this.ReadVp8XHeader();
+ WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(this.currentStream, this.buffer, features);
+ while (this.currentStream.Position < this.currentStream.Length)
+ {
+ chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
+ if (chunkType == WebpChunkType.Vp8)
+ {
+ this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
+ webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
+ }
+ else if (chunkType == WebpChunkType.Vp8L)
+ {
+ this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
+ webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
+ }
+ else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
+ {
+ bool isAnimationChunk = this.ParseOptionalExtendedChunks(chunkType, features, ignoreAlpha);
+ if (isAnimationChunk)
+ {
+ return webpInfos;
+ }
+ }
+ else
+ {
+ WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
+ }
+ }
+
+ return webpInfos;
default:
WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header");
- return new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception.
- }
- }
-
- ///
- /// Reads an the extended webp file header. An extended file header consists of:
- /// - A 'VP8X' chunk with information about features used in the file.
- /// - An optional 'ICCP' chunk with color profile.
- /// - An optional 'XMP' chunk with metadata.
- /// - An optional 'ANIM' chunk with animation control data.
- /// - An optional 'ALPH' chunk with alpha channel data.
- /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
- ///
- /// Information about this webp image.
- private WebpImageInfo ReadVp8XHeader()
- {
- var features = new WebpFeatures();
- uint fileSize = this.ReadChunkSize();
-
- // The first byte contains information about the image features used.
- int imageFeatures = this.currentStream.ReadByte();
- if (imageFeatures == -1)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("VP8X header doe not contain enough data");
- }
-
- // The first two bit of it are reserved and should be 0.
- if (imageFeatures >> 6 != 0)
- {
- WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero");
- }
-
- // If bit 3 is set, a ICC Profile Chunk should be present.
- features.IccProfile = (imageFeatures & (1 << 5)) != 0;
-
- // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk).
- features.Alpha = (imageFeatures & (1 << 4)) != 0;
-
- // If bit 5 is set, a EXIF metadata should be present.
- features.ExifProfile = (imageFeatures & (1 << 3)) != 0;
-
- // If bit 6 is set, XMP metadata should be present.
- features.XmpMetaData = (imageFeatures & (1 << 2)) != 0;
-
- // If bit 7 is set, animation should be present.
- features.Animation = (imageFeatures & (1 << 1)) != 0;
-
- // 3 reserved bytes should follow which are supposed to be zero.
- int bytesRead = this.currentStream.Read(this.buffer, 0, 3);
- if (bytesRead != 3)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("VP8X header does not contain enough data");
- }
-
- if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0)
- {
- WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
- }
-
- // 3 bytes for the width.
- bytesRead = this.currentStream.Read(this.buffer, 0, 3);
- if (bytesRead != 3)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the width");
+ return
+ new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception.
}
-
- this.buffer[3] = 0;
- uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
-
- // 3 bytes for the height.
- bytesRead = this.currentStream.Read(this.buffer, 0, 3);
- if (bytesRead != 3)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the height");
- }
-
- this.buffer[3] = 0;
- uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
-
- // Read all the chunks in the order they occur.
- var info = new WebpImageInfo();
- while (this.currentStream.Position < this.currentStream.Length)
- {
- WebpChunkType chunkType = this.ReadChunkType();
- if (chunkType == WebpChunkType.Vp8)
- {
- info = this.ReadVp8Header(features);
- }
- else if (chunkType == WebpChunkType.Vp8L)
- {
- info = this.ReadVp8LHeader(features);
- }
- else if (IsOptionalVp8XChunk(chunkType))
- {
- this.ParseOptionalExtendedChunks(chunkType, features);
- }
- else
- {
- WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
- }
- }
-
- if (features.Animation)
- {
- // TODO: Animations are not yet supported.
- return new WebpImageInfo() { Width = width, Height = height, Features = features };
- }
-
- return info;
- }
-
- ///
- /// Reads the header of a lossy webp image.
- ///
- /// Webp features.
- /// Information about this webp image.
- private WebpImageInfo ReadVp8Header(WebpFeatures features = null)
- {
- this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
-
- // VP8 data size (not including this 4 bytes).
- int bytesRead = this.currentStream.Read(this.buffer, 0, 4);
- if (bytesRead != 4)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 data size");
- }
-
- uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
-
- // remaining counts the available image data payload.
- uint remaining = dataSize;
-
- // Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30
- // Frame tag that contains four fields:
- // - A 1-bit frame type (0 for key frames, 1 for interframes).
- // - A 3-bit version number.
- // - A 1-bit show_frame flag.
- // - A 19-bit field containing the size of the first data partition in bytes.
- bytesRead = this.currentStream.Read(this.buffer, 0, 3);
- if (bytesRead != 3)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 frame tag");
- }
-
- uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16));
- remaining -= 3;
- bool isNoKeyFrame = (frameTag & 0x1) == 1;
- if (isNoKeyFrame)
- {
- WebpThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame");
- }
-
- uint version = (frameTag >> 1) & 0x7;
- if (version > 3)
- {
- WebpThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}");
- }
-
- bool invisibleFrame = ((frameTag >> 4) & 0x1) == 0;
- if (invisibleFrame)
- {
- WebpThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible");
- }
-
- uint partitionLength = frameTag >> 5;
- if (partitionLength > dataSize)
- {
- WebpThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information");
- }
-
- // Check for VP8 magic bytes.
- bytesRead = this.currentStream.Read(this.buffer, 0, 3);
- if (bytesRead != 3)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
- }
-
- if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
- {
- WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
- }
-
- bytesRead = this.currentStream.Read(this.buffer, 0, 4);
- if (bytesRead != 4)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the image width and height");
- }
-
- uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer);
- uint width = tmp & 0x3fff;
- sbyte xScale = (sbyte)(tmp >> 6);
- tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2));
- uint height = tmp & 0x3fff;
- sbyte yScale = (sbyte)(tmp >> 6);
- remaining -= 7;
- if (width == 0 || height == 0)
- {
- WebpThrowHelper.ThrowImageFormatException("width or height can not be zero");
- }
-
- if (partitionLength > remaining)
- {
- WebpThrowHelper.ThrowImageFormatException("bad partition length");
- }
-
- var vp8FrameHeader = new Vp8FrameHeader()
- {
- KeyFrame = true,
- Profile = (sbyte)version,
- PartitionLength = partitionLength
- };
-
- var bitReader = new Vp8BitReader(
- this.currentStream,
- remaining,
- this.memoryAllocator,
- partitionLength)
- {
- Remaining = remaining
- };
-
- return new WebpImageInfo()
- {
- Width = width,
- Height = height,
- XScale = xScale,
- YScale = yScale,
- BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
- IsLossless = false,
- Features = features,
- Vp8Profile = (sbyte)version,
- Vp8FrameHeader = vp8FrameHeader,
- Vp8BitReader = bitReader
- };
- }
-
- ///
- /// Reads the header of a lossless webp image.
- ///
- /// Webp image features.
- /// Information about this image.
- private WebpImageInfo ReadVp8LHeader(WebpFeatures features = null)
- {
- this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
-
- // VP8 data size.
- uint imageDataSize = this.ReadChunkSize();
-
- var bitReader = new Vp8LBitReader(this.currentStream, imageDataSize, this.memoryAllocator);
-
- // One byte signature, should be 0x2f.
- uint signature = bitReader.ReadValue(8);
- if (signature != WebpConstants.Vp8LHeaderMagicByte)
- {
- WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
- }
-
- // The first 28 bits of the bitstream specify the width and height of the image.
- uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
- uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
- if (width == 0 || height == 0)
- {
- WebpThrowHelper.ThrowImageFormatException("invalid width or height read");
- }
-
- // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
- // TODO: this flag value is not used yet
- bool alphaIsUsed = bitReader.ReadBit();
-
- // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0.
- // Any other value should be treated as an error.
- uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits);
- if (version != 0)
- {
- WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
- }
-
- return new WebpImageInfo()
- {
- Width = width,
- Height = height,
- BitsPerPixel = WebpBitsPerPixel.Pixel32,
- IsLossless = true,
- Features = features,
- Vp8LBitReader = bitReader
- };
}
///
@@ -472,7 +238,9 @@ private WebpImageInfo ReadVp8LHeader(WebpFeatures features = null)
///
/// The chunk type.
/// The webp image features.
- private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features)
+ /// For identify, the alpha data should not be read.
+ /// true, if its a alpha chunk.
+ private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features, bool ignoreAlpha)
{
switch (chunkType)
{
@@ -488,32 +256,23 @@ private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures f
this.ReadXmpProfile();
break;
- case WebpChunkType.Animation:
- // TODO: Decoding animation is not implemented yet.
- break;
+ case WebpChunkType.AnimationParameter:
+ this.ReadAnimationParameters(features);
+ return true;
case WebpChunkType.Alpha:
- uint alphaChunkSize = this.ReadChunkSize();
- features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
- int alphaDataSize = (int)(alphaChunkSize - 1);
- features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize);
- int bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
- if (bytesRead != alphaDataSize)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk");
- }
-
+ this.ReadAlphaData(features, ignoreAlpha);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
+
+ return false;
}
///
- /// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
- /// If there are more such chunks, readers MAY ignore all except the first one.
- /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
+ /// Reads the optional metadata EXIF of XMP profiles, which can follow the image data.
///
/// The webp features.
private void ParseOptionalChunks(WebpFeatures features)
@@ -622,6 +381,53 @@ private void ReadIccProfile()
}
}
+ ///
+ /// Reads the animation parameters chunk from the stream.
+ ///
+ /// The webp features.
+ private void ReadAnimationParameters(WebpFeatures features)
+ {
+ features.Animation = true;
+ uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
+ byte blue = (byte)this.currentStream.ReadByte();
+ byte green = (byte)this.currentStream.ReadByte();
+ byte red = (byte)this.currentStream.ReadByte();
+ byte alpha = (byte)this.currentStream.ReadByte();
+ features.AnimationBackgroundColor = new Color(new Rgba32(red, green, blue, alpha));
+ int bytesRead = this.currentStream.Read(this.buffer, 0, 2);
+ if (bytesRead != 2)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the animation loop count");
+ }
+
+ features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(this.buffer);
+ }
+
+ ///
+ /// Reads the alpha data chunk data from the stream.
+ ///
+ /// The features.
+ /// if set to true, skips the chunk data.
+ private void ReadAlphaData(WebpFeatures features, bool ignoreAlpha)
+ {
+ uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
+ if (ignoreAlpha)
+ {
+ this.currentStream.Skip((int)alphaChunkSize);
+ return;
+ }
+
+ features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
+ int alphaDataSize = (int)(alphaChunkSize - 1);
+ this.AlphaData = this.memoryAllocator.Allocate(alphaDataSize);
+ Span alphaData = this.AlphaData.GetSpan();
+ int bytesRead = this.currentStream.Read(alphaData, 0, alphaDataSize);
+ if (bytesRead != alphaDataSize)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha data from the stream");
+ }
+ }
+
///
/// Identifies the chunk type from the chunk.
///
@@ -655,19 +461,7 @@ private uint ReadChunkSize()
throw new ImageFormatException("Invalid Webp data.");
}
- ///
- /// Determines if the chunk type is an optional VP8X chunk.
- ///
- /// The chunk type.
- /// True, if its an optional chunk type.
- private static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch
- {
- WebpChunkType.Alpha => true,
- WebpChunkType.Animation => true,
- WebpChunkType.Exif => true,
- WebpChunkType.Iccp => true,
- WebpChunkType.Xmp => true,
- _ => false
- };
+ ///
+ public void Dispose() => this.AlphaData?.Dispose();
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpFeatures.cs b/src/ImageSharp/Formats/Webp/WebpFeatures.cs
index b26e4101e0..398514d5bd 100644
--- a/src/ImageSharp/Formats/Webp/WebpFeatures.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFeatures.cs
@@ -1,15 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System;
-using System.Buffers;
-
namespace SixLabors.ImageSharp.Formats.Webp
{
///
/// Image features of a VP8X image.
///
- internal class WebpFeatures : IDisposable
+ internal class WebpFeatures
{
///
/// Gets or sets a value indicating whether this image has an ICC Profile.
@@ -21,11 +18,6 @@ internal class WebpFeatures : IDisposable
///
public bool Alpha { get; set; }
- ///
- /// Gets or sets the alpha data, if an ALPH chunk is present.
- ///
- public IMemoryOwner AlphaData { get; set; }
-
///
/// Gets or sets the alpha chunk header.
///
@@ -46,7 +38,15 @@ internal class WebpFeatures : IDisposable
///
public bool Animation { get; set; }
- ///
- public void Dispose() => this.AlphaData?.Dispose();
+ ///
+ /// Gets or sets the animation loop count. 0 means infinitely.
+ ///
+ public ushort AnimationLoopCount { get; set; }
+
+ ///
+ /// Gets or sets default background color of the animation frame canvas.
+ /// 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..
+ ///
+ public Color? AnimationBackgroundColor { get; set; }
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs
index 1f27c4d843..bc3fb09c32 100644
--- a/src/ImageSharp/Formats/Webp/WebpFormat.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs
@@ -6,9 +6,9 @@
namespace SixLabors.ImageSharp.Formats.Webp
{
///
- /// Registers the image encoders, decoders and mime type detectors for the Webp format
+ /// Registers the image encoders, decoders and mime type detectors for the Webp format.
///
- public sealed class WebpFormat : IImageFormat
+ public sealed class WebpFormat : IImageFormat
{
private WebpFormat()
{
@@ -17,7 +17,7 @@ private WebpFormat()
///
/// Gets the current instance.
///
- public static WebpFormat Instance { get; } = new WebpFormat();
+ public static WebpFormat Instance { get; } = new();
///
public string Name => "Webp";
@@ -32,6 +32,9 @@ private WebpFormat()
public IEnumerable FileExtensions => WebpConstants.FileExtensions;
///
- public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
+ public WebpMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
new file mode 100644
index 0000000000..bebfb9d792
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Webp
+{
+ ///
+ /// Provides webp specific metadata information for the image frame.
+ ///
+ public class WebpFrameMetadata : IDeepCloneable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WebpFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration;
+
+ ///
+ /// Gets or sets the frame duration. The time to wait before displaying the next frame,
+ /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined.
+ ///
+ public uint FrameDuration { get; set; }
+
+ ///
+ public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
index 530f5c0a5a..aa11d38c38 100644
--- a/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
+++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs
@@ -63,7 +63,6 @@ public void Dispose()
{
this.Vp8BitReader?.Dispose();
this.Vp8LBitReader?.Dispose();
- this.Features?.AlphaData?.Dispose();
}
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
index f398d3d874..5dd0105024 100644
--- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
@@ -19,13 +19,22 @@ public WebpMetadata()
/// Initializes a new instance of the class.
///
/// The metadata to create an instance from.
- private WebpMetadata(WebpMetadata other) => this.FileFormat = other.FileFormat;
+ private WebpMetadata(WebpMetadata other)
+ {
+ this.FileFormat = other.FileFormat;
+ this.AnimationLoopCount = other.AnimationLoopCount;
+ }
///
/// Gets or sets the webp file format used. Either lossless or lossy.
///
public WebpFileFormatType? FileFormat { get; set; }
+ ///
+ /// Gets or sets the loop count. The number of times to loop the animation. 0 means infinitely.
+ ///
+ public ushort AnimationLoopCount { get; set; } = 1;
+
///
public IDeepCloneable DeepClone() => new WebpMetadata(this);
}
diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
index 13b3395977..9b9c1aa5b0 100644
--- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
@@ -141,6 +141,9 @@ internal ref T GetReferenceToOrigin()
return ref this.Buffer.DangerousGetRowSpan(y)[x];
}
+ ///
+ /// Clears the contents of this .
+ ///
internal void Clear()
{
// Optimization for when the size of the area is the same as the buffer size.
@@ -156,5 +159,25 @@ internal void Clear()
row.Clear();
}
}
+
+ ///
+ /// Fills the elements of this with the specified value.
+ ///
+ /// The value to assign to each element of the region.
+ internal void Fill(T value)
+ {
+ // Optimization for when the size of the area is the same as the buffer size.
+ if (this.IsFullBufferArea)
+ {
+ this.Buffer.FastMemoryGroup.Fill(value);
+ return;
+ }
+
+ for (int y = 0; y < this.Rectangle.Height; y++)
+ {
+ Span row = this.DangerousGetRowSpan(y);
+ row.Fill(value);
+ }
+ }
}
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
index d200b223a7..7a4e0df5dc 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
@@ -7,6 +7,12 @@ namespace SixLabors.ImageSharp.Memory
{
internal static class MemoryGroupExtensions
{
+ ///
+ /// Fills the elements of this with the specified value.
+ ///
+ /// The type of element.
+ /// The group to fill.
+ /// The value to assign to each element of the group.
internal static void Fill(this IMemoryGroup group, T value)
where T : struct
{
@@ -16,6 +22,11 @@ internal static void Fill(this IMemoryGroup group, T value)
}
}
+ ///
+ /// Clears the contents of this .
+ ///
+ /// The type of element.
+ /// The group to clear.
internal static void Clear(this IMemoryGroup group)
where T : struct
{
diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
index 1cad4ebe86..f8ed18e28a 100644
--- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
@@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata
///
public sealed class ImageFrameMetadata : IDeepCloneable
{
- private readonly Dictionary formatMetadata = new Dictionary();
+ private readonly Dictionary formatMetadata = new();
///
/// Initializes a new instance of the class.
@@ -67,7 +67,7 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
public IptcProfile IptcProfile { get; set; }
///
- public ImageFrameMetadata DeepClone() => new ImageFrameMetadata(this);
+ public ImageFrameMetadata DeepClone() => new(this);
///
/// Gets the metadata value associated with the specified key.
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index 4760fa141e..d3cb916d4a 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -33,7 +33,7 @@ public sealed class ImageMetadata : IDeepCloneable
///
public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch;
- private readonly Dictionary formatMetadata = new Dictionary();
+ private readonly Dictionary formatMetadata = new();
private double horizontalResolution;
private double verticalResolution;
diff --git a/src/ImageSharp/Primitives/Rectangle.cs b/src/ImageSharp/Primitives/Rectangle.cs
index 1904b09790..cd18282496 100644
--- a/src/ImageSharp/Primitives/Rectangle.cs
+++ b/src/ImageSharp/Primitives/Rectangle.cs
@@ -81,7 +81,7 @@ public Rectangle(Point point, Size size)
public Point Location
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => new Point(this.X, this.Y);
+ get => new(this.X, this.Y);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@@ -98,7 +98,7 @@ public Point Location
public Size Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => new Size(this.Width, this.Height);
+ get => new(this.Width, this.Height);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@@ -147,14 +147,14 @@ public int Bottom
///
/// The rectangle.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static implicit operator RectangleF(Rectangle rectangle) => new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ public static implicit operator RectangleF(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
///
/// Creates a with the coordinates of the specified .
///
/// The rectangle.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static implicit operator Vector4(Rectangle rectangle) => new Vector4(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ public static implicit operator Vector4(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
///
/// Compares two objects for equality.
@@ -188,7 +188,7 @@ public int Bottom
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once InconsistentNaming
- public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top));
+ public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top));
///
/// Returns the center point of the given .
@@ -196,7 +196,7 @@ public int Bottom
/// The rectangle.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Point Center(Rectangle rectangle) => new Point(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
+ public static Point Center(Rectangle rectangle) => new(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
///
/// Creates a rectangle that represents the intersection between and
@@ -376,7 +376,7 @@ public void Inflate(int width, int height)
public void Inflate(Size size) => this.Inflate(size.Width, size.Height);
///
- /// Determines if the specfied point is contained within the rectangular region defined by
+ /// Determines if the specified point is contained within the rectangular region defined by
/// this .
///
/// The x-coordinate of the given point.
@@ -405,10 +405,10 @@ public bool Contains(Rectangle rectangle) =>
(this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom);
///
- /// Determines if the specfied intersects the rectangular region defined by
+ /// Determines if the specified intersects the rectangular region defined by
/// this .
///
- /// The other Rectange.
+ /// The other Rectangle.
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IntersectsWith(Rectangle rectangle) =>
@@ -438,16 +438,10 @@ public void Offset(int dx, int dy)
}
///
- public override int GetHashCode()
- {
- return HashCode.Combine(this.X, this.Y, this.Width, this.Height);
- }
+ public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Width, this.Height);
///
- public override string ToString()
- {
- return $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
- }
+ public override string ToString() => $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
///
public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other);
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index 7a5241c5a8..ffe053f6a1 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -131,7 +131,7 @@ public void DetectPixelSize(string imagePath, int expectedPixelSize)
public void Decode_WithInvalidDimensions_DoesThrowException(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
- System.Exception ex = Record.Exception(
+ Exception ex = Record.Exception(
() =>
{
using Image image = provider.GetImage(GifDecoder);
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index f29fa5d793..f5f472bf34 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -3,8 +3,10 @@
using System.IO;
using SixLabors.ImageSharp.Formats.Webp;
+using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Webp;
@@ -34,7 +36,7 @@ public class WebpDecoderTests
[InlineData(Lossless.NoTransform2, 128, 128, 32)]
[InlineData(Lossy.Alpha1, 1000, 307, 32)]
[InlineData(Lossy.Alpha2, 1000, 307, 32)]
- [InlineData(Lossy.Bike, 250, 195, 24)]
+ [InlineData(Lossy.BikeWithExif, 250, 195, 24)]
public void Identify_DetectsCorrectDimensionsAndBitDepth(
string imagePath,
int expectedWidth,
@@ -53,7 +55,7 @@ public void Identify_DetectsCorrectDimensionsAndBitDepth(
}
[Theory]
- [WithFile(Lossy.Bike, PixelTypes.Rgba32)]
+ [WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter01, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter02, PixelTypes.Rgba32)]
[WithFile(Lossy.NoFilter03, PixelTypes.Rgba32)]
@@ -234,7 +236,7 @@ public void WebpDecoder_CanDecode_Lossless_WithoutTransforms(TestImagePr
// TODO: Reference decoder throws here MagickCorruptImageErrorException, webpinfo also indicates an error here, but decoding the image seems to work.
// [WithFile(Lossless.GreenTransform5, PixelTypes.Rgba32)]
- public void WebpDecoder_CanDecode_Lossless_WithSubstractGreenTransform(
+ public void WebpDecoder_CanDecode_Lossless_WithSubtractGreenTransform(
TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
@@ -330,6 +332,55 @@ public void WebpDecoder_CanDecode_Lossless_WithThreeTransforms(TestImage
}
}
+ [Theory]
+ [WithFile(Lossless.Animated, PixelTypes.Rgba32)]
+ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(WebpDecoder))
+ {
+ WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
+ WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata();
+
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
+
+ Assert.Equal(0, webpMetaData.AnimationLoopCount);
+ Assert.Equal(150U, frameMetaData.FrameDuration);
+ Assert.Equal(12, image.Frames.Count);
+ }
+ }
+
+ [Theory]
+ [WithFile(Lossy.Animated, PixelTypes.Rgba32)]
+ public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(WebpDecoder))
+ {
+ WebpMetadata webpMetaData = image.Metadata.GetWebpMetadata();
+ WebpFrameMetadata frameMetaData = image.Frames.RootFrame.Metadata.GetWebpMetadata();
+
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
+
+ Assert.Equal(0, webpMetaData.AnimationLoopCount);
+ Assert.Equal(150U, frameMetaData.FrameDuration);
+ Assert.Equal(12, image.Frames.Count);
+ }
+ }
+
+ [Theory]
+ [WithFile(Lossless.Animated, PixelTypes.Rgba32)]
+ public void Decode_AnimatedLossless_WithFrameDecodingModeFirst_OnlyDecodesOneFrame(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(new WebpDecoder() { DecodingMode = FrameDecodingMode.First }))
+ {
+ Assert.Equal(1, image.Frames.Count);
+ }
+ }
+
[Theory]
[WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)]
[WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
index 7c74429edc..21056bdc61 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
@@ -20,7 +20,7 @@ public class WebpEncoderTests
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
- [WithFile(Lossy.Bike, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
+ [WithFile(Lossy.BikeWithExif, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
public void Encode_PreserveRatio(TestImageProvider provider, WebpFileFormatType expectedFormat)
where TPixel : unmanaged, IPixel
{
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
index d1f148be02..1787afb93e 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
@@ -18,11 +18,31 @@ public class WebpMetaDataTests
private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false };
[Theory]
- [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, false)]
- [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, true)]
+ [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)]
+ [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)]
+ public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLossyImage(TestImageProvider provider, bool ignoreMetadata)
+ where TPixel : unmanaged, IPixel
+ {
+ var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
+
+ using Image image = provider.GetImage(decoder);
+ if (ignoreMetadata)
+ {
+ Assert.Null(image.Metadata.ExifProfile);
+ }
+ else
+ {
+ ExifProfile exifProfile = image.Metadata.ExifProfile;
+ Assert.NotNull(exifProfile);
+ Assert.NotEmpty(exifProfile.Values);
+ Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2"));
+ }
+ }
+
+ [Theory]
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, true)]
- public void IgnoreMetadata_ControlsWhetherExifIsParsed(TestImageProvider provider, bool ignoreMetadata)
+ public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLosslessImage(TestImageProvider provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
@@ -111,7 +131,7 @@ public void Encode_WritesExifWithPadding(WebpFileFormatType fileFormatType)
}
[Theory]
- [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32)]
public void EncodeLossyWebp_PreservesExif(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
index 13664ee9b2..cdd6754cf1 100644
--- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
+++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
@@ -59,9 +59,9 @@ public class TestMemoryDiagnostics : IDisposable
public void Validate(int expectedAllocationCount)
{
- var count = this.TotalRemainingAllocated;
- var pass = expectedAllocationCount == count;
- Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}");
+ int count = this.TotalRemainingAllocated;
+ bool pass = expectedAllocationCount == count;
+ Assert.True(pass, $"Expected {expectedAllocationCount} undisposed buffers but found {count}");
}
public void Dispose()
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index d36cec630d..a3ef6942f3 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -562,16 +562,9 @@ public static class Webp
// Test images for converting rgb data to yuv.
public const string Yuv = "Webp/yuv_test.png";
- public static class Animated
- {
- public const string Animated1 = "Webp/animated-webp.webp";
- public const string Animated2 = "Webp/animated2.webp";
- public const string Animated3 = "Webp/animated3.webp";
- public const string Animated4 = "Webp/animated_lossy.webp";
- }
-
public static class Lossless
{
+ public const string Animated = "Webp/leo_animated_lossless.webp";
public const string Earth = "Webp/earth_lossless.webp";
public const string Alpha = "Webp/lossless_alpha_small.webp";
public const string WithExif = "Webp/exif_lossless.webp";
@@ -648,10 +641,11 @@ public static class Lossy
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";
public const string WithIccp = "Webp/lossy_with_iccp.webp";
public const string WithXmp = "Webp/xmp_lossy.webp";
- public const string BikeSmall = "Webp/bike_lossless_small.webp";
+ public const string BikeSmall = "Webp/bike_lossy_small.webp";
+ public const string Animated = "Webp/leo_animated_lossy.webp";
// Lossy images without macroblock filtering.
- public const string Bike = "Webp/bike_lossy.webp";
+ public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp";
public const string NoFilter01 = "Webp/vp80-01-intra-1400.webp";
public const string NoFilter02 = "Webp/vp80-00-comprehensive-010.webp";
public const string NoFilter03 = "Webp/vp80-00-comprehensive-005.webp";
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png
new file mode 100644
index 0000000000..ba7d1f98eb
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5
+size 50298
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png
new file mode 100644
index 0000000000..7e5669acb7
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad
+size 49389
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png
new file mode 100644
index 0000000000..38e594a18d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/02.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85
+size 52686
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png
new file mode 100644
index 0000000000..8fa981590c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/03.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559
+size 53244
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png
new file mode 100644
index 0000000000..382f196e20
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/04.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473
+size 56046
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png
new file mode 100644
index 0000000000..79a5f44ec6
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/05.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de
+size 62469
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png
new file mode 100644
index 0000000000..3299889d80
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/06.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0
+size 61068
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png
new file mode 100644
index 0000000000..bc43fac5d4
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/07.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5
+size 60411
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png
new file mode 100644
index 0000000000..7a71a5ff0b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/08.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527
+size 58793
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png
new file mode 100644
index 0000000000..e8c9eb3f24
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/09.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391
+size 57157
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png
new file mode 100644
index 0000000000..05d5ab1d0f
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d
+size 55424
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png
new file mode 100644
index 0000000000..ae6ce177b0
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossless_VerifyAllFrames_Rgba32_leo_animated_lossless.webp/11.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de
+size 59950
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png
new file mode 100644
index 0000000000..ba7d1f98eb
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d99914f1a4dc3e554b9dded9e547194685b1b9ecc5d816d9f329cef483c525d5
+size 50298
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png
new file mode 100644
index 0000000000..7e5669acb7
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:391ed80dc5ba4a21bdc4ea4db9fde4c6dad8556d1b8f0bf198db3c2bb5dc50ad
+size 49389
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png
new file mode 100644
index 0000000000..38e594a18d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/02.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84bf215392014c2d7dbeb495bd1717bc2da4566b285bc388ed7bc8e88ebb0e85
+size 52686
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png
new file mode 100644
index 0000000000..8fa981590c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/03.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e7a47ba473440f699f337fb8886cd170c6610452b3145c068a0f18584541559
+size 53244
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png
new file mode 100644
index 0000000000..382f196e20
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/04.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4e7572c91c73e63e74c795e16ce951fbbdba5a015921102844d7bdf0fb0b473
+size 56046
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png
new file mode 100644
index 0000000000..79a5f44ec6
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/05.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6681af3640adb85452f9c1fa0cb5dce04638b48d80994c20c40d11e07670f1de
+size 62469
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png
new file mode 100644
index 0000000000..3299889d80
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/06.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8549aeb786fc12d4e947b3b5f862701fab8158576193a03877f4b891815077e0
+size 61068
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png
new file mode 100644
index 0000000000..bc43fac5d4
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/07.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:474a6bbf07604de5a412d1eed2d3ba6ce191a85b88464c5848a50bef42566de5
+size 60411
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png
new file mode 100644
index 0000000000..7a71a5ff0b
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/08.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f296bbd4b5637d1583ea337e8b807b34613640e0eabfb5e13e4e6cefe8ae2527
+size 58793
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png
new file mode 100644
index 0000000000..e8c9eb3f24
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/09.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b16c16f9663b5ba80fa2ef06503851009b15700ff257375bd41cdb362098a391
+size 57157
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png
new file mode 100644
index 0000000000..05d5ab1d0f
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5c39781b77219a6e9c05233d2376dfde04bd0dbe39f63274168073abf7a0e4d
+size 55424
diff --git a/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png
new file mode 100644
index 0000000000..ae6ce177b0
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_AnimatedLossy_VerifyAllFrames_Rgba32_leo_animated_lossy.webp/11.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5133dc9a5f8f6d26d388f40fd1df3a262f489d80a0d1eed588f7662bef7523de
+size 59950
diff --git a/tests/Images/Input/Webp/bike_lossless_small.webp b/tests/Images/Input/Webp/bike_lossy_small.webp
similarity index 100%
rename from tests/Images/Input/Webp/bike_lossless_small.webp
rename to tests/Images/Input/Webp/bike_lossy_small.webp
diff --git a/tests/Images/Input/Webp/bike_lossy.webp b/tests/Images/Input/Webp/bike_lossy_with_exif.webp
similarity index 100%
rename from tests/Images/Input/Webp/bike_lossy.webp
rename to tests/Images/Input/Webp/bike_lossy_with_exif.webp
diff --git a/tests/Images/Input/Webp/leo_animated_lossless.webp b/tests/Images/Input/Webp/leo_animated_lossless.webp
new file mode 100644
index 0000000000..3778e4a259
--- /dev/null
+++ b/tests/Images/Input/Webp/leo_animated_lossless.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bab815db08e8f413c7a355b7e9c152e1a73e503392012af16ada92858706d255
+size 400342
diff --git a/tests/Images/Input/Webp/leo_animated_lossy.webp b/tests/Images/Input/Webp/leo_animated_lossy.webp
new file mode 100644
index 0000000000..3bd434bc27
--- /dev/null
+++ b/tests/Images/Input/Webp/leo_animated_lossy.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:00fffbb0d67b0336574d9bad9cbacaf97d81f2e70db3d458508c430e3d103228
+size 64972