diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs
new file mode 100644
index 0000000000..8f06e5886f
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs
@@ -0,0 +1,124 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Common.Helpers;
+
+internal static class RiffHelper
+{
+ ///
+ /// The header bytes identifying RIFF file.
+ ///
+ private const uint RiffFourCc = 0x52_49_46_46;
+
+ public static void WriteRiffFile(Stream stream, string formType, Action func) =>
+ WriteChunk(stream, RiffFourCc, s =>
+ {
+ s.Write(Encoding.ASCII.GetBytes(formType));
+ func(s);
+ });
+
+ public static void WriteChunk(Stream stream, uint fourCc, Action func)
+ {
+ Span buffer = stackalloc byte[4];
+
+ // write the fourCC
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
+ stream.Write(buffer);
+
+ long sizePosition = stream.Position;
+ stream.Position += 4;
+
+ func(stream);
+
+ long position = stream.Position;
+
+ uint dataSize = (uint)(position - sizePosition - 4);
+
+ // padding
+ if (dataSize % 2 == 1)
+ {
+ stream.WriteByte(0);
+ position++;
+ }
+
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
+ stream.Position = sizePosition;
+ stream.Write(buffer);
+ stream.Position = position;
+ }
+
+ public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data)
+ {
+ Span buffer = stackalloc byte[4];
+
+ // write the fourCC
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
+ stream.Write(buffer);
+ uint size = (uint)data.Length;
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, size);
+ stream.Write(buffer);
+ stream.Write(data);
+
+ // padding
+ if (size % 2 is 1)
+ {
+ stream.WriteByte(0);
+ }
+ }
+
+ public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk)
+ where TStruct : unmanaged
+ {
+ fixed (TStruct* ptr = &chunk)
+ {
+ WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct)));
+ }
+ }
+
+ public static long BeginWriteChunk(Stream stream, uint fourCc)
+ {
+ Span buffer = stackalloc byte[4];
+
+ // write the fourCC
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
+ stream.Write(buffer);
+
+ long sizePosition = stream.Position;
+ stream.Position += 4;
+
+ return sizePosition;
+ }
+
+ public static void EndWriteChunk(Stream stream, long sizePosition)
+ {
+ Span buffer = stackalloc byte[4];
+
+ long position = stream.Position;
+
+ uint dataSize = (uint)(position - sizePosition - 4);
+
+ // padding
+ if (dataSize % 2 is 1)
+ {
+ stream.WriteByte(0);
+ position++;
+ }
+
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
+ stream.Position = sizePosition;
+ stream.Write(buffer);
+ stream.Position = position;
+ }
+
+ public static long BeginWriteRiffFile(Stream stream, string formType)
+ {
+ long sizePosition = BeginWriteChunk(stream, RiffFourCc);
+ stream.Write(Encoding.ASCII.GetBytes(formType));
+ return sizePosition;
+ }
+
+ public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition);
+}
diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
index 289ebd35ca..63e6541354 100644
--- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
@@ -59,7 +59,7 @@ public AlphaDecoder(int width, int height, IMemoryOwner data, byte alphaCh
if (this.Compressed)
{
- Vp8LBitReader bitReader = new(data);
+ Vp8LBitReader bitReader = new Vp8LBitReader(data);
this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration);
this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true);
diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
index 596715b205..cbd2aa8e7f 100644
--- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs
@@ -19,7 +19,7 @@ internal static class AlphaEncoder
/// Data is either compressed as lossless webp image or uncompressed.
///
/// The pixel format.
- /// The to encode from.
+ /// The to encode from.
/// The global configuration.
/// The memory manager.
/// Whether to skip metadata encoding.
@@ -27,7 +27,7 @@ internal static class AlphaEncoder
/// The size in bytes of the alpha data.
/// The encoded alpha data.
public static IMemoryOwner EncodeAlpha(
- Image image,
+ ImageFrame frame,
Configuration configuration,
MemoryAllocator memoryAllocator,
bool skipMetadata,
@@ -35,9 +35,9 @@ public static IMemoryOwner EncodeAlpha(
out int size)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
- IMemoryOwner alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator);
+ int width = frame.Width;
+ int height = frame.Height;
+ IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator);
if (compress)
{
@@ -58,9 +58,9 @@ public static IMemoryOwner EncodeAlpha(
// The transparency information will be stored in the green channel of the ARGB quadruplet.
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression.
- using Image alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan());
+ using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan());
- size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, alphaData);
+ size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData);
return alphaData;
}
@@ -73,19 +73,19 @@ public static IMemoryOwner EncodeAlpha(
/// Store the transparency in the green channel.
///
/// The pixel format.
- /// The to encode from.
+ /// The to encode from.
/// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.
- /// The transparency image.
- private static Image DispatchAlphaToGreen(Image image, Span alphaData)
+ /// The transparency frame.
+ private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
- Image alphaAsImage = new(width, height);
+ int width = frame.Width;
+ int height = frame.Height;
+ ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height);
for (int y = 0; y < height; y++)
{
- Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y);
+ Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y);
Span pixelRow = rowBuffer.Span;
Span alphaRow = alphaData.Slice(y * width, width);
for (int x = 0; x < width; x++)
@@ -95,23 +95,23 @@ private static Image DispatchAlphaToGreen(Image image, S
}
}
- return alphaAsImage;
+ return alphaAsFrame;
}
///
/// Extract the alpha data of the image.
///
/// The pixel format.
- /// The to encode from.
+ /// The to encode from.
/// The global configuration.
/// The memory manager.
/// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.
- private static IMemoryOwner ExtractAlphaChannel(Image image, Configuration configuration, MemoryAllocator memoryAllocator)
+ private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer;
- int height = image.Height;
- int width = image.Width;
+ Buffer2D imageBuffer = frame.PixelBuffer;
+ int height = frame.Height;
+ int width = frame.Width;
IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height);
Span alphaData = alphaDataBuffer.GetSpan();
diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs
deleted file mode 100644
index 714ec428ec..0000000000
--- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-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/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index ab78d18604..d502fd6063 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -1,9 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers.Binary;
-using System.Runtime.InteropServices;
+using System.Diagnostics;
+using SixLabors.ImageSharp.Common.Helpers;
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@@ -14,18 +16,11 @@ internal abstract class BitWriterBase
private const ulong MaxCanvasPixels = 4294967295ul;
- protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
-
///
/// Buffer to write to.
///
private byte[] buffer;
- ///
- /// A scratch buffer to reduce allocations.
- ///
- private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly
-
///
/// Initializes a new instance of the class.
///
@@ -41,17 +36,23 @@ internal abstract class BitWriterBase
public byte[] Buffer => this.buffer;
+ ///
+ /// Gets the number of bytes of the encoded image data.
+ ///
+ /// The number of bytes of the image data.
+ public abstract int NumBytes { get; }
+
///
/// Writes the encoded bytes of the image to the stream. Call Finish() before this.
///
/// The stream to write to.
- public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes()));
+ public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes));
///
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
///
/// The destination buffer.
- public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest);
+ public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest);
///
/// Resizes the buffer to write to.
@@ -59,12 +60,6 @@ internal abstract class BitWriterBase
/// The extra size in bytes needed.
public abstract void BitWriterResize(int extraSize);
- ///
- /// Returns the number of bytes of the encoded image data.
- ///
- /// The number of bytes of the image data.
- public abstract int NumBytes();
-
///
/// Flush leftover bits.
///
@@ -84,63 +79,89 @@ protected void ResizeBuffer(int maxBytes, int sizeRequired)
}
///
- /// Writes the RIFF header to the stream.
+ /// Write the trunks before data trunk.
///
/// The stream to write to.
- /// The block length.
- protected void WriteRiffHeader(Stream stream, uint riffSize)
+ /// The width of the image.
+ /// The height of the image.
+ /// The exif profile.
+ /// The XMP profile.
+ /// The color profile.
+ /// Flag indicating, if a alpha channel is present.
+ /// Flag indicating, if an animation parameter is present.
+ public static void WriteTrunksBeforeData(
+ Stream stream,
+ uint width,
+ uint height,
+ ExifProfile? exifProfile,
+ XmpProfile? xmpProfile,
+ IccProfile? iccProfile,
+ bool hasAlpha,
+ bool hasAnimation)
{
- stream.Write(WebpConstants.RiffFourCc);
- BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
- stream.Write(this.scratchBuffer.Span.Slice(0, 4));
- stream.Write(WebpConstants.WebpHeader);
+ // Write file size later
+ long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
+
+ Debug.Assert(pos is 4, "Stream should be written from position 0.");
+
+ // Write VP8X, header if necessary.
+ bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
+ if (isVp8X)
+ {
+ WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
+
+ if (iccProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
+ }
+ }
}
///
- /// Calculates the chunk size of EXIF, XMP or ICCP metadata.
+ /// Writes the encoded image to the stream.
///
- /// The metadata profile bytes.
- /// The metadata chunk size in bytes.
- protected static uint MetadataChunkSize(byte[] metadataBytes)
- {
- uint metaSize = (uint)metadataBytes.Length;
- return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
- }
+ /// The stream to write to.
+ public abstract void WriteEncodedImageToStream(Stream stream);
///
- /// Calculates the chunk size of a alpha chunk.
+ /// Write the trunks after data trunk.
///
- /// The alpha chunk bytes.
- /// The alpha data chunk size in bytes.
- protected static uint AlphaChunkSize(Span alphaBytes)
+ /// The stream to write to.
+ /// The exif profile.
+ /// The XMP profile.
+ public static void WriteTrunksAfterData(
+ Stream stream,
+ ExifProfile? exifProfile,
+ XmpProfile? xmpProfile)
{
- uint alphaSize = (uint)alphaBytes.Length + 1;
- return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
+ if (exifProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
+ }
+
+ if (xmpProfile != null)
+ {
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
+ }
+
+ RiffHelper.EndWriteRiffFile(stream, 4);
}
///
- /// Writes a metadata profile (EXIF or XMP) to the stream.
+ /// Writes the animation parameter() to the stream.
///
/// The stream to write to.
- /// The metadata profile's bytes.
- /// The chuck type to write.
- protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
+ ///
+ /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
+ /// This color MAY be used to fill the unused space on the canvas around the frames,
+ /// as well as the transparent pixels of the first frame.
+ /// The background color is also used when the Disposal method is 1.
+ ///
+ /// The number of times to loop the animation. If it is 0, this means infinitely.
+ public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount)
{
- DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
-
- uint size = (uint)metadataBytes.Length;
- Span buf = this.scratchBuffer.Span.Slice(0, 4);
- BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
- stream.Write(buf);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
- stream.Write(buf);
- stream.Write(metadataBytes);
-
- // Add padding byte if needed.
- if ((size & 1) == 1)
- {
- stream.WriteByte(0);
- }
+ WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
+ chunk.WriteTo(stream);
}
///
@@ -149,53 +170,19 @@ protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpCh
/// The stream to write to.
/// The alpha channel data bytes.
/// Indicates, if the alpha channel data is compressed.
- protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed)
+ public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed)
{
- uint size = (uint)dataBytes.Length + 1;
- Span buf = this.scratchBuffer.Span.Slice(0, 4);
- BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
- stream.Write(buf);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
- stream.Write(buf);
-
+ long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha);
byte flags = 0;
if (alphaDataIsCompressed)
{
- flags |= 1;
+ // TODO: Filtering and preprocessing
+ flags = 1;
}
stream.WriteByte(flags);
stream.Write(dataBytes);
-
- // Add padding byte if needed.
- if ((size & 1) == 1)
- {
- stream.WriteByte(0);
- }
- }
-
- ///
- /// Writes the color profile to the stream.
- ///
- /// The stream to write to.
- /// The color profile bytes.
- protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
- {
- uint size = (uint)iccProfileBytes.Length;
-
- Span buf = this.scratchBuffer.Span.Slice(0, 4);
- BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp);
- stream.Write(buf);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
- stream.Write(buf);
-
- stream.Write(iccProfileBytes);
-
- // Add padding byte if needed.
- if ((size & 1) == 1)
- {
- stream.WriteByte(0);
- }
+ RiffHelper.EndWriteChunk(stream, pos);
}
///
@@ -204,65 +191,17 @@ protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
/// The stream to write to.
/// A exif profile or null, if it does not exist.
/// A XMP profile or null, if it does not exist.
- /// The color profile bytes.
+ /// The color profile.
/// The width of the image.
/// The height of the image.
/// Flag indicating, if a alpha channel is present.
- protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha)
+ /// Flag indicating, if an animation parameter is present.
+ protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation)
{
- if (width > MaxDimension || height > MaxDimension)
- {
- WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}");
- }
-
- // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1.
- if (width * height > MaxCanvasPixels)
- {
- WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1");
- }
-
- uint flags = 0;
- if (exifProfile != null)
- {
- // Set exif bit.
- flags |= 8;
- }
-
- if (xmpProfile != null)
- {
- // Set xmp bit.
- flags |= 4;
- }
+ WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
- if (hasAlpha)
- {
- // Set alpha bit.
- flags |= 16;
- }
-
- if (iccProfileBytes != null)
- {
- // Set iccp flag.
- flags |= 32;
- }
-
- Span buf = this.scratchBuffer.Span.Slice(0, 4);
- stream.Write(WebpConstants.Vp8XMagicBytes);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
- stream.Write(buf);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, flags);
- stream.Write(buf);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, width - 1);
- stream.Write(buf[..3]);
- BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1);
- stream.Write(buf[..3]);
- }
-
- private unsafe struct ScratchBuffer
- {
- private const int Size = 4;
- private fixed byte scratch[Size];
+ chunk.Validate(MaxDimension, MaxCanvasPixels);
- public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
+ chunk.WriteTo(stream);
}
}
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
index 5b4eab64a3..81530706d6 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
@@ -3,9 +3,6 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
-using SixLabors.ImageSharp.Metadata.Profiles.Exif;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
-using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@@ -72,7 +69,7 @@ public Vp8BitWriter(int expectedSize, Vp8Encoder enc)
}
///
- public override int NumBytes() => (int)this.pos;
+ public override int NumBytes => (int)this.pos;
public int PutCoeffs(int ctx, Vp8Residual residual)
{
@@ -116,7 +113,7 @@ public int PutCoeffs(int ctx, Vp8Residual residual)
else
{
this.PutBit(v >= 9, 165);
- this.PutBit(!((v & 1) != 0), 145);
+ this.PutBit((v & 1) == 0, 145);
}
}
else
@@ -394,87 +391,28 @@ private void Flush()
}
}
- ///
- /// Writes the encoded image to the stream.
- ///
- /// The stream to write to.
- /// The exif profile.
- /// The XMP profile.
- /// The color profile.
- /// The width of the image.
- /// The height of the image.
- /// Flag indicating, if a alpha channel is present.
- /// The alpha channel data.
- /// Indicates, if the alpha data is compressed.
- public void WriteEncodedImageToStream(
- Stream stream,
- ExifProfile? exifProfile,
- XmpProfile? xmpProfile,
- IccProfile? iccProfile,
- uint width,
- uint height,
- bool hasAlpha,
- Span alphaData,
- bool alphaDataIsCompressed)
+ ///
+ public override void WriteEncodedImageToStream(Stream stream)
{
- bool isVp8X = false;
- byte[]? exifBytes = null;
- byte[]? xmpBytes = null;
- byte[]? iccProfileBytes = null;
- uint riffSize = 0;
- if (exifProfile != null)
- {
- isVp8X = true;
- exifBytes = exifProfile.ToByteArray();
- riffSize += MetadataChunkSize(exifBytes!);
- }
-
- if (xmpProfile != null)
- {
- isVp8X = true;
- xmpBytes = xmpProfile.Data;
- riffSize += MetadataChunkSize(xmpBytes!);
- }
-
- if (iccProfile != null)
- {
- isVp8X = true;
- iccProfileBytes = iccProfile.ToByteArray();
- riffSize += MetadataChunkSize(iccProfileBytes);
- }
-
- if (hasAlpha)
- {
- isVp8X = true;
- riffSize += AlphaChunkSize(alphaData);
- }
-
- if (isVp8X)
- {
- riffSize += ExtendedFileChunkSize;
- }
+ uint numBytes = (uint)this.NumBytes;
- this.Finish();
- uint numBytes = (uint)this.NumBytes();
int mbSize = this.enc.Mbw * this.enc.Mbh;
int expectedSize = (int)((uint)mbSize * 7 / 8);
- Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc);
+ Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(expectedSize, this.enc);
// Partition #0 with header and partition sizes.
- uint size0 = this.GeneratePartition0(bitWriterPartZero);
+ uint size0 = bitWriterPartZero.GeneratePartition0();
uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0;
vp8Size += numBytes;
uint pad = vp8Size & 1;
vp8Size += pad;
- // Compute RIFF size.
- // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size.
- riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
+ // Emit header and partition #0
+ this.WriteVp8Header(stream, vp8Size);
+ this.WriteFrameHeader(stream, size0);
- // Emit headers and partition #0
- this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
@@ -483,59 +421,49 @@ public void WriteEncodedImageToStream(
{
stream.WriteByte(0);
}
-
- if (exifProfile != null)
- {
- this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
- }
-
- if (xmpProfile != null)
- {
- this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
- }
}
- private uint GeneratePartition0(Vp8BitWriter bitWriter)
+ private uint GeneratePartition0()
{
- bitWriter.PutBitUniform(0); // colorspace
- bitWriter.PutBitUniform(0); // clamp type
+ this.PutBitUniform(0); // colorspace
+ this.PutBitUniform(0); // clamp type
- this.WriteSegmentHeader(bitWriter);
- this.WriteFilterHeader(bitWriter);
+ this.WriteSegmentHeader();
+ this.WriteFilterHeader();
- bitWriter.PutBits(0, 2);
+ this.PutBits(0, 2);
- this.WriteQuant(bitWriter);
- bitWriter.PutBitUniform(0);
- this.WriteProbas(bitWriter);
- this.CodeIntraModes(bitWriter);
+ this.WriteQuant();
+ this.PutBitUniform(0);
+ this.WriteProbas();
+ this.CodeIntraModes();
- bitWriter.Finish();
+ this.Finish();
- return (uint)bitWriter.NumBytes();
+ return (uint)this.NumBytes;
}
- private void WriteSegmentHeader(Vp8BitWriter bitWriter)
+ private void WriteSegmentHeader()
{
Vp8EncSegmentHeader hdr = this.enc.SegmentHeader;
Vp8EncProba proba = this.enc.Proba;
- if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0)
+ if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0)
{
// We always 'update' the quant and filter strength values.
int updateData = 1;
- bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0);
- if (bitWriter.PutBitUniform(updateData) != 0)
+ this.PutBitUniform(hdr.UpdateMap ? 1 : 0);
+ if (this.PutBitUniform(updateData) != 0)
{
// We always use absolute values, not relative ones.
- bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.)
+ this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.)
for (int s = 0; s < WebpConstants.NumMbSegments; ++s)
{
- bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7);
+ this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7);
}
for (int s = 0; s < WebpConstants.NumMbSegments; ++s)
{
- bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6);
+ this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6);
}
}
@@ -543,50 +471,50 @@ private void WriteSegmentHeader(Vp8BitWriter bitWriter)
{
for (int s = 0; s < 3; ++s)
{
- if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0)
+ if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0)
{
- bitWriter.PutBits(proba.Segments[s], 8);
+ this.PutBits(proba.Segments[s], 8);
}
}
}
}
}
- private void WriteFilterHeader(Vp8BitWriter bitWriter)
+ private void WriteFilterHeader()
{
Vp8FilterHeader hdr = this.enc.FilterHeader;
bool useLfDelta = hdr.I4x4LfDelta != 0;
- bitWriter.PutBitUniform(hdr.Simple ? 1 : 0);
- bitWriter.PutBits((uint)hdr.FilterLevel, 6);
- bitWriter.PutBits((uint)hdr.Sharpness, 3);
- if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0)
+ this.PutBitUniform(hdr.Simple ? 1 : 0);
+ this.PutBits((uint)hdr.FilterLevel, 6);
+ this.PutBits((uint)hdr.Sharpness, 3);
+ if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0)
{
// '0' is the default value for i4x4LfDelta at frame #0.
bool needUpdate = hdr.I4x4LfDelta != 0;
- if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0)
+ if (this.PutBitUniform(needUpdate ? 1 : 0) != 0)
{
// we don't use refLfDelta => emit four 0 bits.
- bitWriter.PutBits(0, 4);
+ this.PutBits(0, 4);
// we use modeLfDelta for i4x4
- bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6);
- bitWriter.PutBits(0, 3); // all others unused.
+ this.PutSignedBits(hdr.I4x4LfDelta, 6);
+ this.PutBits(0, 3); // all others unused.
}
}
}
// Nominal quantization parameters
- private void WriteQuant(Vp8BitWriter bitWriter)
+ private void WriteQuant()
{
- bitWriter.PutBits((uint)this.enc.BaseQuant, 7);
- bitWriter.PutSignedBits(this.enc.DqY1Dc, 4);
- bitWriter.PutSignedBits(this.enc.DqY2Dc, 4);
- bitWriter.PutSignedBits(this.enc.DqY2Ac, 4);
- bitWriter.PutSignedBits(this.enc.DqUvDc, 4);
- bitWriter.PutSignedBits(this.enc.DqUvAc, 4);
+ this.PutBits((uint)this.enc.BaseQuant, 7);
+ this.PutSignedBits(this.enc.DqY1Dc, 4);
+ this.PutSignedBits(this.enc.DqY2Dc, 4);
+ this.PutSignedBits(this.enc.DqY2Ac, 4);
+ this.PutSignedBits(this.enc.DqUvDc, 4);
+ this.PutSignedBits(this.enc.DqUvAc, 4);
}
- private void WriteProbas(Vp8BitWriter bitWriter)
+ private void WriteProbas()
{
Vp8EncProba probas = this.enc.Proba;
for (int t = 0; t < WebpConstants.NumTypes; ++t)
@@ -599,25 +527,25 @@ private void WriteProbas(Vp8BitWriter bitWriter)
{
byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p];
bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p];
- if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p]))
+ if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p]))
{
- bitWriter.PutBits(p0, 8);
+ this.PutBits(p0, 8);
}
}
}
}
}
- if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0)
+ if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0)
{
- bitWriter.PutBits(probas.SkipProba, 8);
+ this.PutBits(probas.SkipProba, 8);
}
}
// Writes the partition #0 modes (that is: all intra modes)
- private void CodeIntraModes(Vp8BitWriter bitWriter)
+ private void CodeIntraModes()
{
- var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh);
+ Vp8EncIterator it = new Vp8EncIterator(this.enc);
int predsWidth = this.enc.PredsWidth;
do
@@ -627,18 +555,18 @@ private void CodeIntraModes(Vp8BitWriter bitWriter)
Span preds = it.Preds.AsSpan(predIdx);
if (this.enc.SegmentHeader.UpdateMap)
{
- bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments);
+ this.PutSegment(mb.Segment, this.enc.Proba.Segments);
}
if (this.enc.Proba.UseSkipProba)
{
- bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba);
+ this.PutBit(mb.Skip, this.enc.Proba.SkipProba);
}
- if (bitWriter.PutBit(mb.MacroBlockType != 0, 145))
+ if (this.PutBit(mb.MacroBlockType != 0, 145))
{
// i16x16
- bitWriter.PutI16Mode(preds[0]);
+ this.PutI16Mode(preds[0]);
}
else
{
@@ -649,7 +577,7 @@ private void CodeIntraModes(Vp8BitWriter bitWriter)
for (int x = 0; x < 4; x++)
{
byte[] probas = WebpLookupTables.ModesProba[topPred[x], left];
- left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas);
+ left = this.PutI4Mode(it.Preds[predIdx + x], probas);
}
topPred = it.Preds.AsSpan(predIdx);
@@ -657,56 +585,18 @@ private void CodeIntraModes(Vp8BitWriter bitWriter)
}
}
- bitWriter.PutUvMode(mb.UvMode);
+ this.PutUvMode(mb.UvMode);
}
while (it.Next());
}
- private void WriteWebpHeaders(
- Stream stream,
- uint size0,
- uint vp8Size,
- uint riffSize,
- bool isVp8X,
- uint width,
- uint height,
- ExifProfile? exifProfile,
- XmpProfile? xmpProfile,
- byte[]? iccProfileBytes,
- bool hasAlpha,
- Span alphaData,
- bool alphaDataIsCompressed)
- {
- this.WriteRiffHeader(stream, riffSize);
-
- // Write VP8X, header if necessary.
- if (isVp8X)
- {
- this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha);
-
- if (iccProfileBytes != null)
- {
- this.WriteColorProfile(stream, iccProfileBytes);
- }
-
- if (hasAlpha)
- {
- this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
- }
- }
-
- this.WriteVp8Header(stream, vp8Size);
- this.WriteFrameHeader(stream, size0);
- }
-
private void WriteVp8Header(Stream stream, uint size)
{
- Span vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize];
-
- WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader);
- BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size);
-
- stream.Write(vp8ChunkHeader);
+ Span buf = stackalloc byte[WebpConstants.TagSize];
+ BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8);
+ stream.Write(buf);
+ BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
+ stream.Write(buf);
}
private void WriteFrameHeader(Stream stream, uint size0)
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
index 9dc7912392..dc867fa85e 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
@@ -3,9 +3,6 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
-using SixLabors.ImageSharp.Metadata.Profiles.Exif;
-using SixLabors.ImageSharp.Metadata.Profiles.Icc;
-using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@@ -59,6 +56,9 @@ private Vp8LBitWriter(byte[] buffer, ulong bits, int used, int cur)
this.cur = cur;
}
+ ///
+ public override int NumBytes => this.cur + ((this.used + 7) >> 3);
+
///
/// This function writes bits into bytes in increasing addresses (little endian),
/// and within a byte least-significant-bit first. This function can write up to 32 bits in one go.
@@ -98,9 +98,6 @@ public void WriteHuffmanCodeWithExtraBits(HuffmanTreeCode code, int codeIndex, i
this.PutBits((uint)((bits << depth) | symbol), depth + nBits);
}
- ///
- public override int NumBytes() => this.cur + ((this.used + 7) >> 3);
-
public Vp8LBitWriter Clone()
{
byte[] clonedBuffer = new byte[this.Buffer.Length];
@@ -122,76 +119,20 @@ public override void Finish()
this.used = 0;
}
- ///
- /// Writes the encoded image to the stream.
- ///
- /// The stream to write to.
- /// The exif profile.
- /// The XMP profile.
- /// The color profile.
- /// The width of the image.
- /// The height of the image.
- /// Flag indicating, if a alpha channel is present.
- public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha)
+ ///
+ public override void WriteEncodedImageToStream(Stream stream)
{
- bool isVp8X = false;
- byte[]? exifBytes = null;
- byte[]? xmpBytes = null;
- byte[]? iccBytes = null;
- uint riffSize = 0;
- if (exifProfile != null)
- {
- isVp8X = true;
- exifBytes = exifProfile.ToByteArray();
- riffSize += MetadataChunkSize(exifBytes!);
- }
-
- if (xmpProfile != null)
- {
- isVp8X = true;
- xmpBytes = xmpProfile.Data;
- riffSize += MetadataChunkSize(xmpBytes!);
- }
-
- if (iccProfile != null)
- {
- isVp8X = true;
- iccBytes = iccProfile.ToByteArray();
- riffSize += MetadataChunkSize(iccBytes);
- }
-
- if (isVp8X)
- {
- riffSize += ExtendedFileChunkSize;
- }
-
- this.Finish();
- uint size = (uint)this.NumBytes();
- size++; // One byte extra for the VP8L signature.
-
- // Write RIFF header.
+ uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature
uint pad = size & 1;
- riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad;
- this.WriteRiffHeader(stream, riffSize);
-
- // Write VP8X, header if necessary.
- if (isVp8X)
- {
- this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha);
-
- if (iccBytes != null)
- {
- this.WriteColorProfile(stream, iccBytes);
- }
- }
// Write magic bytes indicating its a lossless webp.
- stream.Write(WebpConstants.Vp8LMagicBytes);
+ Span scratchBuffer = stackalloc byte[WebpConstants.TagSize];
+ BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L);
+ stream.Write(scratchBuffer);
// Write Vp8 Header.
- Span scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size);
- stream.Write(scratchBuffer.Slice(0, 4));
+ stream.Write(scratchBuffer);
stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte);
// Write the encoded bytes of the image to the stream.
@@ -200,16 +141,6 @@ public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, X
{
stream.WriteByte(0);
}
-
- if (exifProfile != null)
- {
- this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
- }
-
- if (xmpProfile != null)
- {
- this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
- }
}
///
@@ -226,7 +157,7 @@ private void PutBitsFlushBits()
Span scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits);
- scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur));
+ scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur));
this.cur += WriterBytes;
this.bits >>= WriterBits;
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs
new file mode 100644
index 0000000000..3855a293c1
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using SixLabors.ImageSharp.Common.Helpers;
+
+namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
+
+internal readonly struct WebpAnimationParameter
+{
+ public WebpAnimationParameter(uint background, ushort loopCount)
+ {
+ this.Background = background;
+ this.LoopCount = loopCount;
+ }
+
+ ///
+ /// Gets default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
+ /// This color MAY be used to fill the unused space on the canvas around the frames,
+ /// as well as the transparent pixels of the first frame.
+ /// The background color is also used when the Disposal method is 1.
+ ///
+ public uint Background { get; }
+
+ ///
+ /// Gets number of times to loop the animation. If it is 0, this means infinitely.
+ ///
+ public ushort LoopCount { get; }
+
+ public void WriteTo(Stream stream)
+ {
+ Span buffer = stackalloc byte[6];
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background);
+ BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount);
+ RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer);
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
new file mode 100644
index 0000000000..f22a3fd540
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Common.Helpers;
+
+namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
+
+internal readonly struct WebpFrameData
+{
+ ///
+ /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags.
+ ///
+ public const uint HeaderSize = 16;
+
+ public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod)
+ {
+ this.DataSize = dataSize;
+ this.X = x;
+ this.Y = y;
+ this.Width = width;
+ this.Height = height;
+ this.Duration = duration;
+ this.DisposalMethod = disposalMethod;
+ this.BlendingMethod = blendingMethod;
+ }
+
+ public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, int flags)
+ : this(
+ dataSize,
+ x,
+ y,
+ width,
+ height,
+ duration,
+ (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending,
+ (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose)
+ {
+ }
+
+ public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod)
+ : this(0, x, y, width, height, duration, blendingMethod, disposalMethod)
+ {
+ }
+
+ ///
+ /// Gets the animation chunk size.
+ ///
+ public uint DataSize { get; }
+
+ ///
+ /// Gets the X coordinate of the upper left corner of the frame is Frame X * 2.
+ ///
+ public uint X { get; }
+
+ ///
+ /// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2.
+ ///
+ public uint Y { get; }
+
+ ///
+ /// Gets the width of the frame.
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets the height of the frame.
+ ///
+ public uint Height { get; }
+
+ ///
+ /// Gets 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 { get; }
+
+ ///
+ /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
+ ///
+ public WebpBlendingMethod BlendingMethod { get; }
+
+ ///
+ /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
+ ///
+ public WebpDisposalMethod DisposalMethod { get; }
+
+ public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height);
+
+ ///
+ /// Writes the animation frame() to the stream.
+ ///
+ /// The stream to write to.
+ public long WriteHeaderTo(Stream stream)
+ {
+ byte flags = 0;
+
+ if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend)
+ {
+ // Set blending flag.
+ flags |= 2;
+ }
+
+ if (this.DisposalMethod is WebpDisposalMethod.Dispose)
+ {
+ // Set disposal flag.
+ flags |= 1;
+ }
+
+ long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData);
+
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration);
+ stream.WriteByte(flags);
+
+ return pos;
+ }
+
+ ///
+ /// Reads the animation frame header.
+ ///
+ /// The stream to read from.
+ /// Animation frame data.
+ public static WebpFrameData Parse(Stream stream)
+ {
+ Span buffer = stackalloc byte[4];
+
+ WebpFrameData data = new(
+ dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
+ x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
+ y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
+ width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
+ height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
+ duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
+ flags: stream.ReadByte());
+
+ return data;
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
new file mode 100644
index 0000000000..70d6870ce4
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Common.Helpers;
+
+namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
+
+internal readonly struct WebpVp8X
+{
+ public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height)
+ {
+ this.HasAnimation = hasAnimation;
+ this.HasXmp = hasXmp;
+ this.HasExif = hasExif;
+ this.HasAlpha = hasAlpha;
+ this.HasIcc = hasIcc;
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation.
+ ///
+ public bool HasAnimation { get; }
+
+ ///
+ /// Gets a value indicating whether the file contains XMP metadata.
+ ///
+ public bool HasXmp { get; }
+
+ ///
+ /// Gets a value indicating whether the file contains Exif metadata.
+ ///
+ public bool HasExif { get; }
+
+ ///
+ /// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha").
+ ///
+ public bool HasAlpha { get; }
+
+ ///
+ /// Gets a value indicating whether the file contains an 'ICCP' Chunk.
+ ///
+ public bool HasIcc { get; }
+
+ ///
+ /// Gets width of the canvas in pixels. (uint24)
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets height of the canvas in pixels. (uint24)
+ ///
+ public uint Height { get; }
+
+ public void Validate(uint maxDimension, ulong maxCanvasPixels)
+ {
+ if (this.Width > maxDimension || this.Height > maxDimension)
+ {
+ WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}");
+ }
+
+ // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1.
+ if (this.Width * this.Height > maxCanvasPixels)
+ {
+ WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1");
+ }
+ }
+
+ public void WriteTo(Stream stream)
+ {
+ byte flags = 0;
+
+ if (this.HasAnimation)
+ {
+ // Set animated flag.
+ flags |= 2;
+ }
+
+ if (this.HasXmp)
+ {
+ // Set xmp bit.
+ flags |= 4;
+ }
+
+ if (this.HasExif)
+ {
+ // Set exif bit.
+ flags |= 8;
+ }
+
+ if (this.HasAlpha)
+ {
+ // Set alpha bit.
+ flags |= 16;
+ }
+
+ if (this.HasIcc)
+ {
+ // Set icc flag.
+ flags |= 32;
+ }
+
+ long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X);
+
+ stream.WriteByte(flags);
+ stream.Position += 3; // Reserved bytes
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
+ WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);
+
+ RiffHelper.EndWriteChunk(stream, pos);
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
index 211185dbba..2e7dd722fc 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
@@ -779,7 +779,7 @@ private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs)
{
int pixelIndex = 0;
- ColorCache colorCache = new(cacheBits);
+ ColorCache colorCache = new ColorCache(cacheBits);
for (int idx = 0; idx < refs.Refs.Count; idx++)
{
PixOrCopy v = refs.Refs[idx];
diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
index e393c065ec..63ce9dbec6 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
@@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable
private const int FreeIntervalsStartCount = 25;
- private readonly Stack freeIntervals = new(FreeIntervalsStartCount);
+ private readonly Stack freeIntervals = new Stack(FreeIntervalsStartCount);
public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner distArray, int pixCount, CostModel costModel)
{
diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
index cedc809382..d6b10ada55 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
@@ -15,7 +15,7 @@ internal sealed class PixOrCopy
public uint BgraOrDistance { get; set; }
public static PixOrCopy CreateCacheIdx(int idx) =>
- new()
+ new PixOrCopy
{
Mode = PixOrCopyMode.CacheIdx,
BgraOrDistance = (uint)idx,
@@ -23,14 +23,15 @@ public static PixOrCopy CreateCacheIdx(int idx) =>
};
public static PixOrCopy CreateLiteral(uint bgra) =>
- new()
+ new PixOrCopy
{
Mode = PixOrCopyMode.Literal,
BgraOrDistance = bgra,
Len = 1
};
- public static PixOrCopy CreateCopy(uint distance, ushort len) => new()
+ public static PixOrCopy CreateCopy(uint distance, ushort len) =>
+ new PixOrCopy
{
Mode = PixOrCopyMode.Copy,
BgraOrDistance = distance,
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 878d487a86..4fdbb31d37 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -6,7 +6,9 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -235,26 +237,60 @@ public Vp8LEncoder(
///
public Vp8LHashChain HashChain { get; }
- ///
- /// Encodes the image as lossless webp to the specified stream.
- ///
- /// The pixel format.
- /// The to encode from.
- /// The to encode the image data to.
- public void Encode(Image image, Stream stream)
+ public void EncodeHeader(Image image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
-
+ // Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
+ BitWriterBase.WriteTrunksBeforeData(
+ stream,
+ (uint)image.Width,
+ (uint)image.Height,
+ exifProfile,
+ xmpProfile,
+ metadata.IccProfile,
+ false,
+ hasAnimation);
+
+ if (hasAnimation)
+ {
+ WebpMetadata webpMetadata = metadata.GetWebpMetadata();
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ }
+ }
+
+ public void EncodeFooter(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ // Write bytes from the bitwriter buffer to the stream.
+ ImageMetadata metadata = image.Metadata;
+
+ ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
+ XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
+
+ BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ }
+
+ ///
+ /// Encodes the image as lossless webp to the specified stream.
+ ///
+ /// The pixel format.
+ /// The to encode from.
+ /// The to encode the image data to.
+ /// Flag indicating, if an animation parameter is present.
+ public void Encode(ImageFrame frame, Stream stream, bool hasAnimation)
+ where TPixel : unmanaged, IPixel
+ {
+ int width = frame.Width;
+ int height = frame.Height;
+
// Convert image pixels to bgra array.
- bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
+ bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height);
// Write the image size.
this.WriteImageSize(width, height);
@@ -263,35 +299,60 @@ public void Encode(Image image, Stream stream)
this.WriteAlphaAndVersion(hasAlpha);
// Encode the main image stream.
- this.EncodeStream(image);
+ this.EncodeStream(frame);
+
+ this.bitWriter.Finish();
+
+ long prevPosition = 0;
+
+ if (hasAnimation)
+ {
+ WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
+
+ // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
+ prevPosition = new WebpFrameData(
+ 0,
+ 0,
+ (uint)frame.Width,
+ (uint)frame.Height,
+ frameMetadata.FrameDelay,
+ frameMetadata.BlendMethod,
+ frameMetadata.DisposalMethod)
+ .WriteHeaderTo(stream);
+ }
// Write bytes from the bitwriter buffer to the stream.
- this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
+ this.bitWriter.WriteEncodedImageToStream(stream);
+
+ if (hasAnimation)
+ {
+ RiffHelper.EndWriteChunk(stream, prevPosition);
+ }
}
///
/// Encodes the alpha image data using the webp lossless compression.
///
/// The type of the pixel.
- /// The to encode from.
+ /// The to encode from.
/// The destination buffer to write the encoded alpha data to.
/// The size of the compressed data in bytes.
/// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed.
///
- public int EncodeAlphaImageData(Image image, IMemoryOwner alphaData)
+ public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
+ int width = frame.Width;
+ int height = frame.Height;
int pixelCount = width * height;
// Convert image pixels to bgra array.
- this.ConvertPixelsToBgra(image, width, height);
+ this.ConvertPixelsToBgra(frame, width, height);
// The image-stream will NOT contain any headers describing the image dimension, the dimension is already known.
- this.EncodeStream(image);
+ this.EncodeStream(frame);
this.bitWriter.Finish();
- int size = this.bitWriter.NumBytes();
+ int size = this.bitWriter.NumBytes;
if (size >= pixelCount)
{
// Compressing would not yield in smaller data -> leave the data uncompressed.
@@ -333,12 +394,12 @@ private void WriteAlphaAndVersion(bool hasAlpha)
/// Encodes the image stream using lossless webp format.
///
/// The pixel type.
- /// The image to encode.
- private void EncodeStream(Image image)
+ /// The frame to encode.
+ private void EncodeStream(ImageFrame frame)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
+ int width = frame.Width;
+ int height = frame.Height;
Span bgra = this.Bgra.GetSpan();
Span encodedData = this.EncodedData.GetSpan();
@@ -425,9 +486,9 @@ private void EncodeStream(Image image)
lowEffort);
// If we are better than what we already have.
- if (isFirstConfig || this.bitWriter.NumBytes() < bestSize)
+ if (isFirstConfig || this.bitWriter.NumBytes < bestSize)
{
- bestSize = this.bitWriter.NumBytes();
+ bestSize = this.bitWriter.NumBytes;
BitWriterSwap(ref this.bitWriter, ref bitWriterBest);
}
@@ -447,14 +508,14 @@ private void EncodeStream(Image image)
/// Converts the pixels of the image to bgra.
///
/// The type of the pixels.
- /// The image to convert.
+ /// The frame to convert.
/// The width of the image.
/// The height of the image.
/// true, if the image is non opaque.
- private bool ConvertPixelsToBgra(Image image, int width, int height)
+ private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer;
+ Buffer2D imageBuffer = frame.PixelBuffer;
bool nonOpaque = false;
Span bgra = this.Bgra.GetSpan();
Span bgraBytes = MemoryMarshal.Cast(bgra);
@@ -682,7 +743,7 @@ private void EncodeImage(int width, int height, bool useCache, CrunchConfig conf
this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes);
// Keep track of the smallest image so far.
- if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes()))
+ if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes))
{
(bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
}
@@ -1154,35 +1215,41 @@ private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height,
entropyComp[j] = bitEntropy.BitsEntropyRefine();
}
- entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] +
- entropyComp[(int)HistoIx.HistoRed] +
- entropyComp[(int)HistoIx.HistoGreen] +
- entropyComp[(int)HistoIx.HistoBlue];
- entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] +
- entropyComp[(int)HistoIx.HistoRedPred] +
- entropyComp[(int)HistoIx.HistoGreenPred] +
- entropyComp[(int)HistoIx.HistoBluePred];
- entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] +
- entropyComp[(int)HistoIx.HistoRedSubGreen] +
- entropyComp[(int)HistoIx.HistoGreen] +
- entropyComp[(int)HistoIx.HistoBlueSubGreen];
- entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] +
- entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
- entropyComp[(int)HistoIx.HistoGreenPred] +
- entropyComp[(int)HistoIx.HistoBluePredSubGreen];
+ entropy[(int)EntropyIx.Direct] =
+ entropyComp[(int)HistoIx.HistoAlpha] +
+ entropyComp[(int)HistoIx.HistoRed] +
+ entropyComp[(int)HistoIx.HistoGreen] +
+ entropyComp[(int)HistoIx.HistoBlue];
+ entropy[(int)EntropyIx.Spatial] =
+ entropyComp[(int)HistoIx.HistoAlphaPred] +
+ entropyComp[(int)HistoIx.HistoRedPred] +
+ entropyComp[(int)HistoIx.HistoGreenPred] +
+ entropyComp[(int)HistoIx.HistoBluePred];
+ entropy[(int)EntropyIx.SubGreen] =
+ entropyComp[(int)HistoIx.HistoAlpha] +
+ entropyComp[(int)HistoIx.HistoRedSubGreen] +
+ entropyComp[(int)HistoIx.HistoGreen] +
+ entropyComp[(int)HistoIx.HistoBlueSubGreen];
+ entropy[(int)EntropyIx.SpatialSubGreen] =
+ entropyComp[(int)HistoIx.HistoAlphaPred] +
+ entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
+ entropyComp[(int)HistoIx.HistoGreenPred] +
+ entropyComp[(int)HistoIx.HistoBluePredSubGreen];
entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette];
// When including transforms, there is an overhead in bits from
// storing them. This overhead is small but matters for small images.
// For spatial, there are 14 transformations.
- entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) *
- LosslessUtils.SubSampleSize(height, transformBits) *
- LosslessUtils.FastLog2(14);
+ entropy[(int)EntropyIx.Spatial] +=
+ LosslessUtils.SubSampleSize(width, transformBits) *
+ LosslessUtils.SubSampleSize(height, transformBits) *
+ LosslessUtils.FastLog2(14);
// For color transforms: 24 as only 3 channels are considered in a ColorTransformElement.
- entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) *
- LosslessUtils.SubSampleSize(height, transformBits) *
- LosslessUtils.FastLog2(24);
+ entropy[(int)EntropyIx.SpatialSubGreen] +=
+ LosslessUtils.SubSampleSize(width, transformBits) *
+ LosslessUtils.SubSampleSize(height, transformBits) *
+ LosslessUtils.FastLog2(24);
// For palettes, add the cost of storing the palette.
// We empirically estimate the cost of a compressed entry as 8 bits.
@@ -1844,9 +1911,9 @@ public void AllocateTransformBuffer(int width, int height)
///
public void ClearRefs()
{
- for (int i = 0; i < this.Refs.Length; i++)
+ foreach (Vp8LBackwardRefs t in this.Refs)
{
- this.Refs[i].Refs.Clear();
+ t.Refs.Clear();
}
}
@@ -1855,9 +1922,9 @@ public void Dispose()
{
this.Bgra.Dispose();
this.EncodedData.Dispose();
- this.BgraScratch.Dispose();
+ this.BgraScratch?.Dispose();
this.Palette.Dispose();
- this.TransformData.Dispose();
+ this.TransformData?.Dispose();
this.HashChain.Dispose();
}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
index 19ea424199..e4c2a7ddf6 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
@@ -95,12 +95,10 @@ public WebpLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAlloca
public void Decode(Buffer2D pixels, int width, int height)
where TPixel : unmanaged, IPixel
{
- using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator))
- {
- this.DecodeImageStream(decoder, width, height, true);
- this.DecodeImageData(decoder, decoder.Pixels.Memory.Span);
- this.DecodePixelValues(decoder, pixels, width, height);
- }
+ using Vp8LDecoder decoder = new(width, height, this.memoryAllocator);
+ this.DecodeImageStream(decoder, width, height, true);
+ this.DecodeImageData(decoder, decoder.Pixels.Memory.Span);
+ this.DecodePixelValues(decoder, pixels, width, height);
}
public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0)
@@ -619,12 +617,9 @@ private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder)
Vp8LTransform transform = new(transformType, xSize, ySize);
// Each transform is allowed to be used only once.
- foreach (Vp8LTransform decoderTransform in decoder.Transforms)
+ if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType))
{
- if (decoderTransform.TransformType == transform.TransformType)
- {
- WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once");
- }
+ WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once");
}
switch (transformType)
@@ -744,61 +739,69 @@ public void DecodeAlphaData(AlphaDecoder dec)
this.bitReader.FillBitWindow();
int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]);
- if (code < WebpConstants.NumLiteralCodes)
+ switch (code)
{
- // Literal
- data[pos] = (byte)code;
- ++pos;
- ++col;
-
- if (col >= width)
+ case < WebpConstants.NumLiteralCodes:
{
- col = 0;
- ++row;
- if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
+ // Literal
+ data[pos] = (byte)code;
+ ++pos;
+ ++col;
+
+ if (col >= width)
{
- dec.ExtractPalettedAlphaRows(row);
+ col = 0;
+ ++row;
+ if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
+ {
+ dec.ExtractPalettedAlphaRows(row);
+ }
}
- }
- }
- else if (code < lenCodeLimit)
- {
- // Backward reference
- int lengthSym = code - WebpConstants.NumLiteralCodes;
- int length = this.GetCopyLength(lengthSym);
- int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]);
- this.bitReader.FillBitWindow();
- int distCode = this.GetCopyDistance(distSymbol);
- int dist = PlaneCodeToDistance(width, distCode);
- if (pos >= dist && end - pos >= length)
- {
- CopyBlock8B(data, pos, dist, length);
- }
- else
- {
- WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data");
+
+ break;
}
- pos += length;
- col += length;
- while (col >= width)
+ case < lenCodeLimit:
{
- col -= width;
- ++row;
- if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
+ // Backward reference
+ int lengthSym = code - WebpConstants.NumLiteralCodes;
+ int length = this.GetCopyLength(lengthSym);
+ int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]);
+ this.bitReader.FillBitWindow();
+ int distCode = this.GetCopyDistance(distSymbol);
+ int dist = PlaneCodeToDistance(width, distCode);
+ if (pos >= dist && end - pos >= length)
{
- dec.ExtractPalettedAlphaRows(row);
+ CopyBlock8B(data, pos, dist, length);
+ }
+ else
+ {
+ WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data");
}
- }
- if (pos < last && (col & mask) > 0)
- {
- htreeGroup = GetHTreeGroupForPos(hdr, col, row);
+ pos += length;
+ col += length;
+ while (col >= width)
+ {
+ col -= width;
+ ++row;
+ if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
+ {
+ dec.ExtractPalettedAlphaRows(row);
+ }
+ }
+
+ if (pos < last && (col & mask) > 0)
+ {
+ htreeGroup = GetHTreeGroupForPos(hdr, col, row);
+ }
+
+ break;
}
- }
- else
- {
- WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data");
+
+ default:
+ WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data");
+ break;
}
this.bitReader.Eos = this.bitReader.IsEndOfStream();
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
index 7211f93766..52c7e9703b 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
@@ -50,6 +50,11 @@ internal class Vp8EncIterator
private int uvTopIdx;
+ public Vp8EncIterator(Vp8Encoder enc)
+ : this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh)
+ {
+ }
+
public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh)
{
this.YTop = yTop;
@@ -391,7 +396,7 @@ public int MbAnalyzeBestIntra16Mode()
this.MakeLuma16Preds();
for (mode = 0; mode < maxMode; mode++)
{
- Vp8Histogram histo = new();
+ Vp8Histogram histo = new Vp8Histogram();
histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16);
int alpha = histo.GetAlpha();
if (alpha > bestAlpha)
@@ -409,7 +414,7 @@ public int MbAnalyzeBestIntra4Mode(int bestAlpha)
{
Span modes = stackalloc byte[16];
const int maxMode = MaxIntra4Mode;
- Vp8Histogram totalHisto = new();
+ Vp8Histogram totalHisto = new Vp8Histogram();
int curHisto = 0;
this.StartI4();
do
@@ -462,7 +467,7 @@ public int MbAnalyzeBestUvMode()
this.MakeChroma8Preds();
for (mode = 0; mode < maxMode; ++mode)
{
- Vp8Histogram histo = new();
+ Vp8Histogram histo = new Vp8Histogram();
histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4);
int alpha = histo.GetAlpha();
if (alpha > bestAlpha)
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index f17d965e87..98e50bb9c2 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -4,7 +4,9 @@
using System.Buffers;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -88,7 +90,8 @@ internal class Vp8Encoder : IDisposable
private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11;
- private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
+ private const long HeaderSizeEstimate =
+ WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const int QMin = 0;
@@ -165,7 +168,7 @@ public Vp8Encoder(
// TODO: make partition_limit configurable?
const int limit = 100; // original code: limit = 100 - config->partition_limit;
this.maxI4HeaderBits =
- 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
+ 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh];
for (int i = 0; i < this.MbInfo.Length; i++)
@@ -308,27 +311,94 @@ public Vp8Encoder(
///
private int MbHeaderLimit { get; }
+ public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation)
+ where TPixel : unmanaged, IPixel
+ {
+ // Write bytes from the bitwriter buffer to the stream.
+ ImageMetadata metadata = image.Metadata;
+ metadata.SyncProfiles();
+
+ ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
+ XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
+
+ BitWriterBase.WriteTrunksBeforeData(
+ stream,
+ (uint)image.Width,
+ (uint)image.Height,
+ exifProfile,
+ xmpProfile,
+ metadata.IccProfile,
+ hasAlpha,
+ hasAnimation);
+
+ if (hasAnimation)
+ {
+ WebpMetadata webpMetadata = metadata.GetWebpMetadata();
+ BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
+ }
+ }
+
+ public void EncodeFooter(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ // Write bytes from the bitwriter buffer to the stream.
+ ImageMetadata metadata = image.Metadata;
+
+ ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
+ XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
+
+ BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ }
+
+ ///
+ /// Encodes the image to the specified stream from the .
+ ///
+ /// The pixel format.
+ /// The to encode from.
+ /// The to encode the image data to.
+ public void EncodeAnimation(ImageFrame frame, Stream stream)
+ where TPixel : unmanaged, IPixel =>
+ this.Encode(frame, stream, true, null);
+
///
/// Encodes the image to the specified stream from the .
///
/// The pixel format.
/// The to encode from.
/// The to encode the image data to.
- public void Encode(Image image, Stream stream)
+ public void EncodeStatic(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel =>
+ this.Encode(image.Frames.RootFrame, stream, false, image);
+
+ ///
+ /// Encodes the image to the specified stream from the .
+ ///
+ /// The pixel format.
+ /// The to encode from.
+ /// The to encode the image data to.
+ /// Flag indicating, if an animation parameter is present.
+ /// The to encode from.
+ private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image)
where TPixel : unmanaged, IPixel
{
- int width = image.Width;
- int height = image.Height;
+ int width = frame.Width;
+ int height = frame.Height;
+
int pixelCount = width * height;
Span y = this.Y.GetSpan();
Span u = this.U.GetSpan();
Span v = this.V.GetSpan();
- bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v);
+ bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v);
+
+ if (!hasAnimation)
+ {
+ this.EncodeHeader(image, stream, hasAlpha, false);
+ }
int yStride = width;
int uvStride = (yStride + 1) >> 1;
- Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh);
+ Vp8EncIterator it = new(this);
Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1];
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
int totalMb = this.Mbw * this.Mbw;
@@ -375,13 +445,6 @@ public void Encode(Image image, Stream stream)
// Store filter stats.
this.AdjustFilterStrength();
- // Write bytes from the bitwriter buffer to the stream.
- ImageMetadata metadata = image.Metadata;
- metadata.SyncProfiles();
-
- ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
- XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
-
// Extract and encode alpha channel data, if present.
int alphaDataSize = 0;
bool alphaCompressionSucceeded = false;
@@ -393,7 +456,7 @@ public void Encode(Image image, Stream stream)
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
- image,
+ frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@@ -408,16 +471,39 @@ public void Encode(Image image, Stream stream)
}
}
- this.bitWriter.WriteEncodedImageToStream(
- stream,
- exifProfile,
- xmpProfile,
- metadata.IccProfile,
- (uint)width,
- (uint)height,
- hasAlpha,
- alphaData[..alphaDataSize],
- this.alphaCompression && alphaCompressionSucceeded);
+ this.bitWriter.Finish();
+
+ long prevPosition = 0;
+
+ if (hasAnimation)
+ {
+ WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
+
+ // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
+ prevPosition = new WebpFrameData(
+ 0,
+ 0,
+ (uint)frame.Width,
+ (uint)frame.Height,
+ frameMetadata.FrameDelay,
+ frameMetadata.BlendMethod,
+ frameMetadata.DisposalMethod)
+ .WriteHeaderTo(stream);
+ }
+
+ if (hasAlpha)
+ {
+ Span data = alphaData[..alphaDataSize];
+ bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded;
+ BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed);
+ }
+
+ this.bitWriter.WriteEncodedImageToStream(stream);
+
+ if (hasAnimation)
+ {
+ RiffHelper.EndWriteChunk(stream, prevPosition);
+ }
}
finally
{
@@ -520,7 +606,7 @@ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8Rd
Span y = this.Y.GetSpan();
Span u = this.U.GetSpan();
Span v = this.V.GetSpan();
- Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh);
+ Vp8EncIterator it = new(this);
long size = 0;
long sizeP0 = 0;
long distortion = 0;
@@ -862,10 +948,11 @@ private void SetSegmentProbas()
this.ResetSegments();
}
- this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
- (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
- (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
- (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
+ this.SegmentHeader.Size =
+ (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
+ (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
+ (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
+ (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
}
else
{
@@ -1027,7 +1114,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid
it.NzToBytes();
- int pos1 = this.bitWriter.NumBytes();
+ int pos1 = this.bitWriter.NumBytes;
if (i16)
{
residual.Init(0, 1, this.Proba);
@@ -1054,7 +1141,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid
}
}
- int pos2 = this.bitWriter.NumBytes();
+ int pos2 = this.bitWriter.NumBytes;
// U/V
residual.Init(0, 2, this.Proba);
@@ -1072,7 +1159,7 @@ private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd, Vp8Residual resid
}
}
- int pos3 = this.bitWriter.NumBytes();
+ int pos3 = this.bitWriter.NumBytes;
it.LumaBits = pos2 - pos1;
it.UvBits = pos3 - pos2;
it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits;
diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
index 7952b15b44..3eb03b1724 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
@@ -76,47 +76,48 @@ public void Decode(Buffer2D pixels, int width, int height, WebpI
Vp8Proba proba = new();
Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba);
- using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator))
- {
- Vp8Io io = InitializeVp8Io(decoder, pictureHeader);
+ using Vp8Decoder decoder = new(
+ info.Vp8FrameHeader,
+ pictureHeader,
+ vp8SegmentHeader,
+ proba,
+ this.memoryAllocator);
+ Vp8Io io = InitializeVp8Io(decoder, pictureHeader);
- // Paragraph 9.4: Parse the filter specs.
- this.ParseFilterHeader(decoder);
- decoder.PrecomputeFilterStrengths();
+ // Paragraph 9.4: Parse the filter specs.
+ this.ParseFilterHeader(decoder);
+ decoder.PrecomputeFilterStrengths();
- // Paragraph 9.5: Parse partitions.
- this.ParsePartitions(decoder);
+ // Paragraph 9.5: Parse partitions.
+ this.ParsePartitions(decoder);
- // Paragraph 9.6: Dequantization Indices.
- this.ParseDequantizationIndices(decoder);
+ // Paragraph 9.6: Dequantization Indices.
+ this.ParseDequantizationIndices(decoder);
- // Ignore the value of update probabilities.
- this.bitReader.ReadBool();
+ // Ignore the value of update probabilities.
+ this.bitReader.ReadBool();
- // Paragraph 13.4: Parse probabilities.
- this.ParseProbabilities(decoder);
+ // Paragraph 13.4: Parse probabilities.
+ this.ParseProbabilities(decoder);
- // Decode image data.
- this.ParseFrame(decoder, io);
+ // Decode image data.
+ this.ParseFrame(decoder, io);
- if (info.Features?.Alpha == true)
- {
- using (AlphaDecoder alphaDecoder = new(
- width,
- height,
- alphaData,
- info.Features.AlphaChunkHeader,
- this.memoryAllocator,
- this.configuration))
- {
- alphaDecoder.Decode();
- DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha);
- }
- }
- else
- {
- this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
- }
+ if (info.Features?.Alpha == true)
+ {
+ using AlphaDecoder alphaDecoder = new(
+ width,
+ height,
+ alphaData,
+ info.Features.AlphaChunkHeader,
+ this.memoryAllocator,
+ this.configuration);
+ alphaDecoder.Decode();
+ DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha);
+ }
+ else
+ {
+ this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
}
}
@@ -194,8 +195,8 @@ private void ParseIntraMode(Vp8Decoder dec, int mbX)
{
// Hardcoded tree parsing.
block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0
- ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1])
- : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2);
+ ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1])
+ : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2);
}
else
{
@@ -590,57 +591,65 @@ private static void DoFilter(Vp8Decoder dec, int mbx, int mby)
return;
}
- if (dec.Filter == LoopFilter.Simple)
+ switch (dec.Filter)
{
- int offset = dec.CacheYOffset + (mbx * 16);
- if (mbx > 0)
+ case LoopFilter.Simple:
{
- LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
- }
+ int offset = dec.CacheYOffset + (mbx * 16);
+ if (mbx > 0)
+ {
+ LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
+ }
- if (filterInfo.UseInnerFiltering)
- {
- LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
- }
+ if (filterInfo.UseInnerFiltering)
+ {
+ LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
+ }
- if (mby > 0)
- {
- LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
- }
+ if (mby > 0)
+ {
+ LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
+ }
- if (filterInfo.UseInnerFiltering)
- {
- LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
- }
- }
- else if (dec.Filter == LoopFilter.Complex)
- {
- int uvBps = dec.CacheUvStride;
- int yOffset = dec.CacheYOffset + (mbx * 16);
- int uvOffset = dec.CacheUvOffset + (mbx * 8);
- int hevThresh = filterInfo.HighEdgeVarianceThreshold;
- if (mbx > 0)
- {
- LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
- LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
- }
+ if (filterInfo.UseInnerFiltering)
+ {
+ LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
+ }
- if (filterInfo.UseInnerFiltering)
- {
- LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
- LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
+ break;
}
- if (mby > 0)
+ case LoopFilter.Complex:
{
- LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
- LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
- }
+ int uvBps = dec.CacheUvStride;
+ int yOffset = dec.CacheYOffset + (mbx * 16);
+ int uvOffset = dec.CacheUvOffset + (mbx * 8);
+ int hevThresh = filterInfo.HighEdgeVarianceThreshold;
+ if (mbx > 0)
+ {
+ LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
+ LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
+ }
- if (filterInfo.UseInnerFiltering)
- {
- LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
- LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
+ if (filterInfo.UseInnerFiltering)
+ {
+ LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
+ LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
+ }
+
+ if (mby > 0)
+ {
+ LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh);
+ LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh);
+ }
+
+ if (filterInfo.UseInnerFiltering)
+ {
+ LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
+ LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh);
+ }
+
+ break;
}
}
}
@@ -1328,18 +1337,12 @@ private static Vp8Io InitializeVp8Io(Vp8Decoder dec, Vp8PictureHeader pictureHea
private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz)
{
nzCoeffs <<= 2;
- if (nz > 3)
+ nzCoeffs |= nz switch
{
- nzCoeffs |= 3;
- }
- else if (nz > 1)
- {
- nzCoeffs |= 2;
- }
- else
- {
- nzCoeffs |= (uint)dcNz;
- }
+ > 3 => 3,
+ > 1 => 2,
+ _ => (uint)dcNz
+ };
return nzCoeffs;
}
@@ -1353,13 +1356,13 @@ private static int CheckMode(int mbx, int mby, int mode)
if (mbx == 0)
{
return mby == 0
- ? 6 // B_DC_PRED_NOTOPLEFT
- : 5; // B_DC_PRED_NOLEFT
+ ? 6 // B_DC_PRED_NOTOPLEFT
+ : 5; // B_DC_PRED_NOLEFT
}
return mby == 0
- ? 4 // B_DC_PRED_NOTOP
- : 0; // B_DC_PRED
+ ? 4 // B_DC_PRED_NOTOP
+ : 0; // B_DC_PRED
}
return mode;
diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
index 8ef7fe9cba..d669a37b74 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
@@ -262,17 +262,17 @@ private static void PackAndStore(Vector128 a, Vector128 b, Vector128
/// Converts the RGB values of the image to YUV.
///
/// The pixel type of the image.
- /// The image to convert.
+ /// The frame to convert.
/// The global configuration.
/// The memory allocator.
/// Span to store the luma component of the image.
/// Span to store the u component of the image.
/// Span to store the v component of the image.
/// true, if the image contains alpha data.
- public static bool ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v)
+ public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer;
+ Buffer2D imageBuffer = frame.PixelBuffer;
int width = imageBuffer.Width;
int height = imageBuffer.Height;
int uvWidth = (width + 1) >> 1;
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index 90c9c70b26..66e69d9a43 100644
--- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
-using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@@ -100,7 +100,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat
remainingBytes -= 4;
switch (chunkType)
{
- case WebpChunkType.Animation:
+ case WebpChunkType.FrameData:
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? new Color(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value;
@@ -138,7 +138,7 @@ public Image Decode(BufferedReadStream stream, WebpFeatures feat
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);
+ WebpFrameData frameData = WebpFrameData.Parse(stream);
long streamStartPosition = stream.Position;
Span buffer = stackalloc byte[4];
@@ -162,6 +162,11 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima
features.AlphaChunkHeader = alphaChunkHeader;
break;
case WebpChunkType.Vp8L:
+ if (hasAlpha)
+ {
+ WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images.");
+ }
+
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
break;
default:
@@ -175,7 +180,7 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima
{
image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata);
- SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration);
+ SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData);
imageFrame = image.Frames.RootFrame;
}
@@ -183,29 +188,22 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima
{
currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
- SetFrameMetadata(currentFrame.Metadata, frameData.Duration);
+ SetFrameMetadata(currentFrame.Metadata, frameData);
imageFrame = currentFrame;
}
- int frameX = (int)(frameData.X * 2);
- int frameY = (int)(frameData.Y * 2);
- int frameWidth = (int)frameData.Width;
- int frameHeight = (int)frameData.Height;
- Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight);
+ Rectangle regionRectangle = frameData.Bounds;
- if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose)
+ if (frameData.DisposalMethod is WebpDisposalMethod.Dispose)
{
this.RestoreToBackground(imageFrame, backgroundColor);
}
- using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo);
- DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
+ using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo);
- if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending)
- {
- this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight);
- }
+ bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending;
+ DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle;
@@ -217,12 +215,13 @@ private uint ReadFrame(BufferedReadStream stream, ref Image? ima
/// Sets the frames metadata.
///
/// The metadata.
- /// The frame duration.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration)
+ /// The frame data.
+ private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData)
{
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
- frameMetadata.FrameDuration = duration;
+ frameMetadata.FrameDelay = frameData.Duration;
+ frameMetadata.BlendMethod = frameData.BlendingMethod;
+ frameMetadata.DisposalMethod = frameData.DisposalMethod;
}
///
@@ -239,7 +238,7 @@ private byte ReadAlphaData(BufferedReadStream stream)
byte alphaChunkHeader = (byte)stream.ReadByte();
Span alphaData = this.alphaData.GetSpan();
- stream.Read(alphaData, 0, alphaDataSize);
+ _ = stream.Read(alphaData, 0, alphaDataSize);
return alphaChunkHeader;
}
@@ -251,22 +250,24 @@ private byte ReadAlphaData(BufferedReadStream stream)
/// The frame data.
/// The webp information.
/// A decoded image.
- private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo)
+ private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo)
where TPixel : unmanaged, IPixel
{
- Image decodedImage = new((int)frameData.Width, (int)frameData.Height);
+ ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height);
try
{
- Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer;
+ Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer;
if (webpInfo.IsLossless)
{
- WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
+ WebpLosslessDecoder losslessDecoder =
+ new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
}
else
{
- WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
+ WebpLossyDecoder lossyDecoder =
+ new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
}
@@ -274,7 +275,7 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W
}
catch
{
- decodedImage?.Dispose();
+ decodedFrame?.Dispose();
throw;
}
finally
@@ -287,48 +288,43 @@ private Buffer2D DecodeImageData(AnimationFrameData frameData, W
/// Draws the decoded image on canvas. The decoded image can be smaller the canvas.
///
/// The type of the pixel.
- /// The decoded image.
+ /// 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 static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight)
+ /// The area of the frame.
+ /// Whether to blend the decoded frame data onto the target frame.
+ private static void DrawDecodedImageFrameOnCanvas(
+ Buffer2D decodedImageFrame,
+ ImageFrame imageFrame,
+ Rectangle restoreArea,
+ bool blend)
where TPixel : unmanaged, IPixel
{
- Buffer2D imageFramePixels = imageFrame.PixelBuffer;
- int decodedRowIdx = 0;
- for (int y = frameY; y < frameY + frameHeight; y++)
+ // Trim the destination frame to match the restore area. The source frame is already trimmed.
+ Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea);
+ if (blend)
{
- Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
- Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth];
- decodedPixelRow.TryCopyTo(framePixelRow[frameX..]);
+ // The destination frame has already been prepopulated with the pixel data from the previous frame
+ // so blending will leave the desired result which takes into consideration restoration to the
+ // background color within the restore area.
+ PixelBlender blender =
+ PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
+
+ for (int y = 0; y < restoreArea.Height; y++)
+ {
+ Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
+ Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
+
+ blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
+ }
+
+ return;
}
- }
- ///
- /// 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++)
+ for (int y = 0; y < restoreArea.Height; 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);
+ Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
+ Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
+ decodedPixelRow.CopyTo(framePixelRow);
}
}
@@ -353,42 +349,6 @@ private void RestoreToBackground(ImageFrame imageFrame, Color ba
pixelRegion.Fill(backgroundPixel);
}
- ///
- /// Reads the animation frame header.
- ///
- /// The stream to read from.
- /// Animation frame data.
- private AnimationFrameData ReadFrameHeader(BufferedReadStream stream)
- {
- Span buffer = stackalloc byte[4];
-
- AnimationFrameData data = new()
- {
- DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
-
- // 3 bytes for the X coordinate of the upper left corner of the frame.
- X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer),
-
- // 3 bytes for the Y coordinate of the upper left corner of the frame.
- Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer),
-
- // Frame width Minus One.
- Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
-
- // Frame height Minus One.
- Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1,
-
- // Frame duration.
- Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, 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/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
similarity index 95%
rename from src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
rename to src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
index 99b2462cea..cbd0e9a8cc 100644
--- a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
@@ -6,7 +6,7 @@ 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
+public enum WebpBlendingMethod
{
///
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
index a7ae474e46..80ffe8a996 100644
--- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
+++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
+using System.Drawing;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@@ -77,7 +78,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
}
- if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
+ if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
@@ -91,7 +92,7 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6);
- tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2));
+ tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]);
uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7;
@@ -105,23 +106,16 @@ public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Buffe
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
- var vp8FrameHeader = new Vp8FrameHeader()
+ Vp8FrameHeader vp8FrameHeader = new()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
- var bitReader = new Vp8BitReader(
- stream,
- remaining,
- memoryAllocator,
- partitionLength)
- {
- Remaining = remaining
- };
+ Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
- return new WebpImageInfo()
+ return new WebpImageInfo
{
Width = width,
Height = height,
@@ -145,7 +139,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff
// VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer);
- var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
+ Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
@@ -174,7 +168,7 @@ public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Buff
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
- return new WebpImageInfo()
+ return new WebpImageInfo
{
Width = width,
Height = height,
@@ -231,13 +225,13 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span
}
// 3 bytes for the width.
- uint width = ReadUnsignedInt24Bit(stream, buffer) + 1;
+ uint width = ReadUInt24LittleEndian(stream, buffer) + 1;
// 3 bytes for the height.
- uint height = ReadUnsignedInt24Bit(stream, buffer) + 1;
+ uint height = ReadUInt24LittleEndian(stream, buffer) + 1;
// Read all the chunks in the order they occur.
- var info = new WebpImageInfo()
+ WebpImageInfo info = new()
{
Width = width,
Height = height,
@@ -253,7 +247,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span
/// The stream to read from.
/// The buffer to store the read data into.
/// A unsigned 24 bit integer.
- public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer)
+ public static uint ReadUInt24LittleEndian(Stream stream, Span buffer)
{
if (stream.Read(buffer, 0, 3) == 3)
{
@@ -261,7 +255,28 @@ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span bu
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
- throw new ImageFormatException("Invalid Webp data, could not read unsigned integer.");
+ throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer.");
+ }
+
+ ///
+ /// Writes a unsigned 24 bit integer.
+ ///
+ /// The stream to read from.
+ /// The uint24 data to write.
+ public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data)
+ {
+ if (data >= 1 << 24)
+ {
+ throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer.");
+ }
+
+ uint* ptr = &data;
+ byte* b = (byte*)ptr;
+
+ // Write the data in little endian.
+ stream.WriteByte(b[0]);
+ stream.WriteByte(b[1]);
+ stream.WriteByte(b[2]);
}
///
@@ -271,14 +286,14 @@ public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span bu
/// 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, Span buffer)
+ public static uint ReadChunkSize(Stream stream, Span buffer)
{
- DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length");
+ DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length");
- if (stream.Read(buffer) == 4)
+ if (stream.Read(buffer) is 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
- return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
+ return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1;
}
throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
@@ -298,7 +313,7 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span
if (stream.Read(buffer) == 4)
{
- var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
}
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs
index 802d7f7288..12e3297775 100644
--- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs
+++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs
@@ -12,45 +12,54 @@ internal enum WebpChunkType : uint
///
/// Header signaling the use of the VP8 format.
///
+ /// VP8 (Single)
Vp8 = 0x56503820U,
///
/// Header signaling the image uses lossless encoding.
///
+ /// VP8L (Single)
Vp8L = 0x5650384CU,
///
/// Header for a extended-VP8 chunk.
///
+ /// VP8X (Single)
Vp8X = 0x56503858U,
///
/// Chunk contains information about the alpha channel.
///
+ /// ALPH (Single)
Alpha = 0x414C5048U,
///
/// Chunk which contains a color profile.
///
+ /// ICCP (Single)
Iccp = 0x49434350U,
///
/// Chunk which contains EXIF metadata about the image.
///
+ /// EXIF (Single)
Exif = 0x45584946U,
///
/// Chunk contains XMP metadata about the image.
///
+ /// XMP (Single)
Xmp = 0x584D5020U,
///
/// For an animated image, this chunk contains the global parameters of the animation.
///
+ /// ANIM (Single)
AnimationParameter = 0x414E494D,
///
/// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present.
///
- Animation = 0x414E4D46,
+ /// ANMF (Multiple)
+ FrameData = 0x414E4D46,
}
diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs
index d105d8dd62..818c843ea9 100644
--- a/src/ImageSharp/Formats/Webp/WebpConstants.cs
+++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs
@@ -33,39 +33,6 @@ internal static class WebpConstants
///
public const byte Vp8LHeaderMagicByte = 0x2F;
- ///
- /// Signature bytes identifying a lossy image.
- ///
- public static readonly byte[] Vp8MagicBytes =
- {
- 0x56, // V
- 0x50, // P
- 0x38, // 8
- 0x20 // ' '
- };
-
- ///
- /// Signature bytes identifying a lossless image.
- ///
- public static readonly byte[] Vp8LMagicBytes =
- {
- 0x56, // V
- 0x50, // P
- 0x38, // 8
- 0x4C // L
- };
-
- ///
- /// Signature bytes identifying a VP8X header.
- ///
- public static readonly byte[] Vp8XMagicBytes =
- {
- 0x56, // V
- 0x50, // P
- 0x38, // 8
- 0x58 // X
- };
-
///
/// The header bytes identifying RIFF file.
///
@@ -88,6 +55,11 @@ internal static class WebpConstants
0x50 // P
};
+ ///
+ /// The header bytes identifying a Webp.
+ ///
+ public const string WebpFourCc = "WEBP";
+
///
/// 3 bits reserved for version.
///
@@ -103,11 +75,6 @@ internal static class WebpConstants
///
public const int Vp8FrameHeaderSize = 10;
- ///
- /// Size of a VP8X chunk in bytes.
- ///
- public const int Vp8XChunkSize = 10;
-
///
/// Size of a chunk header.
///
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs
index e23b817ccd..dfbf4ef0e6 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs
@@ -17,7 +17,7 @@ private WebpDecoder()
///
/// Gets the shared instance.
///
- public static WebpDecoder Instance { get; } = new();
+ public static WebpDecoder Instance { get; } = new WebpDecoder();
///
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
@@ -25,7 +25,7 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
- using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options });
+ using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options });
return decoder.Identify(options.Configuration, stream, cancellationToken);
}
@@ -35,7 +35,7 @@ protected override Image Decode(WebpDecoderOptions options, Stre
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
- using WebpDecoderCore decoder = new(options);
+ using WebpDecoderCore decoder = new WebpDecoderCore(options);
Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken);
ScaleToTargetSize(options.GeneralOptions, image);
@@ -52,6 +52,5 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat
=> this.Decode(options, stream, cancellationToken);
///
- protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
- => new() { GeneralOptions = options };
+ protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options };
}
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 8832ac1068..de188b137b 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -8,7 +8,9 @@
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -89,25 +91,30 @@ public Image Decode(BufferedReadStream stream, CancellationToken
{
if (this.webImageInfo.Features is { Animation: true })
{
- using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling);
+ using WebpAnimationDecoder animationDecoder = new(
+ this.memoryAllocator,
+ this.configuration,
+ this.maxFrames,
+ this.backgroundColorHandling);
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");
- }
-
image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
{
- WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
+ WebpLosslessDecoder losslessDecoder = new(
+ this.webImageInfo.Vp8LBitReader,
+ this.memoryAllocator,
+ this.configuration);
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
{
- WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
+ WebpLossyDecoder lossyDecoder = new(
+ this.webImageInfo.Vp8BitReader,
+ this.memoryAllocator,
+ this.configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}
@@ -137,7 +144,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
{
return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
- new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
+ new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
metadata);
}
}
@@ -332,7 +339,7 @@ private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata,
return;
}
- metadata.ExifProfile = new(exifData);
+ metadata.ExifProfile = new ExifProfile(exifData);
}
}
@@ -359,7 +366,7 @@ private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, S
return;
}
- metadata.XmpProfile = new(xmpData);
+ metadata.XmpProfile = new XmpProfile(xmpData);
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
index 6fb15acbb4..8840805b1f 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
@@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public sealed class WebpDecoderOptions : ISpecializedDecoderOptions
{
///
- public DecoderOptions GeneralOptions { get; init; } = new();
+ public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
///
/// Gets the flag to decide how to handle the background color Animation Chunk.
diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
similarity index 94%
rename from src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
rename to src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
index 23bc37c283..d409973a99 100644
--- a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
@@ -6,7 +6,7 @@ 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
+public enum WebpDisposalMethod
{
///
/// Do not dispose. Leave the canvas as is.
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs
index 29d0c9e3b0..bc93df3a5b 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Advanced;
-
namespace SixLabors.ImageSharp.Formats.Webp;
///
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index 49512e03b5..47712071bf 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -129,7 +129,7 @@ public void Encode(Image image, Stream stream, CancellationToken
if (lossless)
{
- using Vp8LEncoder enc = new(
+ using Vp8LEncoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@@ -140,11 +140,38 @@ public void Encode(Image image, Stream stream, CancellationToken
this.transparentColorMode,
this.nearLossless,
this.nearLosslessQuality);
- enc.Encode(image, stream);
+
+ bool hasAnimation = image.Frames.Count > 1;
+ encoder.EncodeHeader(image, stream, hasAnimation);
+ if (hasAnimation)
+ {
+ foreach (ImageFrame imageFrame in image.Frames)
+ {
+ using Vp8LEncoder enc = new(
+ this.memoryAllocator,
+ this.configuration,
+ image.Width,
+ image.Height,
+ this.quality,
+ this.skipMetadata,
+ this.method,
+ this.transparentColorMode,
+ this.nearLossless,
+ this.nearLosslessQuality);
+
+ enc.Encode(imageFrame, stream, true);
+ }
+ }
+ else
+ {
+ encoder.Encode(image.Frames.RootFrame, stream, false);
+ }
+
+ encoder.EncodeFooter(image, stream);
}
else
{
- using Vp8Encoder enc = new(
+ using Vp8Encoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@@ -156,7 +183,34 @@ public void Encode(Image image, Stream stream, CancellationToken
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
- enc.Encode(image, stream);
+ if (image.Frames.Count > 1)
+ {
+ encoder.EncodeHeader(image, stream, false, true);
+
+ foreach (ImageFrame imageFrame in image.Frames)
+ {
+ using Vp8Encoder enc = new(
+ this.memoryAllocator,
+ this.configuration,
+ image.Width,
+ image.Height,
+ this.quality,
+ this.skipMetadata,
+ this.method,
+ this.entropyPasses,
+ this.filterStrength,
+ this.spatialNoiseShaping,
+ this.alphaCompression);
+
+ enc.EncodeAnimation(imageFrame, stream);
+ }
+ }
+ else
+ {
+ encoder.EncodeStatic(image, stream);
+ }
+
+ encoder.EncodeFooter(image, stream);
}
}
}
diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs
index 29c74b11bf..197041234e 100644
--- a/src/ImageSharp/Formats/Webp/WebpFormat.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs
@@ -15,7 +15,7 @@ private WebpFormat()
///
/// Gets the shared instance.
///
- public static WebpFormat Instance { get; } = new();
+ public static WebpFormat Instance { get; } = new WebpFormat();
///
public string Name => "Webp";
@@ -30,8 +30,8 @@ private WebpFormat()
public IEnumerable FileExtensions => WebpConstants.FileExtensions;
///
- public WebpMetadata CreateDefaultFormatMetadata() => new();
+ public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
///
- public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new();
+ public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata();
}
diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
index bce1b09d6f..ef21d8b6fe 100644
--- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
@@ -19,13 +19,28 @@ public WebpFrameMetadata()
/// Initializes a new instance of the class.
///
/// The metadata to create an instance from.
- private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration;
+ private WebpFrameMetadata(WebpFrameMetadata other)
+ {
+ this.FrameDelay = other.FrameDelay;
+ this.DisposalMethod = other.DisposalMethod;
+ this.BlendMethod = other.BlendMethod;
+ }
+
+ ///
+ /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
+ ///
+ public WebpBlendingMethod BlendMethod { get; set; }
+
+ ///
+ /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
+ ///
+ public WebpDisposalMethod DisposalMethod { get; set; }
///
/// 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 uint FrameDelay { get; set; }
///
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);
diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
index 5d1051c751..a6bb0a7b80 100644
--- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs
+++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs
@@ -23,6 +23,7 @@ private WebpMetadata(WebpMetadata other)
{
this.FileFormat = other.FileFormat;
this.AnimationLoopCount = other.AnimationLoopCount;
+ this.AnimationBackground = other.AnimationBackground;
}
///
@@ -35,6 +36,14 @@ private WebpMetadata(WebpMetadata other)
///
public ushort AnimationLoopCount { get; set; } = 1;
+ ///
+ /// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
+ /// This color MAY be used to fill the unused space on the canvas around the frames,
+ /// as well as the transparent pixels of the first frame.
+ /// The background color is also used when the Disposal method is 1.
+ ///
+ public Color AnimationBackground { get; set; }
+
///
public IDeepCloneable DeepClone() => new WebpMetadata(this);
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
index 3b5e438299..be7350bc44 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
@@ -158,8 +158,7 @@ public bool CheckIsValid()
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
- this.Header.Size >= minSize &&
- this.Header.Size < maxSize;
+ this.Header.Size is >= minSize and < maxSize;
}
///
@@ -175,7 +174,6 @@ public byte[] ToByteArray()
return copy;
}
- IccWriter writer = new();
return IccWriter.Write(this);
}
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index c0fc00b82d..4b03671e16 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -308,7 +308,7 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider(TestImageProvider(TestImagePr
image.CompareToOriginal(provider, ReferenceDecoder);
}
+ [Theory]
+ [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
+ public void Decode_AnimatedLossy_AlphaBlending_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(WebpDecoder.Instance);
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact);
+ }
+
[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 6c5fa50ff6..0ad684b277 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
@@ -17,6 +17,49 @@ public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
+ [Theory]
+ [WithFile(Lossless.Animated, PixelTypes.Rgba32)]
+ public void Encode_AnimatedLossless(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ WebpEncoder encoder = new()
+ {
+ FileFormat = WebpFileFormatType.Lossless,
+ Quality = 100
+ };
+
+ // Always save as we need to compare the encoded output.
+ provider.Utility.SaveTestOutputFile(image, "webp", encoder);
+
+ // Compare encoded result
+ image.VerifyEncoder(provider, "webp", string.Empty, encoder);
+ }
+
+ [Theory]
+ [WithFile(Lossy.Animated, PixelTypes.Rgba32)]
+ [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
+ public void Encode_AnimatedLossy(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ WebpEncoder encoder = new()
+ {
+ FileFormat = WebpFileFormatType.Lossy,
+ Quality = 100
+ };
+
+ // Always save as we need to compare the encoded output.
+ provider.Utility.SaveTestOutputFile(image, "webp", encoder);
+
+ // Compare encoded result
+ // The reference decoder seems to produce differences up to 0.1% but the input/output have been
+ // checked to be correct.
+ string path = provider.Utility.GetTestOutputFileName("webp", null, true);
+ using Image encoded = Image.Load(path);
+ encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp");
+ }
+
[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)]
diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
index 9b03a447a9..433b280bc3 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
@@ -143,7 +143,7 @@ public void ConvertRgbToYuv_Works(TestImageProvider provider)
};
// act
- YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v);
+ YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert
Assert.True(expectedY.AsSpan().SequenceEqual(y));
@@ -249,7 +249,7 @@ public void ConvertRgbToYuv_WithAlpha_Works(TestImageProvider pr
};
// act
- YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v);
+ YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert
Assert.True(expectedY.AsSpan().SequenceEqual(y));
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 048b19dc5b..6ad93adfbd 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -681,6 +681,7 @@ public static class Lossless
public static class Lossy
{
+ public const string AnimatedLandscape = "Webp/landscape.webp";
public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp";
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";
diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp
new file mode 100644
index 0000000000..2312cb8576
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9ece3c7acc6f40318e3cda6b0189607df6b9b60dd112212c72ec0f6aa26431d
+size 409346
diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp
new file mode 100644
index 0000000000..8474504da7
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:71800dff476f50ebd2a3d0cf0b4f5bef427a1c2cd8732b415511f10d3d93f9a0
+size 126382
diff --git a/tests/Images/Input/Webp/landscape.webp b/tests/Images/Input/Webp/landscape.webp
new file mode 100644
index 0000000000..5f1f31a055
--- /dev/null
+++ b/tests/Images/Input/Webp/landscape.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1e9f8b7ee87ecb59d8cee5e84320da7670eb5e274e1c0a7dd5f13fe3675be62a
+size 26892