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)]