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 @@ internal class AlphaDecoder : IDisposable
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 @@ internal static class AlphaEncoder
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 @@ internal static class AlphaEncoder
// The transparency information will be stored in the green channel of the ARGB quadruplet.
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression.
- using 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 @@ internal static class AlphaEncoder
/// 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 @@ internal static class AlphaEncoder
}
}
- 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 @@ internal abstract class BitWriterBase
}
///
- /// 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 @@ internal abstract class BitWriterBase
/// 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 @@ internal abstract class BitWriterBase
/// 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 @@ internal class Vp8BitWriter : BitWriterBase
}
///
- 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 @@ internal class Vp8BitWriter : BitWriterBase
else
{
this.PutBit(v >= 9, 165);
- this.PutBit(!((v & 1) != 0), 145);
+ this.PutBit((v & 1) == 0, 145);
}
}
else
@@ -394,87 +391,28 @@ internal class Vp8BitWriter : BitWriterBase
}
}
- ///
- /// 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 @@ internal class Vp8BitWriter : BitWriterBase
{
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 @@ internal class Vp8BitWriter : BitWriterBase
{
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 @@ internal class Vp8BitWriter : BitWriterBase
{
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 @@ internal class Vp8BitWriter : BitWriterBase
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 @@ internal class Vp8BitWriter : BitWriterBase
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 @@ internal class Vp8BitWriter : BitWriterBase
}
}
- 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 @@ internal class Vp8LBitWriter : BitWriterBase
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 @@ internal class Vp8LBitWriter : BitWriterBase
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 @@ internal class Vp8LBitWriter : BitWriterBase
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 @@ internal class Vp8LBitWriter : BitWriterBase
{
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 @@ internal class Vp8LBitWriter : BitWriterBase
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 61133142bf..2e7dd722fc 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
@@ -50,8 +50,10 @@ internal static class BackwardReferenceEncoder
double bitCostBest = -1;
int cacheBitsInitial = cacheBits;
Vp8LHashChain? hashChainBox = null;
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
+
+ ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
{
int cacheBitsTmp = cacheBitsInitial;
@@ -76,21 +78,19 @@ internal static class BackwardReferenceEncoder
}
// Next, try with a color cache and update the references.
- cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp);
+ cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp);
if (cacheBitsTmp > 0)
{
BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst);
}
// Keep the best backward references.
- var histo = new Vp8LHistogram(worst, cacheBitsTmp);
+ using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp);
double bitCost = histo.EstimateBits(stats, bitsEntropy);
if (lz77TypeBest == 0 || bitCost < bitCostBest)
{
- Vp8LBackwardRefs tmp = worst;
- worst = best;
- best = tmp;
+ (best, worst) = (worst, best);
bitCostBest = bitCost;
cacheBits = cacheBitsTmp;
lz77TypeBest = lz77Type;
@@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder
{
Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!;
BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst);
- var histo = new Vp8LHistogram(worst, cacheBits);
+ using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits);
double bitCostTrace = histo.EstimateBits(stats, bitsEntropy);
if (bitCostTrace < bitCostBest)
{
@@ -123,7 +123,13 @@ internal static class BackwardReferenceEncoder
/// The local color cache is also disabled for the lower (smaller then 25) quality.
///
/// Best cache size.
- private static int CalculateBestCacheSize(ReadOnlySpan bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits)
+ private static int CalculateBestCacheSize(
+ MemoryAllocator memoryAllocator,
+ Span colorCache,
+ ReadOnlySpan bgra,
+ uint quality,
+ Vp8LBackwardRefs refs,
+ int bestCacheBits)
{
int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits;
if (cacheBitsMax == 0)
@@ -134,11 +140,11 @@ internal static class BackwardReferenceEncoder
double entropyMin = MaxEntropy;
int pos = 0;
- var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
- var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1];
- for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++)
+
+ using Vp8LHistogramSet histos = new(memoryAllocator, colorCache.Length, 0);
+ for (int i = 0; i < colorCache.Length; i++)
{
- histos[i] = new Vp8LHistogram(paletteCodeBits: i);
+ histos[i].PaletteCodeBits = i;
colorCache[i] = new ColorCache(i);
}
@@ -149,10 +155,10 @@ internal static class BackwardReferenceEncoder
if (v.IsLiteral())
{
uint pix = bgra[pos++];
- uint a = (pix >> 24) & 0xff;
- uint r = (pix >> 16) & 0xff;
- uint g = (pix >> 8) & 0xff;
- uint b = (pix >> 0) & 0xff;
+ int a = (int)(pix >> 24) & 0xff;
+ int r = (int)(pix >> 16) & 0xff;
+ int g = (int)(pix >> 8) & 0xff;
+ int b = (int)(pix >> 0) & 0xff;
// The keys of the caches can be derived from the longest one.
int key = ColorCache.HashPix(pix, 32 - cacheBitsMax);
@@ -218,8 +224,8 @@ internal static class BackwardReferenceEncoder
}
}
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i <= cacheBitsMax; i++)
{
double entropy = histos[i].EstimateBits(stats, bitsEntropy);
@@ -266,7 +272,7 @@ internal static class BackwardReferenceEncoder
int pixCount = xSize * ySize;
bool useColorCache = cacheBits > 0;
int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0);
- var costModel = new CostModel(literalArraySize);
+ CostModel costModel = new(memoryAllocator, literalArraySize);
int offsetPrev = -1;
int lenPrev = -1;
double offsetCost = -1;
@@ -280,7 +286,7 @@ internal static class BackwardReferenceEncoder
}
costModel.Build(xSize, cacheBits, refs);
- using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel);
+ using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel);
Span costManagerCosts = costManager.Costs.GetSpan();
Span distArray = distArrayBuffer.GetSpan();
@@ -441,12 +447,12 @@ internal static class BackwardReferenceEncoder
int ix = useColorCache ? colorCache!.Contains(color) : -1;
if (ix >= 0)
{
- double mul0 = 0.68;
+ const double mul0 = 0.68;
costVal += costModel.GetCacheCost((uint)ix) * mul0;
}
else
{
- double mul1 = 0.82;
+ const double mul1 = 0.82;
if (useColorCache)
{
colorCache!.Insert(color);
@@ -693,10 +699,8 @@ internal static class BackwardReferenceEncoder
bestLength = MaxLength;
break;
}
- else
- {
- bestLength = currLength;
- }
+
+ bestLength = currLength;
}
}
}
@@ -775,7 +779,7 @@ internal static class BackwardReferenceEncoder
private static void BackwardRefsWithLocalCache(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/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
index c99e8fe6e2..beebc48abc 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
@@ -1,18 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Memory;
+
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal class CostModel
{
+ private readonly MemoryAllocator memoryAllocator;
private const int ValuesInBytes = 256;
///
/// Initializes a new instance of the class.
///
+ /// The memory allocator.
/// The literal array size.
- public CostModel(int literalArraySize)
+ public CostModel(MemoryAllocator memoryAllocator, int literalArraySize)
{
+ this.memoryAllocator = memoryAllocator;
this.Alpha = new double[ValuesInBytes];
this.Red = new double[ValuesInBytes];
this.Blue = new double[ValuesInBytes];
@@ -32,13 +37,12 @@ internal class CostModel
public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs)
{
- var histogram = new Vp8LHistogram(cacheBits);
- using System.Collections.Generic.List.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator();
+ using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits);
// The following code is similar to HistogramCreate but converts the distance to plane code.
- while (refsEnumerator.MoveNext())
+ for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
- histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize);
+ histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize);
}
ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal);
@@ -70,7 +74,7 @@ internal class CostModel
public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff];
- private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output)
+ private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span populationCounts, double[] output)
{
uint sum = 0;
int nonzeros = 0;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
index dd59ed2097..3a96362cfd 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
@@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License.
#nullable disable
+using System.Buffers;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@@ -27,19 +29,28 @@ internal static class HistogramEncoder
private const ushort InvalidHistogramSymbol = ushort.MaxValue;
- public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, Span histogramSymbols)
+ public static void GetHistoImageSymbols(
+ MemoryAllocator memoryAllocator,
+ int xSize,
+ int ySize,
+ Vp8LBackwardRefs refs,
+ uint quality,
+ int histoBits,
+ int cacheBits,
+ Vp8LHistogramSet imageHisto,
+ Vp8LHistogram tmpHisto,
+ Span histogramSymbols)
{
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1;
int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1;
int imageHistoRawSize = histoXSize * histoYSize;
- int entropyCombineNumBins = BinSize;
- ushort[] mapTmp = new ushort[imageHistoRawSize];
- ushort[] clusterMappings = new ushort[imageHistoRawSize];
- var origHisto = new List(imageHistoRawSize);
- for (int i = 0; i < imageHistoRawSize; i++)
- {
- origHisto.Add(new Vp8LHistogram(cacheBits));
- }
+ const int entropyCombineNumBins = BinSize;
+
+ using IMemoryOwner tmp = memoryAllocator.Allocate(imageHistoRawSize * 2, AllocationOptions.Clean);
+ Span mapTmp = tmp.Slice(0, imageHistoRawSize);
+ Span clusterMappings = tmp.Slice(imageHistoRawSize, imageHistoRawSize);
+
+ using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits);
// Construct the histograms from the backward references.
HistogramBuild(xSize, histoBits, refs, origHisto);
@@ -50,18 +61,17 @@ internal static class HistogramEncoder
bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100;
if (entropyCombine)
{
- ushort[] binMap = mapTmp;
int numClusters = numUsed;
double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality);
- HistogramAnalyzeEntropyBin(imageHisto, binMap);
+ HistogramAnalyzeEntropyBin(imageHisto, mapTmp);
// Collapse histograms with similar entropy.
- HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor);
+ HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, mapTmp, entropyCombineNumBins, combineCostFactor);
OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols);
}
- float x = quality / 100.0f;
+ float x = quality / 100F;
// Cubic ramp between 1 and MaxHistoGreedy:
int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1)));
@@ -77,26 +87,25 @@ internal static class HistogramEncoder
HistogramRemap(origHisto, imageHisto, histogramSymbols);
}
- private static void RemoveEmptyHistograms(List histograms)
+ private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms)
{
- int size = 0;
- for (int i = 0; i < histograms.Count; i++)
+ for (int i = histograms.Count - 1; i >= 0; i--)
{
if (histograms[i] == null)
{
- continue;
+ histograms.RemoveAt(i);
}
-
- histograms[size++] = histograms[i];
}
-
- histograms.RemoveRange(size, histograms.Count - size);
}
///
/// Construct the histograms from the backward references.
///
- private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List histograms)
+ private static void HistogramBuild(
+ int xSize,
+ int histoBits,
+ Vp8LBackwardRefs backwardRefs,
+ Vp8LHistogramSet histograms)
{
int x = 0, y = 0;
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
@@ -119,10 +128,10 @@ internal static class HistogramEncoder
/// Partition histograms to different entropy bins for three dominant (literal,
/// red and blue) symbol costs and compute the histogram aggregate bitCost.
///
- private static void HistogramAnalyzeEntropyBin(List histograms, ushort[] binMap)
+ private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span binMap)
{
int histoSize = histograms.Count;
- var costRange = new DominantCostRange();
+ DominantCostRange costRange = new();
// Analyze the dominant (literal, red and blue) entropy costs.
for (int i = 0; i < histoSize; i++)
@@ -148,17 +157,20 @@ internal static class HistogramEncoder
}
}
- private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, Span histogramSymbols)
+ private static int HistogramCopyAndAnalyze(
+ Vp8LHistogramSet origHistograms,
+ Vp8LHistogramSet histograms,
+ Span histogramSymbols)
{
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
for (int clusterId = 0, i = 0; i < origHistograms.Count; i++)
{
Vp8LHistogram origHistogram = origHistograms[i];
origHistogram.UpdateHistogramCost(stats, bitsEntropy);
// Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77).
- if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4])
+ if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4))
{
origHistograms[i] = null;
histograms[i] = null;
@@ -166,7 +178,7 @@ internal static class HistogramEncoder
}
else
{
- histograms[i] = (Vp8LHistogram)origHistogram.DeepClone();
+ origHistogram.CopyTo(histograms[i]);
histogramSymbols[i] = (ushort)clusterId++;
}
}
@@ -184,11 +196,11 @@ internal static class HistogramEncoder
}
private static void HistogramCombineEntropyBin(
- List histograms,
+ Vp8LHistogramSet histograms,
Span clusters,
- ushort[] clusterMappings,
+ Span clusterMappings,
Vp8LHistogram curCombo,
- ushort[] binMap,
+ ReadOnlySpan binMap,
int numBins,
double combineCostFactor)
{
@@ -205,9 +217,9 @@ internal static class HistogramEncoder
clusterMappings[idx] = (ushort)idx;
}
- var indicesToRemove = new List();
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ List indicesToRemove = new();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
for (int idx = 0; idx < histograms.Count; idx++)
{
if (histograms[idx] == null)
@@ -236,13 +248,11 @@ internal static class HistogramEncoder
// histogram pairs. In that case, we fallback to combining
// histograms as usual to avoid increasing the header size.
bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym);
- int maxCombineFailures = 32;
+ const int maxCombineFailures = 32;
if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures)
{
// Move the (better) merged histogram to its final slot.
- Vp8LHistogram tmp = curCombo;
- curCombo = histograms[first];
- histograms[first] = tmp;
+ (histograms[first], curCombo) = (curCombo, histograms[first]);
histograms[idx] = null;
indicesToRemove.Add(idx);
@@ -256,9 +266,9 @@ internal static class HistogramEncoder
}
}
- foreach (int index in indicesToRemove.OrderByDescending(i => i))
+ for (int i = indicesToRemove.Count - 1; i >= 0; i--)
{
- histograms.RemoveAt(index);
+ histograms.RemoveAt(indicesToRemove[i]);
}
}
@@ -266,7 +276,7 @@ internal static class HistogramEncoder
/// Given a Histogram set, the mapping of clusters 'clusterMapping' and the
/// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values.
///
- private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span symbols)
+ private static void OptimizeHistogramSymbols(Span clusterMappings, int numClusters, Span clusterMappingsTmp, Span symbols)
{
bool doContinue = true;
@@ -293,7 +303,7 @@ internal static class HistogramEncoder
// Create a mapping from a cluster id to its minimal version.
int clusterMax = 0;
- clusterMappingsTmp.AsSpan().Clear();
+ clusterMappingsTmp.Clear();
// Re-map the ids.
for (int i = 0; i < symbols.Length; i++)
@@ -318,15 +328,15 @@ internal static class HistogramEncoder
/// Perform histogram aggregation using a stochastic approach.
///
/// true if a greedy approach needs to be performed afterwards, false otherwise.
- private static bool HistogramCombineStochastic(List histograms, int minClusterSize)
+ private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize)
{
uint seed = 1;
int triesWithNoSuccess = 0;
int numUsed = histograms.Count(h => h != null);
int outerIters = numUsed;
int numTriesNoSuccess = (int)((uint)outerIters / 2);
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
if (numUsed < minClusterSize)
{
@@ -335,25 +345,25 @@ internal static class HistogramEncoder
// Priority list of histogram pairs. Its size impacts the quality of the compression and the speed:
// the smaller the faster but the worse for the compression.
- var histoPriorityList = new List();
- int maxSize = 9;
+ List histoPriorityList = new();
+ const int maxSize = 9;
// Fill the initial mapping.
Span mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count];
- for (int j = 0, iter = 0; iter < histograms.Count; iter++)
+ for (int j = 0, i = 0; i < histograms.Count; i++)
{
- if (histograms[iter] == null)
+ if (histograms[i] == null)
{
continue;
}
- mappings[j++] = iter;
+ mappings[j++] = i;
}
// Collapse similar histograms.
- for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++)
+ for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++)
{
- double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff;
+ double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff;
int numTries = (int)((uint)numUsed / 2);
uint randRange = (uint)((numUsed - 1) * numUsed);
@@ -398,12 +408,12 @@ internal static class HistogramEncoder
int mappingIndex = mappings.IndexOf(bestIdx2);
Span src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1);
- Span dst = mappings.Slice(mappingIndex);
+ Span dst = mappings[mappingIndex..];
src.CopyTo(dst);
// Merge the histograms and remove bestIdx2 from the list.
HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]);
- histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo;
+ histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo;
histograms[bestIdx2] = null;
numUsed--;
@@ -418,7 +428,7 @@ internal static class HistogramEncoder
// check for it all the time nevertheless.
if (isIdx1Best && isIdx2Best)
{
- histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
+ histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@@ -439,18 +449,17 @@ internal static class HistogramEncoder
// Make sure the index order is respected.
if (p.Idx1 > p.Idx2)
{
- int tmp = p.Idx2;
- p.Idx2 = p.Idx1;
- p.Idx1 = tmp;
+ (p.Idx1, p.Idx2) = (p.Idx2, p.Idx1);
}
if (doEval)
{
// Re-evaluate the cost of an updated pair.
- HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0.0d, p);
- if (p.CostDiff >= 0.0d)
+ HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p);
+
+ if (p.CostDiff >= 0D)
{
- histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
+ histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@@ -463,20 +472,18 @@ internal static class HistogramEncoder
triesWithNoSuccess = 0;
}
- bool doGreedy = numUsed <= minClusterSize;
-
- return doGreedy;
+ return numUsed <= minClusterSize;
}
- private static void HistogramCombineGreedy(List histograms)
+ private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
{
int histoSize = histograms.Count(h => h != null);
// Priority list of histogram pairs.
- var histoPriorityList = new List();
+ List histoPriorityList = new();
int maxSize = histoSize * histoSize;
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i < histoSize; i++)
{
@@ -509,11 +516,11 @@ internal static class HistogramEncoder
// Remove pairs intersecting the just combined best pair.
for (int i = 0; i < histoPriorityList.Count;)
{
- HistogramPair p = histoPriorityList.ElementAt(i);
+ HistogramPair p = histoPriorityList[i];
if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2)
{
// Replace item at pos i with the last one and shrinking the list.
- histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1];
+ histoPriorityList[i] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
}
else
@@ -536,12 +543,15 @@ internal static class HistogramEncoder
}
}
- private static void HistogramRemap(List input, List output, Span symbols)
+ private static void HistogramRemap(
+ Vp8LHistogramSet input,
+ Vp8LHistogramSet output,
+ Span symbols)
{
int inSize = input.Count;
int outSize = output.Count;
- var stats = new Vp8LStreaks();
- var bitsEntropy = new Vp8LBitEntropy();
+ Vp8LStreaks stats = new();
+ Vp8LBitEntropy bitsEntropy = new();
if (outSize > 1)
{
for (int i = 0; i < inSize; i++)
@@ -577,11 +587,11 @@ internal static class HistogramEncoder
}
// Recompute each output.
- int paletteCodeBits = output.First().PaletteCodeBits;
- output.Clear();
+ int paletteCodeBits = output[0].PaletteCodeBits;
for (int i = 0; i < outSize; i++)
{
- output.Add(new Vp8LHistogram(paletteCodeBits));
+ output[i].Clear();
+ output[i].PaletteCodeBits = paletteCodeBits;
}
for (int i = 0; i < inSize; i++)
@@ -600,20 +610,26 @@ internal static class HistogramEncoder
/// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy.
///
/// The cost of the pair, or 0 if it superior to threshold.
- private static double HistoPriorityListPush(List histoList, int maxSize, List histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy)
+ private static double HistoPriorityListPush(
+ List histoList,
+ int maxSize,
+ Vp8LHistogramSet histograms,
+ int idx1,
+ int idx2,
+ double threshold,
+ Vp8LStreaks stats,
+ Vp8LBitEntropy bitsEntropy)
{
- var pair = new HistogramPair();
+ HistogramPair pair = new();
if (histoList.Count == maxSize)
{
- return 0.0d;
+ return 0D;
}
if (idx1 > idx2)
{
- int tmp = idx2;
- idx2 = idx1;
- idx1 = tmp;
+ (idx1, idx2) = (idx2, idx1);
}
pair.Idx1 = idx1;
@@ -637,9 +653,16 @@ internal static class HistogramEncoder
}
///
- /// Update the cost diff and combo of a pair of histograms. This needs to be called when the the histograms have been merged with a third one.
+ /// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been
+ /// merged with a third one.
///
- private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double threshold, HistogramPair pair)
+ private static void HistoListUpdatePair(
+ Vp8LHistogram h1,
+ Vp8LHistogram h2,
+ Vp8LStreaks stats,
+ Vp8LBitEntropy bitsEntropy,
+ double threshold,
+ HistogramPair pair)
{
double sumCost = h1.BitCost + h2.BitCost;
pair.CostCombo = 0.0d;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
index 39ad967e38..027d4f7ee9 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
@@ -25,7 +25,7 @@ internal static class HuffmanUtils
0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf
};
- public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode)
+ public static void CreateHuffmanTree(Span histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode)
{
int numSymbols = huffCode.NumSymbols;
bufRle.AsSpan().Clear();
@@ -40,7 +40,7 @@ internal static class HuffmanUtils
/// Change the population counts in a way that the consequent
/// Huffman tree compression, especially its RLE-part, give smaller output.
///
- public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts)
+ public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span counts)
{
// 1) Let's make the Huffman code more compatible with rle encoding.
for (; length >= 0; --length)
@@ -116,7 +116,7 @@ internal static class HuffmanUtils
{
// We don't want to change value at counts[i],
// that is already belonging to the next stride. Thus - 1.
- counts[i - k - 1] = count;
+ counts[(int)(i - k - 1)] = count;
}
}
@@ -159,7 +159,7 @@ internal static class HuffmanUtils
/// The size of the histogram.
/// The tree depth limit.
/// How many bits are used for the symbol.
- public static void GenerateOptimalTree(Span tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
+ public static void GenerateOptimalTree(Span tree, Span histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
{
uint countMin;
int treeSizeOrig = 0;
@@ -177,7 +177,7 @@ internal static class HuffmanUtils
return;
}
- Span treePool = tree.Slice(treeSizeOrig);
+ Span treePool = tree[treeSizeOrig..];
// For block sizes with less than 64k symbols we never need to do a
// second iteration of this loop.
@@ -202,7 +202,7 @@ internal static class HuffmanUtils
}
// Build the Huffman tree.
- Span treeSlice = tree.Slice(0, treeSize);
+ Span treeSlice = tree[..treeSize];
treeSlice.Sort(HuffmanTree.Compare);
if (treeSize > 1)
@@ -357,7 +357,7 @@ internal static class HuffmanUtils
// Special case code with only one value.
if (offsets[WebpConstants.MaxAllowedCodeLength] == 1)
{
- var huffmanCode = new HuffmanCode()
+ HuffmanCode huffmanCode = new()
{
BitsUsed = 0,
Value = (uint)sorted[0]
@@ -390,7 +390,7 @@ internal static class HuffmanUtils
for (; countsLen > 0; countsLen--)
{
- var huffmanCode = new HuffmanCode()
+ HuffmanCode huffmanCode = new()
{
BitsUsed = len,
Value = (uint)sorted[symbol++]
@@ -432,7 +432,7 @@ internal static class HuffmanUtils
};
}
- var huffmanCode = new HuffmanCode
+ HuffmanCode huffmanCode = new()
{
BitsUsed = len - rootBits,
Value = (uint)sorted[symbol++]
diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
index 6a28e5b3fb..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,21 +23,22 @@ internal sealed class PixOrCopy
};
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,
Len = len
};
- public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff;
+ public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF;
public uint CacheIdx() => this.BgraOrDistance;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
index 649845b025..330d1c555e 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
@@ -125,7 +125,7 @@ internal class Vp8LBitEntropy
///
/// Get the entropy for the distribution 'X'.
///
- public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
+ public void BitsEntropyUnrefined(Span x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@@ -147,7 +147,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
- public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats)
+ public void GetCombinedEntropyUnrefined(Span x, Span y, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@@ -169,7 +169,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
- public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
+ public void GetEntropyUnrefined(Span x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
index 469e4c9ab0..4fdbb31d37 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
@@ -6,7 +6,9 @@ using System.Buffers;
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 @@ internal class Vp8LEncoder : IDisposable
///
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 @@ internal class Vp8LEncoder : IDisposable
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 @@ internal class Vp8LEncoder : IDisposable
/// 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 @@ internal class Vp8LEncoder : IDisposable
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 @@ internal class Vp8LEncoder : IDisposable
/// 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);
@@ -589,15 +650,21 @@ internal class Vp8LEncoder : IDisposable
Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0];
this.bitWriter.Reset(bwInit);
- Vp8LHistogram tmpHisto = new(cacheBits);
- List histogramImage = new(histogramImageXySize);
- for (int i = 0; i < histogramImageXySize; i++)
- {
- histogramImage.Add(new Vp8LHistogram(cacheBits));
- }
+ using OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits);
+ using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits);
// Build histogram image and symbols from backward references.
- HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
+ HistogramEncoder.GetHistoImageSymbols(
+ this.memoryAllocator,
+ width,
+ height,
+ refsBest,
+ this.quality,
+ this.HistoBits,
+ cacheBits,
+ histogramImage,
+ tmpHisto,
+ histogramSymbols);
// Create Huffman bit lengths and codes for each histogram image.
int histogramImageSize = histogramImage.Count;
@@ -676,11 +743,9 @@ internal class Vp8LEncoder : IDisposable
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))
{
- Vp8LBitWriter tmp = this.bitWriter;
- this.bitWriter = bitWriterBest;
- bitWriterBest = tmp;
+ (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
}
isFirstIteration = false;
@@ -787,13 +852,8 @@ internal class Vp8LEncoder : IDisposable
refsTmp1,
refsTmp2);
- List histogramImage = new()
- {
- new(cacheBits)
- };
-
// Build histogram image and symbols from backward references.
- histogramImage[0].StoreRefs(refs);
+ using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits);
// Create Huffman bit lengths and codes for each histogram image.
GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
@@ -833,7 +893,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{
int count = 0;
- Span symbols = this.scratch.Span.Slice(0, 2);
+ Span symbols = this.scratch.Span[..2];
symbols.Clear();
const int maxBits = 8;
const int maxSymbol = 1 << maxBits;
@@ -886,6 +946,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
{
+ // TODO: Allocations. This method is called in a loop.
int i;
byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes];
short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes];
@@ -996,7 +1057,12 @@ internal class Vp8LEncoder : IDisposable
}
}
- private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span histogramSymbols, HuffmanTreeCode[] huffmanCodes)
+ private void StoreImageToBitMask(
+ int width,
+ int histoBits,
+ Vp8LBackwardRefs backwardRefs,
+ Span histogramSymbols,
+ HuffmanTreeCode[] huffmanCodes)
{
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1;
int tileMask = histoBits == 0 ? 0 : -(1 << histoBits);
@@ -1008,10 +1074,10 @@ internal class Vp8LEncoder : IDisposable
int tileY = y & tileMask;
int histogramIx = histogramSymbols[0];
Span codes = huffmanCodes.AsSpan(5 * histogramIx);
- using List.Enumerator c = backwardRefs.Refs.GetEnumerator();
- while (c.MoveNext())
+
+ for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
- PixOrCopy v = c.Current;
+ PixOrCopy v = backwardRefs.Refs[i];
if (tileX != (x & tileMask) || tileY != (y & tileMask))
{
tileX = x & tileMask;
@@ -1024,7 +1090,7 @@ internal class Vp8LEncoder : IDisposable
{
for (int k = 0; k < 4; k++)
{
- int code = (int)v.Literal(Order[k]);
+ int code = v.Literal(Order[k]);
this.bitWriter.WriteHuffmanCode(codes[k], code);
}
}
@@ -1149,35 +1215,41 @@ internal class Vp8LEncoder : IDisposable
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.
@@ -1379,10 +1451,8 @@ internal class Vp8LEncoder : IDisposable
useLut = false;
break;
}
- else
- {
- buffer[ind] = (uint)j;
- }
+
+ buffer[ind] = (uint)j;
}
if (useLut)
@@ -1591,14 +1661,12 @@ internal class Vp8LEncoder : IDisposable
}
// Swap color(palette[bestIdx], palette[i]);
- uint best = palette[bestIdx];
- palette[bestIdx] = palette[i];
- palette[i] = best;
+ (palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]);
predict = palette[i];
}
}
- private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes)
+ private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes)
{
int maxNumSymbols = 0;
@@ -1609,13 +1677,25 @@ internal class Vp8LEncoder : IDisposable
int startIdx = 5 * i;
for (int k = 0; k < 5; k++)
{
- int numSymbols =
- k == 0 ? histo.NumCodes() :
- k == 4 ? WebpConstants.NumDistanceCodes : 256;
+ int numSymbols;
+ if (k == 0)
+ {
+ numSymbols = histo.NumCodes();
+ }
+ else if (k == 4)
+ {
+ numSymbols = WebpConstants.NumDistanceCodes;
+ }
+ else
+ {
+ numSymbols = 256;
+ }
+
huffmanCodes[startIdx + k].NumSymbols = numSymbols;
}
}
+ // TODO: Allocations.
int end = 5 * histogramImage.Count;
for (int i = 0; i < end; i++)
{
@@ -1629,8 +1709,9 @@ internal class Vp8LEncoder : IDisposable
}
// Create Huffman trees.
+ // TODO: Allocations.
bool[] bufRle = new bool[maxNumSymbols];
- Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols];
+ HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols];
for (int i = 0; i < histogramImage.Count; i++)
{
@@ -1682,8 +1763,18 @@ internal class Vp8LEncoder : IDisposable
histoBits++;
}
- return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits :
- histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits;
+ if (histoBits < WebpConstants.MinHuffmanBits)
+ {
+ return WebpConstants.MinHuffmanBits;
+ }
+ else if (histoBits > WebpConstants.MaxHuffmanBits)
+ {
+ return WebpConstants.MaxHuffmanBits;
+ }
+ else
+ {
+ return histoBits;
+ }
}
///
@@ -1720,11 +1811,7 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst)
- {
- Vp8LBitWriter tmp = src;
- src = dst;
- dst = tmp;
- }
+ => (dst, src) = (src, dst);
///
/// Calculates the bits used for the transformation.
@@ -1732,9 +1819,21 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetTransformBits(WebpEncodingMethod method, int histoBits)
{
- int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5;
- int res = histoBits > maxTransformBits ? maxTransformBits : histoBits;
- return res;
+ int maxTransformBits;
+ if ((int)method < 4)
+ {
+ maxTransformBits = 6;
+ }
+ else if (method > WebpEncodingMethod.Level4)
+ {
+ maxTransformBits = 4;
+ }
+ else
+ {
+ maxTransformBits = 5;
+ }
+
+ return histoBits > maxTransformBits ? maxTransformBits : histoBits;
}
[MethodImpl(InliningOptions.ShortMethod)]
@@ -1812,9 +1911,9 @@ internal class Vp8LEncoder : IDisposable
///
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();
}
}
@@ -1823,9 +1922,9 @@ internal class Vp8LEncoder : IDisposable
{
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/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
index 5ec3f0d53d..f473977908 100644
--- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
@@ -1,63 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
+using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
-internal sealed class Vp8LHistogram : IDeepCloneable
+internal abstract unsafe class Vp8LHistogram
{
private const uint NonTrivialSym = 0xffffffff;
+ private readonly uint* red;
+ private readonly uint* blue;
+ private readonly uint* alpha;
+ private readonly uint* distance;
+ private readonly uint* literal;
+ private readonly uint* isUsed;
+
+ private const int RedSize = WebpConstants.NumLiteralCodes;
+ private const int BlueSize = WebpConstants.NumLiteralCodes;
+ private const int AlphaSize = WebpConstants.NumLiteralCodes;
+ private const int DistanceSize = WebpConstants.NumDistanceCodes;
+ public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1;
+ private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance
+ public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize;
///
/// Initializes a new instance of the class.
///
- /// The histogram to create an instance from.
- private Vp8LHistogram(Vp8LHistogram other)
- : this(other.PaletteCodeBits)
- {
- other.Red.AsSpan().CopyTo(this.Red);
- other.Blue.AsSpan().CopyTo(this.Blue);
- other.Alpha.AsSpan().CopyTo(this.Alpha);
- other.Literal.AsSpan().CopyTo(this.Literal);
- other.Distance.AsSpan().CopyTo(this.Distance);
- other.IsUsed.AsSpan().CopyTo(this.IsUsed);
- this.LiteralCost = other.LiteralCost;
- this.RedCost = other.RedCost;
- this.BlueCost = other.BlueCost;
- this.BitCost = other.BitCost;
- this.TrivialSymbol = other.TrivialSymbol;
- this.PaletteCodeBits = other.PaletteCodeBits;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
+ /// The base pointer to the backing memory.
/// The backward references to initialize the histogram with.
/// The palette code bits.
- public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits)
- : this(paletteCodeBits) => this.StoreRefs(refs);
+ protected Vp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits)
+ : this(basePointer, paletteCodeBits) => this.StoreRefs(refs);
///
/// Initializes a new instance of the class.
///
+ /// The base pointer to the backing memory.
/// The palette code bits.
- public Vp8LHistogram(int paletteCodeBits)
+ protected Vp8LHistogram(uint* basePointer, int paletteCodeBits)
{
this.PaletteCodeBits = paletteCodeBits;
- this.Red = new uint[WebpConstants.NumLiteralCodes + 1];
- this.Blue = new uint[WebpConstants.NumLiteralCodes + 1];
- this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1];
- this.Distance = new uint[WebpConstants.NumDistanceCodes];
-
- int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits);
- this.Literal = new uint[literalSize + 1];
-
- // 5 for literal, red, blue, alpha, distance.
- this.IsUsed = new bool[5];
+ this.red = basePointer;
+ this.blue = this.red + RedSize;
+ this.alpha = this.blue + BlueSize;
+ this.distance = this.alpha + AlphaSize;
+ this.literal = this.distance + DistanceSize;
+ this.isUsed = this.literal + LiteralSize;
}
///
@@ -85,22 +78,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable
///
public double BlueCost { get; set; }
- public uint[] Red { get; }
+ public Span Red => new(this.red, RedSize);
- public uint[] Blue { get; }
+ public Span Blue => new(this.blue, BlueSize);
- public uint[] Alpha { get; }
+ public Span Alpha => new(this.alpha, AlphaSize);
- public uint[] Literal { get; }
+ public Span Distance => new(this.distance, DistanceSize);
- public uint[] Distance { get; }
+ public Span Literal => new(this.literal, LiteralSize);
public uint TrivialSymbol { get; set; }
- public bool[] IsUsed { get; }
+ private Span IsUsedSpan => new(this.isUsed, UsedSize);
+
+ private Span TotalSpan => new(this.red, BufferSize);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0;
+
+ ///
+ /// Creates a copy of the given class.
+ ///
+ /// The histogram to copy to.
+ public void CopyTo(Vp8LHistogram other)
+ {
+ this.Red.CopyTo(other.Red);
+ this.Blue.CopyTo(other.Blue);
+ this.Alpha.CopyTo(other.Alpha);
+ this.Literal.CopyTo(other.Literal);
+ this.Distance.CopyTo(other.Distance);
+ this.IsUsedSpan.CopyTo(other.IsUsedSpan);
+
+ other.LiteralCost = this.LiteralCost;
+ other.RedCost = this.RedCost;
+ other.BlueCost = this.BlueCost;
+ other.BitCost = this.BitCost;
+ other.TrivialSymbol = this.TrivialSymbol;
+ other.PaletteCodeBits = this.PaletteCodeBits;
+ }
- ///
- public IDeepCloneable DeepClone() => new Vp8LHistogram(this);
+ public void Clear()
+ {
+ this.TotalSpan.Clear();
+ this.PaletteCodeBits = 0;
+ this.BitCost = 0;
+ this.LiteralCost = 0;
+ this.RedCost = 0;
+ this.BlueCost = 0;
+ this.TrivialSymbol = 0;
+ }
///
/// Collect all the references into a histogram (without reset).
@@ -108,10 +138,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// The backward references.
public void StoreRefs(Vp8LBackwardRefs refs)
{
- using List.Enumerator c = refs.Refs.GetEnumerator();
- while (c.MoveNext())
+ for (int i = 0; i < refs.Refs.Count; i++)
{
- this.AddSinglePixOrCopy(c.Current, false);
+ this.AddSinglePixOrCopy(refs.Refs[i], false);
}
}
@@ -163,12 +192,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
{
uint notUsed = 0;
return
- PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0], stats, bitsEntropy)
- + PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1], stats, bitsEntropy)
- + PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2], stats, bitsEntropy)
- + PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3], stats, bitsEntropy)
- + PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy)
- + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes)
+ this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy)
+ + this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy)
+ + this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy)
+ + this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy)
+ + this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy)
+ + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes)
+ ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
}
@@ -177,12 +206,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
uint alphaSym = 0, redSym = 0, blueSym = 0;
uint notUsed = 0;
- double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3], stats, bitsEntropy);
- double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
+ double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy);
+ double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
int numCodes = this.NumCodes();
- this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
- this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1], stats, bitsEntropy);
- this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2], stats, bitsEntropy);
+ this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
+ this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy);
+ this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy);
this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost;
if ((alphaSym | redSym | blueSym) == NonTrivialSym)
{
@@ -234,7 +263,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
for (int i = 0; i < 5; i++)
{
- output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i];
+ output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i));
}
output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol
@@ -247,9 +276,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bool trivialAtEnd = false;
cost = costInitial;
- cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false, stats, bitEntropy);
+ cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy);
- cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
+ cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
if (cost > costThreshold)
{
@@ -270,155 +299,158 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
- cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd, stats, bitEntropy);
+ cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
- cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd, stats, bitEntropy);
+ cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
- cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd, stats, bitEntropy);
+ cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
- cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false, stats, bitEntropy);
+ cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes);
- if (cost > costThreshold)
- {
- return false;
- }
-
- return true;
+ return cost <= costThreshold;
}
private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize)
{
- if (this.IsUsed[0])
+ if (this.IsUsed(0))
{
- if (b.IsUsed[0])
+ if (b.IsUsed(0))
{
AddVector(this.Literal, b.Literal, output.Literal, literalSize);
}
else
{
- this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
+ this.Literal[..literalSize].CopyTo(output.Literal);
}
}
- else if (b.IsUsed[0])
+ else if (b.IsUsed(0))
{
- b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
+ b.Literal[..literalSize].CopyTo(output.Literal);
}
else
{
- output.Literal.AsSpan(0, literalSize).Clear();
+ output.Literal[..literalSize].Clear();
}
}
private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size)
{
- if (this.IsUsed[1])
+ if (this.IsUsed(1))
{
- if (b.IsUsed[1])
+ if (b.IsUsed(1))
{
AddVector(this.Red, b.Red, output.Red, size);
}
else
{
- this.Red.AsSpan(0, size).CopyTo(output.Red);
+ this.Red[..size].CopyTo(output.Red);
}
}
- else if (b.IsUsed[1])
+ else if (b.IsUsed(1))
{
- b.Red.AsSpan(0, size).CopyTo(output.Red);
+ b.Red[..size].CopyTo(output.Red);
}
else
{
- output.Red.AsSpan(0, size).Clear();
+ output.Red[..size].Clear();
}
}
private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size)
{
- if (this.IsUsed[2])
+ if (this.IsUsed(2))
{
- if (b.IsUsed[2])
+ if (b.IsUsed(2))
{
AddVector(this.Blue, b.Blue, output.Blue, size);
}
else
{
- this.Blue.AsSpan(0, size).CopyTo(output.Blue);
+ this.Blue[..size].CopyTo(output.Blue);
}
}
- else if (b.IsUsed[2])
+ else if (b.IsUsed(2))
{
- b.Blue.AsSpan(0, size).CopyTo(output.Blue);
+ b.Blue[..size].CopyTo(output.Blue);
}
else
{
- output.Blue.AsSpan(0, size).Clear();
+ output.Blue[..size].Clear();
}
}
private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size)
{
- if (this.IsUsed[3])
+ if (this.IsUsed(3))
{
- if (b.IsUsed[3])
+ if (b.IsUsed(3))
{
AddVector(this.Alpha, b.Alpha, output.Alpha, size);
}
else
{
- this.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
+ this.Alpha[..size].CopyTo(output.Alpha);
}
}
- else if (b.IsUsed[3])
+ else if (b.IsUsed(3))
{
- b.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
+ b.Alpha[..size].CopyTo(output.Alpha);
}
else
{
- output.Alpha.AsSpan(0, size).Clear();
+ output.Alpha[..size].Clear();
}
}
private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size)
{
- if (this.IsUsed[4])
+ if (this.IsUsed(4))
{
- if (b.IsUsed[4])
+ if (b.IsUsed(4))
{
AddVector(this.Distance, b.Distance, output.Distance, size);
}
else
{
- this.Distance.AsSpan(0, size).CopyTo(output.Distance);
+ this.Distance[..size].CopyTo(output.Distance);
}
}
- else if (b.IsUsed[4])
+ else if (b.IsUsed(4))
{
- b.Distance.AsSpan(0, size).CopyTo(output.Distance);
+ b.Distance[..size].CopyTo(output.Distance);
}
else
{
- output.Distance.AsSpan(0, size).Clear();
+ output.Distance[..size].Clear();
}
}
- private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
+ private static double GetCombinedEntropy(
+ Span x,
+ Span y,
+ int length,
+ bool isXUsed,
+ bool isYUsed,
+ bool trivialAtEnd,
+ Vp8LStreaks stats,
+ Vp8LBitEntropy bitEntropy)
{
stats.Clear();
bitEntropy.Init();
@@ -450,18 +482,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bitEntropy.GetEntropyUnrefined(x, length, stats);
}
}
+ else if (isYUsed)
+ {
+ bitEntropy.GetEntropyUnrefined(y, length, stats);
+ }
else
{
- if (isYUsed)
- {
- bitEntropy.GetEntropyUnrefined(y, length, stats);
- }
- else
- {
- stats.Counts[0] = 1;
- stats.Streaks[0][length > 3 ? 1 : 0] = length;
- bitEntropy.Init();
- }
+ stats.Counts[0] = 1;
+ stats.Streaks[0][length > 3 ? 1 : 0] = length;
+ bitEntropy.Init();
}
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
@@ -482,7 +511,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
///
/// Get the symbol entropy for the distribution 'population'.
///
- private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
+ private double PopulationCost(Span population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
{
bitEntropy.Init();
stats.Clear();
@@ -491,7 +520,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym;
// The histogram is used if there is at least one non-zero streak.
- isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0;
+ this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0);
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
}
@@ -557,3 +586,56 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
}
+
+internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable
+{
+ private readonly IMemoryOwner bufferOwner;
+ private MemoryHandle bufferHandle;
+ private bool isDisposed;
+
+ private OwnedVp8LHistogram(
+ IMemoryOwner bufferOwner,
+ ref MemoryHandle bufferHandle,
+ uint* basePointer,
+ int paletteCodeBits)
+ : base(basePointer, paletteCodeBits)
+ {
+ this.bufferOwner = bufferOwner;
+ this.bufferHandle = bufferHandle;
+ }
+
+ ///
+ /// Creates an that is not a member of a .
+ ///
+ /// The memory allocator.
+ /// The palette code bits.
+ public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits)
+ {
+ IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean);
+ MemoryHandle bufferHandle = bufferOwner.Memory.Pin();
+ return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits);
+ }
+
+ ///
+ /// Creates an that is not a member of a .
+ ///
+ /// The memory allocator.
+ /// The backward references to initialize the histogram with.
+ /// The palette code bits.
+ public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits)
+ {
+ OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits);
+ histogram.StoreRefs(refs);
+ return histogram;
+ }
+
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.bufferHandle.Dispose();
+ this.bufferOwner.Dispose();
+ this.isDisposed = true;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs
new file mode 100644
index 0000000000..a46838ee67
--- /dev/null
+++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+#nullable disable
+
+using System.Buffers;
+using System.Collections;
+using System.Diagnostics;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
+
+internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable
+{
+ private readonly IMemoryOwner buffer;
+ private MemoryHandle bufferHandle;
+ private readonly List items;
+ private bool isDisposed;
+
+ public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits)
+ {
+ this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
+ this.bufferHandle = this.buffer.Memory.Pin();
+
+ unsafe
+ {
+ uint* basePointer = (uint*)this.bufferHandle.Pointer;
+ this.items = new List(capacity);
+ for (int i = 0; i < capacity; i++)
+ {
+ this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits));
+ }
+ }
+ }
+
+ public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits)
+ {
+ this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
+ this.bufferHandle = this.buffer.Memory.Pin();
+
+ unsafe
+ {
+ uint* basePointer = (uint*)this.bufferHandle.Pointer;
+ this.items = new List(capacity);
+ for (int i = 0; i < capacity; i++)
+ {
+ this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits));
+ }
+ }
+ }
+
+ public Vp8LHistogramSet(int capacity) => this.items = new(capacity);
+
+ public Vp8LHistogramSet() => this.items = new();
+
+ public int Count => this.items.Count;
+
+ public Vp8LHistogram this[int index]
+ {
+ get => this.items[index];
+ set => this.items[index] = value;
+ }
+
+ public void RemoveAt(int index)
+ {
+ this.CheckDisposed();
+ this.items.RemoveAt(index);
+ }
+
+ public void Dispose()
+ {
+ if (this.isDisposed)
+ {
+ return;
+ }
+
+ this.buffer.Dispose();
+ this.bufferHandle.Dispose();
+ this.items.Clear();
+ this.isDisposed = true;
+ }
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)this.items).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator();
+
+ [Conditional("DEBUG")]
+ private void CheckDisposed()
+ {
+ if (this.isDisposed)
+ {
+ ThrowDisposed();
+ }
+ }
+
+ private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet));
+
+ private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram
+ {
+ public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits)
+ : base(basePointer, paletteCodeBits)
+ {
+ }
+
+ public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits)
+ : base(basePointer, refs, paletteCodeBits)
+ {
+ }
+ }
+}
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 @@ internal sealed class WebpLosslessDecoder
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 @@ internal sealed class WebpLosslessDecoder
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 @@ internal sealed class WebpLosslessDecoder
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 @@ internal class Vp8EncIterator
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 @@ internal class Vp8EncIterator
{
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 @@ internal class Vp8EncIterator
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 @@ 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,27 +311,94 @@ internal class Vp8Encoder : IDisposable
///
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 @@ 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 +456,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
- image,
+ frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@@ -408,16 +471,39 @@ internal class Vp8Encoder : IDisposable
}
}
- 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 @@ internal class Vp8Encoder : IDisposable
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 @@ internal class Vp8Encoder : IDisposable
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 @@ internal class Vp8Encoder : IDisposable
it.NzToBytes();
- int pos1 = this.bitWriter.NumBytes();
+ int pos1 = this.bitWriter.NumBytes;
if (i16)
{
residual.Init(0, 1, this.Proba);
@@ -1054,7 +1141,7 @@ internal class Vp8Encoder : IDisposable
}
}
- int pos2 = this.bitWriter.NumBytes();
+ int pos2 = this.bitWriter.NumBytes;
// U/V
residual.Init(0, 2, this.Proba);
@@ -1072,7 +1159,7 @@ internal class Vp8Encoder : IDisposable
}
}
- 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 @@ internal sealed class WebpLossyDecoder
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 @@ internal sealed class WebpLossyDecoder
{
// 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 @@ internal sealed class WebpLossyDecoder
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 @@ internal sealed class WebpLossyDecoder
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 @@ internal sealed class WebpLossyDecoder
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 @@ internal static class YuvConversion
/// 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 @@ internal class WebpAnimationDecoder : IDisposable
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 @@ internal class WebpAnimationDecoder : IDisposable
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 @@ internal class WebpAnimationDecoder : IDisposable
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 @@ internal class WebpAnimationDecoder : IDisposable
{
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 @@ internal class WebpAnimationDecoder : IDisposable
{
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 @@ internal class WebpAnimationDecoder : IDisposable
/// 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 @@ internal class WebpAnimationDecoder : IDisposable
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 @@ internal class WebpAnimationDecoder : IDisposable
/// 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 @@ internal class WebpAnimationDecoder : IDisposable
}
catch
{
- decodedImage?.Dispose();
+ decodedFrame?.Dispose();
throw;
}
finally
@@ -287,48 +288,43 @@ internal class WebpAnimationDecoder : IDisposable
/// 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 @@ internal class WebpAnimationDecoder : IDisposable
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 @@ internal static class WebpChunkParsingUtils
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 @@ internal static class WebpChunkParsingUtils
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 @@ internal static class WebpChunkParsingUtils
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 @@ internal static class WebpChunkParsingUtils
// 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 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
- return new WebpImageInfo()
+ return new WebpImageInfo
{
Width = width,
Height = height,
@@ -231,13 +225,13 @@ internal static class WebpChunkParsingUtils
}
// 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 @@ internal static class WebpChunkParsingUtils
/// 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 @@ internal static class WebpChunkParsingUtils
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 @@ internal static class WebpChunkParsingUtils
/// 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 @@ internal static class WebpChunkParsingUtils
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 @@ public sealed class WebpDecoder : SpecializedImageDecoder
///
/// 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 @@ public sealed class WebpDecoder : SpecializedImageDecoder
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 @@ public sealed class WebpDecoder : SpecializedImageDecoder
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 @@ public sealed class WebpDecoder : SpecializedImageDecoder
=> 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.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
+using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@@ -89,25 +91,30 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
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 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
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 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return;
}
- metadata.ExifProfile = new(exifData);
+ metadata.ExifProfile = new ExifProfile(exifData);
}
}
@@ -359,7 +366,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
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 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless)
{
- using Vp8LEncoder enc = new(
+ using Vp8LEncoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@@ -140,11 +140,38 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
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 @@ 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/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 @@ public sealed class WebpFormat : IImageFormat
///
/// 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 @@ public sealed class WebpFormat : IImageFormat
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 class WebpFrameMetadata : IDeepCloneable
/// 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 @@ public class WebpMetadata : IDeepCloneable
{
this.FileFormat = other.FileFormat;
this.AnimationLoopCount = other.AnimationLoopCount;
+ this.AnimationBackground = other.AnimationBackground;
}
///
@@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable
///
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 sealed class IccProfile : IDeepCloneable
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 sealed class IccProfile : IDeepCloneable
return copy;
}
- IccWriter writer = new();
return IccWriter.Write(this);
}
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
index 65b4ae2c31..c5fa8d03da 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
@@ -43,9 +43,9 @@ public class EncodeWebp
[Benchmark(Description = "Magick Webp Lossy")]
public void MagickWebpLossy()
{
- using var memoryStream = new MemoryStream();
+ using MemoryStream memoryStream = new();
- var defines = new WebPWriteDefines
+ WebPWriteDefines defines = new()
{
Lossless = false,
Method = 4,
@@ -65,7 +65,7 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossy")]
public void ImageSharpWebpLossy()
{
- using var memoryStream = new MemoryStream();
+ using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossy,
@@ -80,8 +80,8 @@ public class EncodeWebp
[Benchmark(Baseline = true, Description = "Magick Webp Lossless")]
public void MagickWebpLossless()
{
- using var memoryStream = new MemoryStream();
- var defines = new WebPWriteDefines
+ using MemoryStream memoryStream = new();
+ WebPWriteDefines defines = new()
{
Lossless = true,
Method = 4,
@@ -97,12 +97,13 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossless")]
public void ImageSharpWebpLossless()
{
- using var memoryStream = new MemoryStream();
+ using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossless,
Method = WebpEncodingMethod.Level4,
NearLossless = false,
+ Quality = 75,
// This is equal to exact = false in libwebp, which is the default.
TransparentColorMode = WebpTransparentColorMode.Clear
diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
index 11c4bb62e7..9c48e61823 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
@@ -11,7 +11,7 @@ public class DominantCostRangeTests
[Fact]
public void DominantCost_Constructor()
{
- var dominantCostRange = new DominantCostRange();
+ DominantCostRange dominantCostRange = new();
Assert.Equal(0, dominantCostRange.LiteralMax);
Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin);
Assert.Equal(0, dominantCostRange.RedMax);
@@ -24,13 +24,11 @@ public class DominantCostRangeTests
public void UpdateDominantCostRange_Works()
{
// arrange
- var dominantCostRange = new DominantCostRange();
- var histogram = new Vp8LHistogram(10)
- {
- LiteralCost = 1.0d,
- RedCost = 2.0d,
- BlueCost = 3.0d
- };
+ DominantCostRange dominantCostRange = new();
+ using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10);
+ histogram.LiteralCost = 1.0d;
+ histogram.RedCost = 2.0d;
+ histogram.BlueCost = 3.0d;
// act
dominantCostRange.UpdateDominantCostRange(histogram);
@@ -50,7 +48,7 @@ public class DominantCostRangeTests
public void GetHistoBinIndex_Works(int partitions, int expectedIndex)
{
// arrange
- var dominantCostRange = new DominantCostRange()
+ DominantCostRange dominantCostRange = new()
{
BlueMax = 253.4625,
BlueMin = 109.0,
@@ -59,13 +57,12 @@ public class DominantCostRangeTests
RedMax = 191.0,
RedMin = 109.0
};
- var histogram = new Vp8LHistogram(6)
- {
- LiteralCost = 247.0d,
- RedCost = 112.0d,
- BlueCost = 202.0d,
- BitCost = 733.0d
- };
+ using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6);
+ histogram.LiteralCost = 247.0d;
+ histogram.RedCost = 112.0d;
+ histogram.BlueCost = 202.0d;
+ histogram.BitCost = 733.0d;
+
dominantCostRange.UpdateDominantCostRange(histogram);
// act
diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs
index 39c3c89550..cfe79e49e6 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Webp.Lossless;
+using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Webp;
@@ -65,7 +66,7 @@ public class Vp8LHistogramTests
// All remaining values are expected to be zero.
literals.AsSpan().CopyTo(expectedLiterals);
- var backwardRefs = new Vp8LBackwardRefs(pixelData.Length);
+ Vp8LBackwardRefs backwardRefs = new(pixelData.Length);
for (int i = 0; i < pixelData.Length; i++)
{
backwardRefs.Add(new PixOrCopy()
@@ -76,15 +77,16 @@ public class Vp8LHistogramTests
});
}
- var histogram0 = new Vp8LHistogram(backwardRefs, 3);
- var histogram1 = new Vp8LHistogram(backwardRefs, 3);
+ MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
+ using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
+ using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
for (int i = 0; i < 5; i++)
{
- histogram0.IsUsed[i] = true;
- histogram1.IsUsed[i] = true;
+ histogram0.IsUsed(i, true);
+ histogram1.IsUsed(i, true);
}
- var output = new Vp8LHistogram(3);
+ using OwnedVp8LHistogram output = OwnedVp8LHistogram.Create(memoryAllocator, 3);
// act
histogram0.Add(histogram1, output);
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 class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.Equal(0, webpMetaData.AnimationLoopCount);
- Assert.Equal(150U, frameMetaData.FrameDuration);
+ Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@@ -325,7 +325,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
Assert.Equal(0, webpMetaData.AnimationLoopCount);
- Assert.Equal(150U, frameMetaData.FrameDuration);
+ Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@@ -357,6 +357,16 @@ public class WebpDecoderTests
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 class YuvConversionTests
};
// 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 class YuvConversionTests
};
// 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 405a6c3810..02429b63c3 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -683,6 +683,7 @@ public static class TestImages
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