diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index ad7d69f13..4a9da3cbb 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -87,21 +87,20 @@ internal abstract class BitWriterBase
///
/// Writes the RIFF header to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// The block length.
- protected void WriteRiffHeader(Stream stream, uint riffSize)
+ protected static void WriteRiffHeader(Stream stream, uint riffSize)
{
stream.Write(WebpConstants.RiffFourCc);
- BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
- stream.Write(this.scratchBuffer.Span[..4]);
+ Span buf = stackalloc byte[4];
+ BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize);
+ stream.Write(buf);
stream.Write(WebpConstants.WebpHeader);
}
///
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
///
- /// Think of it as a static method — none of the other members are called except for
/// The metadata profile bytes.
/// The metadata chunk size in bytes.
protected static uint MetadataChunkSize(byte[] metadataBytes)
@@ -125,22 +124,11 @@ internal abstract class BitWriterBase
/// Overwrites ides the write file size.
///
/// The stream to write to.
- protected static void OverwriteFileSize(Stream stream)
- {
- uint position = (uint)stream.Position;
- stream.Position = 4;
- byte[] buffer = new byte[4];
-
- // "RIFF"(4)+uint32 size(4)
- BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize);
- stream.Write(buffer);
- stream.Position = position;
- }
+ protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4);
///
/// Write the trunks before data trunk.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// The width of the image.
/// The height of the image.
@@ -148,9 +136,8 @@ internal abstract class BitWriterBase
/// The XMP profile.
/// The color profile.
/// Flag indicating, if a alpha channel is present.
- /// The alpha channel data.
- /// Indicates, if the alpha data is compressed.
- public void WriteTrunksBeforeData(
+ /// Flag indicating, if an animation parameter is present.
+ public static void WriteTrunksBeforeData(
Stream stream,
uint width,
uint height,
@@ -158,26 +145,20 @@ internal abstract class BitWriterBase
XmpProfile? xmpProfile,
IccProfile? iccProfile,
bool hasAlpha,
- Span alphaData,
- bool alphaDataIsCompressed)
+ bool hasAnimation)
{
// Write file size later
- this.WriteRiffHeader(stream, 0);
+ WriteRiffHeader(stream, 0);
// Write VP8X, header if necessary.
- bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha;
+ bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
if (isVp8X)
{
- this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha);
+ WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
if (iccProfile != null)
{
- this.WriteColorProfile(stream, iccProfile.ToByteArray());
- }
-
- if (hasAlpha)
- {
- this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
+ WriteColorProfile(stream, iccProfile.ToByteArray());
}
}
}
@@ -191,23 +172,22 @@ internal abstract class BitWriterBase
///
/// Write the trunks after data trunk.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// The exif profile.
/// The XMP profile.
- public void WriteTrunksAfterData(
+ public static void WriteTrunksAfterData(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{
if (exifProfile != null)
{
- this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif);
+ WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif);
}
if (xmpProfile != null)
{
- this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp);
+ WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp);
}
OverwriteFileSize(stream);
@@ -216,16 +196,15 @@ internal abstract class BitWriterBase
///
/// Writes a metadata profile (EXIF or XMP) to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// The metadata profile's bytes.
/// The chuck type to write.
- protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
+ protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
{
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)metadataBytes.Length;
- Span buf = this.scratchBuffer.Span[..4];
+ Span buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@@ -242,15 +221,13 @@ internal abstract class BitWriterBase
///
/// Writes the color profile() to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// The color profile bytes.
- protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp);
+ protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp);
///
/// Writes the animation parameter() to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
///
/// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
@@ -259,9 +236,9 @@ internal abstract class BitWriterBase
/// 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.
- protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount)
+ public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount)
{
- Span buf = this.scratchBuffer.Span[..4];
+ Span buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort));
@@ -275,17 +252,15 @@ internal abstract class BitWriterBase
///
/// Writes the animation frame() to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// The stream to write to.
/// Animation frame data.
- /// Frame data.
- protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data)
+ public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation)
{
- uint size = AnimationFrameData.HeaderSize + (uint)data.Length;
- Span buf = this.scratchBuffer.Span[..4];
+ Span buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation);
stream.Write(buf);
- BinaryPrimitives.WriteUInt32BigEndian(buf, size);
+ long position = stream.Position;
+ BinaryPrimitives.WriteUInt32BigEndian(buf, 0);
stream.Write(buf);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y);
@@ -294,20 +269,35 @@ internal abstract class BitWriterBase
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration);
byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod);
stream.WriteByte(flag);
- stream.Write(data);
+ return position;
+ }
+
+ ///
+ /// Overwrites ides the write frame size.
+ ///
+ /// The stream to write to.
+ /// Previous position.
+ public static void OverwriteFrameSize(Stream stream, long prevPosition)
+ {
+ uint position = (uint)stream.Position;
+ stream.Position = prevPosition;
+ byte[] buffer = new byte[4];
+
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4));
+ stream.Write(buffer);
+ stream.Position = position;
}
///
/// Writes the alpha chunk to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// 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[..4];
+ Span buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@@ -332,7 +322,6 @@ internal abstract class BitWriterBase
///
/// Writes a VP8X header to the stream.
///
- /// Think of it as a static method — none of the other members are called except for
/// 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.
@@ -340,7 +329,8 @@ internal abstract class BitWriterBase
/// 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, IccProfile? iccProfile, 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)
{
@@ -360,13 +350,11 @@ internal abstract class BitWriterBase
flags |= 8;
}
- /*
- if (isAnimated)
+ if (hasAnimation)
{
// Set animated flag.
flags |= 2;
}
- */
if (xmpProfile != null)
{
@@ -386,7 +374,7 @@ internal abstract class BitWriterBase
flags |= 32;
}
- Span buf = this.scratchBuffer.Span[..4];
+ Span buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 4d526e7b4..5859d8a87 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -267,7 +267,7 @@ internal class Vp8LEncoder : IDisposable
this.EncodeStream(image.Frames.RootFrame);
this.bitWriter.Finish();
- this.bitWriter.WriteTrunksBeforeData(
+ BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)width,
(uint)height,
@@ -275,13 +275,12 @@ internal class Vp8LEncoder : IDisposable
xmpProfile,
metadata.IccProfile,
false /*hasAlpha*/,
- Span.Empty,
false);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream);
- this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
}
///
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index f744827bf..ccd7d8b6d 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -88,7 +88,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 +166,7 @@ internal class Vp8Encoder : IDisposable
// 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,22 +309,88 @@ internal class Vp8Encoder : IDisposable
///
private int MbHeaderLimit { get; }
+ public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0)
+ 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)
+ {
+ BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount);
+ }
+ }
+
+ 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.Frames.RootFrame, 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;
@@ -375,13 +442,6 @@ internal class Vp8Encoder : IDisposable
// 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 +453,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
- image.Frames.RootFrame,
+ frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@@ -409,20 +469,31 @@ internal class Vp8Encoder : IDisposable
}
this.bitWriter.Finish();
- this.bitWriter.WriteTrunksBeforeData(
- stream,
- (uint)width,
- (uint)height,
- exifProfile,
- xmpProfile,
- metadata.IccProfile,
- hasAlpha,
- alphaData[..alphaDataSize],
- this.alphaCompression && alphaCompressionSucceeded);
+
+ long prevPosition = 0;
+
+ if (hasAnimation)
+ {
+ prevPosition = BitWriterBase.WriteAnimationFrame(stream, new()
+ {
+ Width = (uint)frame.Width,
+ Height = (uint)frame.Height
+ });
+ }
+
+ if (hasAlpha)
+ {
+ Span data = alphaData[..alphaDataSize];
+ bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded;
+ BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed);
+ }
this.bitWriter.WriteEncodedImageToStream(stream);
- this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
+ if (hasAnimation)
+ {
+ BitWriterBase.OverwriteFrameSize(stream, prevPosition);
+ }
}
finally
{
diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
index 49512e03b..2751f9913 100644
--- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
@@ -144,7 +144,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
}
else
{
- using Vp8Encoder enc = new(
+ using Vp8Encoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@@ -156,7 +156,33 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
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/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
index 6c5fa50ff..4b100e854 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
@@ -17,6 +17,13 @@ public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
+ [Fact]
+ public void Encode_AnimatedLossy()
+ {
+ Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp");
+ image.SaveAsWebp(@"C:\Users\poker\Desktop\3.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)]