Browse Source

Merge pull request #2569 from Poker-sang/animated-webp-encoder

Animated webp encoder
pull/2582/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
afe21339af
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 124
      src/ImageSharp/Common/Helpers/RiffHelper.cs
  2. 2
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  3. 40
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  4. 48
      src/ImageSharp/Formats/Webp/AnimationFrameData.cs
  5. 239
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  6. 240
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  7. 91
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  8. 37
      src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs
  9. 140
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  10. 113
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  11. 2
      src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
  12. 2
      src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
  13. 7
      src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
  14. 179
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  15. 113
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  16. 11
      src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
  17. 153
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  18. 187
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  19. 6
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  20. 158
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  21. 2
      src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
  22. 63
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  23. 11
      src/ImageSharp/Formats/Webp/WebpChunkType.cs
  24. 43
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  25. 9
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  26. 29
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  27. 2
      src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
  28. 2
      src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
  29. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  30. 62
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  31. 6
      src/ImageSharp/Formats/Webp/WebpFormat.cs
  32. 19
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  33. 9
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  34. 4
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  35. 14
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  36. 43
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  37. 4
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  38. 1
      tests/ImageSharp.Tests/TestImages.cs
  39. 3
      tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp
  40. 3
      tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp
  41. 3
      tests/Images/Input/Webp/landscape.webp

124
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
{
/// <summary>
/// The header bytes identifying RIFF file.
/// </summary>
private const uint RiffFourCc = 0x52_49_46_46;
public static void WriteRiffFile(Stream stream, string formType, Action<Stream> func) =>
WriteChunk(stream, RiffFourCc, s =>
{
s.Write(Encoding.ASCII.GetBytes(formType));
func(s);
});
public static void WriteChunk(Stream stream, uint fourCc, Action<Stream> func)
{
Span<byte> 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<byte> data)
{
Span<byte> 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<TStruct>(Stream stream, uint fourCc, in TStruct chunk)
where TStruct : unmanaged
{
fixed (TStruct* ptr = &chunk)
{
WriteChunk(stream, fourCc, new Span<byte>(ptr, sizeof(TStruct)));
}
}
public static long BeginWriteChunk(Stream stream, uint fourCc)
{
Span<byte> 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<byte> 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);
}

2
src/ImageSharp/Formats/Webp/AlphaDecoder.cs

@ -59,7 +59,7 @@ internal class AlphaDecoder : IDisposable
if (this.Compressed) if (this.Compressed)
{ {
Vp8LBitReader bitReader = new(data); Vp8LBitReader bitReader = new Vp8LBitReader(data);
this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration);
this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true);

40
src/ImageSharp/Formats/Webp/AlphaEncoder.cs

@ -19,7 +19,7 @@ internal static class AlphaEncoder
/// Data is either compressed as lossless webp image or uncompressed. /// Data is either compressed as lossless webp image or uncompressed.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param> /// <param name="memoryAllocator">The memory manager.</param>
/// <param name="skipMetadata">Whether to skip metadata encoding.</param> /// <param name="skipMetadata">Whether to skip metadata encoding.</param>
@ -27,7 +27,7 @@ internal static class AlphaEncoder
/// <param name="size">The size in bytes of the alpha data.</param> /// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns> /// <returns>The encoded alpha data.</returns>
public static IMemoryOwner<byte> EncodeAlpha<TPixel>( public static IMemoryOwner<byte> EncodeAlpha<TPixel>(
Image<TPixel> image, ImageFrame<TPixel> frame,
Configuration configuration, Configuration configuration,
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
bool skipMetadata, bool skipMetadata,
@ -35,9 +35,9 @@ internal static class AlphaEncoder
out int size) out int size)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
IMemoryOwner<byte> alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); IMemoryOwner<byte> alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator);
if (compress) 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 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, // The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression. // that can improve compression.
using Image<Rgba32> alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan()); using ImageFrame<Rgba32> alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan());
size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, alphaData); size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData);
return alphaData; return alphaData;
} }
@ -73,19 +73,19 @@ internal static class AlphaEncoder
/// Store the transparency in the green channel. /// Store the transparency in the green channel.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param> /// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param>
/// <returns>The transparency image.</returns> /// <returns>The transparency frame.</returns>
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData) private static ImageFrame<Rgba32> DispatchAlphaToGreen<TPixel>(ImageFrame<TPixel> frame, Span<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
Image<Rgba32> alphaAsImage = new(width, height); ImageFrame<Rgba32> alphaAsFrame = new ImageFrame<Rgba32>(Configuration.Default, width, height);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
Memory<Rgba32> rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); Memory<Rgba32> rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y);
Span<Rgba32> pixelRow = rowBuffer.Span; Span<Rgba32> pixelRow = rowBuffer.Span;
Span<byte> alphaRow = alphaData.Slice(y * width, width); Span<byte> alphaRow = alphaData.Slice(y * width, width);
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
@ -95,23 +95,23 @@ internal static class AlphaEncoder
} }
} }
return alphaAsImage; return alphaAsFrame;
} }
/// <summary> /// <summary>
/// Extract the alpha data of the image. /// Extract the alpha data of the image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param> /// <param name="memoryAllocator">The memory manager.</param>
/// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns> /// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns>
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator) private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(ImageFrame<TPixel> frame, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int height = image.Height; int height = frame.Height;
int width = image.Width; int width = frame.Width;
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height); IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
Span<byte> alphaData = alphaDataBuffer.GetSpan(); Span<byte> alphaData = alphaDataBuffer.GetSpan();

48
src/ImageSharp/Formats/Webp/AnimationFrameData.cs

@ -1,48 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Webp;
internal struct AnimationFrameData
{
/// <summary>
/// The animation chunk size.
/// </summary>
public uint DataSize;
/// <summary>
/// The X coordinate of the upper left corner of the frame is Frame X * 2.
/// </summary>
public uint X;
/// <summary>
/// The Y coordinate of the upper left corner of the frame is Frame Y * 2.
/// </summary>
public uint Y;
/// <summary>
/// The width of the frame.
/// </summary>
public uint Width;
/// <summary>
/// The height of the frame.
/// </summary>
public uint Height;
/// <summary>
/// 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.
/// </summary>
public uint Duration;
/// <summary>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public AnimationBlendingMethod BlendingMethod;
/// <summary>
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public AnimationDisposalMethod DisposalMethod;
}

239
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -1,9 +1,11 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Diagnostics;
using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -14,18 +16,11 @@ internal abstract class BitWriterBase
private const ulong MaxCanvasPixels = 4294967295ul; private const ulong MaxCanvasPixels = 4294967295ul;
protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
/// <summary> /// <summary>
/// Buffer to write to. /// Buffer to write to.
/// </summary> /// </summary>
private byte[] buffer; private byte[] buffer;
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BitWriterBase"/> class. /// Initializes a new instance of the <see cref="BitWriterBase"/> class.
/// </summary> /// </summary>
@ -41,17 +36,23 @@ internal abstract class BitWriterBase
public byte[] Buffer => this.buffer; public byte[] Buffer => this.buffer;
/// <summary>
/// Gets the number of bytes of the encoded image data.
/// </summary>
/// <returns>The number of bytes of the image data.</returns>
public abstract int NumBytes { get; }
/// <summary> /// <summary>
/// Writes the encoded bytes of the image to the stream. Call Finish() before this. /// Writes the encoded bytes of the image to the stream. Call Finish() before this.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
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));
/// <summary> /// <summary>
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
/// </summary> /// </summary>
/// <param name="dest">The destination buffer.</param> /// <param name="dest">The destination buffer.</param>
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest); public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest);
/// <summary> /// <summary>
/// Resizes the buffer to write to. /// Resizes the buffer to write to.
@ -59,12 +60,6 @@ internal abstract class BitWriterBase
/// <param name="extraSize">The extra size in bytes needed.</param> /// <param name="extraSize">The extra size in bytes needed.</param>
public abstract void BitWriterResize(int extraSize); public abstract void BitWriterResize(int extraSize);
/// <summary>
/// Returns the number of bytes of the encoded image data.
/// </summary>
/// <returns>The number of bytes of the image data.</returns>
public abstract int NumBytes();
/// <summary> /// <summary>
/// Flush leftover bits. /// Flush leftover bits.
/// </summary> /// </summary>
@ -84,63 +79,89 @@ internal abstract class BitWriterBase
} }
/// <summary> /// <summary>
/// Writes the RIFF header to the stream. /// Write the trunks before data trunk.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="riffSize">The block length.</param> /// <param name="width">The width of the image.</param>
protected void WriteRiffHeader(Stream stream, uint riffSize) /// <param name="height">The height of the image.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public static void WriteTrunksBeforeData(
Stream stream,
uint width,
uint height,
ExifProfile? exifProfile,
XmpProfile? xmpProfile,
IccProfile? iccProfile,
bool hasAlpha,
bool hasAnimation)
{ {
stream.Write(WebpConstants.RiffFourCc); // Write file size later
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
stream.Write(this.scratchBuffer.Span.Slice(0, 4));
stream.Write(WebpConstants.WebpHeader); 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());
}
}
} }
/// <summary> /// <summary>
/// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// Writes the encoded image to the stream.
/// </summary> /// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param> /// <param name="stream">The stream to write to.</param>
/// <returns>The metadata chunk size in bytes.</returns> public abstract void WriteEncodedImageToStream(Stream stream);
protected static uint MetadataChunkSize(byte[] metadataBytes)
{
uint metaSize = (uint)metadataBytes.Length;
return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
}
/// <summary> /// <summary>
/// Calculates the chunk size of a alpha chunk. /// Write the trunks after data trunk.
/// </summary> /// </summary>
/// <param name="alphaBytes">The alpha chunk bytes.</param> /// <param name="stream">The stream to write to.</param>
/// <returns>The alpha data chunk size in bytes.</returns> /// <param name="exifProfile">The exif profile.</param>
protected static uint AlphaChunkSize(Span<byte> alphaBytes) /// <param name="xmpProfile">The XMP profile.</param>
public static void WriteTrunksAfterData(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{ {
uint alphaSize = (uint)alphaBytes.Length + 1; if (exifProfile != null)
return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); {
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
}
if (xmpProfile != null)
{
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}
RiffHelper.EndWriteRiffFile(stream, 4);
} }
/// <summary> /// <summary>
/// Writes a metadata profile (EXIF or XMP) to the stream. /// Writes the animation parameter(<see cref="WebpChunkType.AnimationParameter"/>) to the stream.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="metadataBytes">The metadata profile's bytes.</param> /// <param name="background">
/// <param name="chunkType">The chuck type to write.</param> /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) /// 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.
/// </param>
/// <param name="loopCount">The number of times to loop the animation. If it is 0, this means infinitely.</param>
public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount)
{ {
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
chunk.WriteTo(stream);
uint size = (uint)metadataBytes.Length;
Span<byte> 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);
}
} }
/// <summary> /// <summary>
@ -149,53 +170,19 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="dataBytes">The alpha channel data bytes.</param> /// <param name="dataBytes">The alpha channel data bytes.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param> /// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed) public static void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{ {
uint size = (uint)dataBytes.Length + 1; long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha);
Span<byte> buf = this.scratchBuffer.Span.Slice(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
byte flags = 0; byte flags = 0;
if (alphaDataIsCompressed) if (alphaDataIsCompressed)
{ {
flags |= 1; // TODO: Filtering and preprocessing
flags = 1;
} }
stream.WriteByte(flags); stream.WriteByte(flags);
stream.Write(dataBytes); stream.Write(dataBytes);
RiffHelper.EndWriteChunk(stream, pos);
// Add padding byte if needed.
if ((size & 1) == 1)
{
stream.WriteByte(0);
}
}
/// <summary>
/// Writes the color profile to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes)
{
uint size = (uint)iccProfileBytes.Length;
Span<byte> 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);
}
} }
/// <summary> /// <summary>
@ -204,65 +191,17 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param> /// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param> /// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param> /// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param> /// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha) /// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
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) WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
{
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;
}
if (hasAlpha) chunk.Validate(MaxDimension, MaxCanvasPixels);
{
// Set alpha bit.
flags |= 16;
}
if (iccProfileBytes != null)
{
// Set iccp flag.
flags |= 32;
}
Span<byte> 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];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); chunk.WriteTo(stream);
} }
} }

240
src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

@ -3,9 +3,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Webp.Lossy; 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; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -72,7 +69,7 @@ internal class Vp8BitWriter : BitWriterBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override int NumBytes() => (int)this.pos; public override int NumBytes => (int)this.pos;
public int PutCoeffs(int ctx, Vp8Residual residual) public int PutCoeffs(int ctx, Vp8Residual residual)
{ {
@ -116,7 +113,7 @@ internal class Vp8BitWriter : BitWriterBase
else else
{ {
this.PutBit(v >= 9, 165); this.PutBit(v >= 9, 165);
this.PutBit(!((v & 1) != 0), 145); this.PutBit((v & 1) == 0, 145);
} }
} }
else else
@ -394,87 +391,28 @@ internal class Vp8BitWriter : BitWriterBase
} }
} }
/// <summary> /// <inheritdoc />
/// Writes the encoded image to the stream. public override void WriteEncodedImageToStream(Stream stream)
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
/// <param name="alphaData">The alpha channel data.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
public void WriteEncodedImageToStream(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile,
IccProfile? iccProfile,
uint width,
uint height,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
{ {
bool isVp8X = false; uint numBytes = (uint)this.NumBytes;
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;
}
this.Finish();
uint numBytes = (uint)this.NumBytes();
int mbSize = this.enc.Mbw * this.enc.Mbh; int mbSize = this.enc.Mbw * this.enc.Mbh;
int expectedSize = (int)((uint)mbSize * 7 / 8); 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. // Partition #0 with header and partition sizes.
uint size0 = this.GeneratePartition0(bitWriterPartZero); uint size0 = bitWriterPartZero.GeneratePartition0();
uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0;
vp8Size += numBytes; vp8Size += numBytes;
uint pad = vp8Size & 1; uint pad = vp8Size & 1;
vp8Size += pad; vp8Size += pad;
// Compute RIFF size. // Emit header and partition #0
// At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. this.WriteVp8Header(stream, vp8Size);
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + 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); bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream. // Write the encoded image to the stream.
@ -483,59 +421,49 @@ internal class Vp8BitWriter : BitWriterBase
{ {
stream.WriteByte(0); 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 this.PutBitUniform(0); // colorspace
bitWriter.PutBitUniform(0); // clamp type this.PutBitUniform(0); // clamp type
this.WriteSegmentHeader(bitWriter); this.WriteSegmentHeader();
this.WriteFilterHeader(bitWriter); this.WriteFilterHeader();
bitWriter.PutBits(0, 2); this.PutBits(0, 2);
this.WriteQuant(bitWriter); this.WriteQuant();
bitWriter.PutBitUniform(0); this.PutBitUniform(0);
this.WriteProbas(bitWriter); this.WriteProbas();
this.CodeIntraModes(bitWriter); 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; Vp8EncSegmentHeader hdr = this.enc.SegmentHeader;
Vp8EncProba proba = this.enc.Proba; 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. // We always 'update' the quant and filter strength values.
int updateData = 1; int updateData = 1;
bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); this.PutBitUniform(hdr.UpdateMap ? 1 : 0);
if (bitWriter.PutBitUniform(updateData) != 0) if (this.PutBitUniform(updateData) != 0)
{ {
// We always use absolute values, not relative ones. // 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) 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) 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) 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; Vp8FilterHeader hdr = this.enc.FilterHeader;
bool useLfDelta = hdr.I4x4LfDelta != 0; bool useLfDelta = hdr.I4x4LfDelta != 0;
bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); this.PutBitUniform(hdr.Simple ? 1 : 0);
bitWriter.PutBits((uint)hdr.FilterLevel, 6); this.PutBits((uint)hdr.FilterLevel, 6);
bitWriter.PutBits((uint)hdr.Sharpness, 3); this.PutBits((uint)hdr.Sharpness, 3);
if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0)
{ {
// '0' is the default value for i4x4LfDelta at frame #0. // '0' is the default value for i4x4LfDelta at frame #0.
bool needUpdate = hdr.I4x4LfDelta != 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. // we don't use refLfDelta => emit four 0 bits.
bitWriter.PutBits(0, 4); this.PutBits(0, 4);
// we use modeLfDelta for i4x4 // we use modeLfDelta for i4x4
bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); this.PutSignedBits(hdr.I4x4LfDelta, 6);
bitWriter.PutBits(0, 3); // all others unused. this.PutBits(0, 3); // all others unused.
} }
} }
} }
// Nominal quantization parameters // Nominal quantization parameters
private void WriteQuant(Vp8BitWriter bitWriter) private void WriteQuant()
{ {
bitWriter.PutBits((uint)this.enc.BaseQuant, 7); this.PutBits((uint)this.enc.BaseQuant, 7);
bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); this.PutSignedBits(this.enc.DqY1Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); this.PutSignedBits(this.enc.DqY2Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); this.PutSignedBits(this.enc.DqY2Ac, 4);
bitWriter.PutSignedBits(this.enc.DqUvDc, 4); this.PutSignedBits(this.enc.DqUvDc, 4);
bitWriter.PutSignedBits(this.enc.DqUvAc, 4); this.PutSignedBits(this.enc.DqUvAc, 4);
} }
private void WriteProbas(Vp8BitWriter bitWriter) private void WriteProbas()
{ {
Vp8EncProba probas = this.enc.Proba; Vp8EncProba probas = this.enc.Proba;
for (int t = 0; t < WebpConstants.NumTypes; ++t) 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]; byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p];
bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, 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) // 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; int predsWidth = this.enc.PredsWidth;
do do
@ -627,18 +555,18 @@ internal class Vp8BitWriter : BitWriterBase
Span<byte> preds = it.Preds.AsSpan(predIdx); Span<byte> preds = it.Preds.AsSpan(predIdx);
if (this.enc.SegmentHeader.UpdateMap) 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) 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 // i16x16
bitWriter.PutI16Mode(preds[0]); this.PutI16Mode(preds[0]);
} }
else else
{ {
@ -649,7 +577,7 @@ internal class Vp8BitWriter : BitWriterBase
for (int x = 0; x < 4; x++) for (int x = 0; x < 4; x++)
{ {
byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; 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); topPred = it.Preds.AsSpan(predIdx);
@ -657,56 +585,18 @@ internal class Vp8BitWriter : BitWriterBase
} }
} }
bitWriter.PutUvMode(mb.UvMode); this.PutUvMode(mb.UvMode);
} }
while (it.Next()); 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<byte> 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) private void WriteVp8Header(Stream stream, uint size)
{ {
Span<byte> vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize]; Span<byte> buf = stackalloc byte[WebpConstants.TagSize];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8);
WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size); BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
stream.Write(vp8ChunkHeader);
} }
private void WriteFrameHeader(Stream stream, uint size0) private void WriteFrameHeader(Stream stream, uint size0)

91
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -3,9 +3,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Webp.Lossless; 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; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -59,6 +56,9 @@ internal class Vp8LBitWriter : BitWriterBase
this.cur = cur; this.cur = cur;
} }
/// <inheritdoc/>
public override int NumBytes => this.cur + ((this.used + 7) >> 3);
/// <summary> /// <summary>
/// This function writes bits into bytes in increasing addresses (little endian), /// 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. /// 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); this.PutBits((uint)((bits << depth) | symbol), depth + nBits);
} }
/// <inheritdoc/>
public override int NumBytes() => this.cur + ((this.used + 7) >> 3);
public Vp8LBitWriter Clone() public Vp8LBitWriter Clone()
{ {
byte[] clonedBuffer = new byte[this.Buffer.Length]; byte[] clonedBuffer = new byte[this.Buffer.Length];
@ -122,76 +119,20 @@ internal class Vp8LBitWriter : BitWriterBase
this.used = 0; this.used = 0;
} }
/// <summary> /// <inheritdoc />
/// Writes the encoded image to the stream. public override void WriteEncodedImageToStream(Stream stream)
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="iccProfile">The color profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha)
{ {
bool isVp8X = false; uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature
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 pad = size & 1; 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. // Write magic bytes indicating its a lossless webp.
stream.Write(WebpConstants.Vp8LMagicBytes); Span<byte> scratchBuffer = stackalloc byte[WebpConstants.TagSize];
BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L);
stream.Write(scratchBuffer);
// Write Vp8 Header. // Write Vp8 Header.
Span<byte> scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size);
stream.Write(scratchBuffer.Slice(0, 4)); stream.Write(scratchBuffer);
stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte);
// Write the encoded bytes of the image to the stream. // Write the encoded bytes of the image to the stream.
@ -200,16 +141,6 @@ internal class Vp8LBitWriter : BitWriterBase
{ {
stream.WriteByte(0); stream.WriteByte(0);
} }
if (exifProfile != null)
{
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
} }
/// <summary> /// <summary>
@ -226,7 +157,7 @@ internal class Vp8LBitWriter : BitWriterBase
Span<byte> scratchBuffer = stackalloc byte[8]; Span<byte> scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); 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.cur += WriterBytes;
this.bits >>= WriterBits; this.bits >>= WriterBits;

37
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;
}
/// <summary>
/// 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.
/// </summary>
public uint Background { get; }
/// <summary>
/// Gets number of times to loop the animation. If it is 0, this means infinitely.
/// </summary>
public ushort LoopCount { get; }
public void WriteTo(Stream stream)
{
Span<byte> buffer = stackalloc byte[6];
BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount);
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer);
}
}

140
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
{
/// <summary>
/// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags.
/// </summary>
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)
{
}
/// <summary>
/// Gets the animation chunk size.
/// </summary>
public uint DataSize { get; }
/// <summary>
/// Gets the X coordinate of the upper left corner of the frame is Frame X * 2.
/// </summary>
public uint X { get; }
/// <summary>
/// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2.
/// </summary>
public uint Y { get; }
/// <summary>
/// Gets the width of the frame.
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the height of the frame.
/// </summary>
public uint Height { get; }
/// <summary>
/// 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.
/// </summary>
public uint Duration { get; }
/// <summary>
/// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public WebpBlendingMethod BlendingMethod { get; }
/// <summary>
/// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public WebpDisposalMethod DisposalMethod { get; }
public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height);
/// <summary>
/// Writes the animation frame(<see cref="WebpChunkType.FrameData"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
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;
}
/// <summary>
/// Reads the animation frame header.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns>
public static WebpFrameData Parse(Stream stream)
{
Span<byte> 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;
}
}

113
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;
}
/// <summary>
/// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation.
/// </summary>
public bool HasAnimation { get; }
/// <summary>
/// Gets a value indicating whether the file contains XMP metadata.
/// </summary>
public bool HasXmp { get; }
/// <summary>
/// Gets a value indicating whether the file contains Exif metadata.
/// </summary>
public bool HasExif { get; }
/// <summary>
/// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha").
/// </summary>
public bool HasAlpha { get; }
/// <summary>
/// Gets a value indicating whether the file contains an 'ICCP' Chunk.
/// </summary>
public bool HasIcc { get; }
/// <summary>
/// Gets width of the canvas in pixels. (uint24)
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets height of the canvas in pixels. (uint24)
/// </summary>
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);
}
}

2
src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs

@ -779,7 +779,7 @@ internal static class BackwardReferenceEncoder
private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cacheBits, Vp8LBackwardRefs refs) private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cacheBits, Vp8LBackwardRefs refs)
{ {
int pixelIndex = 0; int pixelIndex = 0;
ColorCache colorCache = new(cacheBits); ColorCache colorCache = new ColorCache(cacheBits);
for (int idx = 0; idx < refs.Refs.Count; idx++) for (int idx = 0; idx < refs.Refs.Count; idx++)
{ {
PixOrCopy v = refs.Refs[idx]; PixOrCopy v = refs.Refs[idx];

2
src/ImageSharp/Formats/Webp/Lossless/CostManager.cs

@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable
private const int FreeIntervalsStartCount = 25; private const int FreeIntervalsStartCount = 25;
private readonly Stack<CostInterval> freeIntervals = new(FreeIntervalsStartCount); private readonly Stack<CostInterval> freeIntervals = new Stack<CostInterval>(FreeIntervalsStartCount);
public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner<ushort> distArray, int pixCount, CostModel costModel) public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner<ushort> distArray, int pixCount, CostModel costModel)
{ {

7
src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs

@ -15,7 +15,7 @@ internal sealed class PixOrCopy
public uint BgraOrDistance { get; set; } public uint BgraOrDistance { get; set; }
public static PixOrCopy CreateCacheIdx(int idx) => public static PixOrCopy CreateCacheIdx(int idx) =>
new() new PixOrCopy
{ {
Mode = PixOrCopyMode.CacheIdx, Mode = PixOrCopyMode.CacheIdx,
BgraOrDistance = (uint)idx, BgraOrDistance = (uint)idx,
@ -23,14 +23,15 @@ internal sealed class PixOrCopy
}; };
public static PixOrCopy CreateLiteral(uint bgra) => public static PixOrCopy CreateLiteral(uint bgra) =>
new() new PixOrCopy
{ {
Mode = PixOrCopyMode.Literal, Mode = PixOrCopyMode.Literal,
BgraOrDistance = bgra, BgraOrDistance = bgra,
Len = 1 Len = 1
}; };
public static PixOrCopy CreateCopy(uint distance, ushort len) => new() public static PixOrCopy CreateCopy(uint distance, ushort len) =>
new PixOrCopy
{ {
Mode = PixOrCopyMode.Copy, Mode = PixOrCopyMode.Copy,
BgraOrDistance = distance, BgraOrDistance = distance,

179
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -6,7 +6,9 @@ using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -235,26 +237,60 @@ internal class Vp8LEncoder : IDisposable
/// </summary> /// </summary>
public Vp8LHashChain HashChain { get; } public Vp8LHashChain HashChain { get; }
/// <summary> public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
/// Encodes the image as lossless webp to the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; // Write bytes from the bitwriter buffer to the stream.
int height = image.Height;
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles(); metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; 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<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
/// Encodes the image as lossless webp to the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public void Encode<TPixel>(ImageFrame<TPixel> frame, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = frame.Width;
int height = frame.Height;
// Convert image pixels to bgra array. // Convert image pixels to bgra array.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height);
// Write the image size. // Write the image size.
this.WriteImageSize(width, height); this.WriteImageSize(width, height);
@ -263,35 +299,60 @@ internal class Vp8LEncoder : IDisposable
this.WriteAlphaAndVersion(hasAlpha); this.WriteAlphaAndVersion(hasAlpha);
// Encode the main image stream. // 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. // 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);
}
} }
/// <summary> /// <summary>
/// Encodes the alpha image data using the webp lossless compression. /// Encodes the alpha image data using the webp lossless compression.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param> /// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param> /// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param>
/// <returns>The size of the compressed data in bytes. /// <returns>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. /// 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.
/// </returns> /// </returns>
public int EncodeAlphaImageData<TPixel>(Image<TPixel> image, IMemoryOwner<byte> alphaData) public int EncodeAlphaImageData<TPixel>(ImageFrame<TPixel> frame, IMemoryOwner<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
int pixelCount = width * height; int pixelCount = width * height;
// Convert image pixels to bgra array. // 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. // 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(); this.bitWriter.Finish();
int size = this.bitWriter.NumBytes(); int size = this.bitWriter.NumBytes;
if (size >= pixelCount) if (size >= pixelCount)
{ {
// Compressing would not yield in smaller data -> leave the data uncompressed. // 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. /// Encodes the image stream using lossless webp format.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="image">The image to encode.</param> /// <param name="frame">The frame to encode.</param>
private void EncodeStream<TPixel>(Image<TPixel> image) private void EncodeStream<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
Span<uint> bgra = this.Bgra.GetSpan(); Span<uint> bgra = this.Bgra.GetSpan();
Span<uint> encodedData = this.EncodedData.GetSpan(); Span<uint> encodedData = this.EncodedData.GetSpan();
@ -425,9 +486,9 @@ internal class Vp8LEncoder : IDisposable
lowEffort); lowEffort);
// If we are better than what we already have. // 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); BitWriterSwap(ref this.bitWriter, ref bitWriterBest);
} }
@ -447,14 +508,14 @@ internal class Vp8LEncoder : IDisposable
/// Converts the pixels of the image to bgra. /// Converts the pixels of the image to bgra.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixels.</typeparam> /// <typeparam name="TPixel">The type of the pixels.</typeparam>
/// <param name="image">The image to convert.</param> /// <param name="frame">The frame to convert.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <returns>true, if the image is non opaque.</returns> /// <returns>true, if the image is non opaque.</returns>
private bool ConvertPixelsToBgra<TPixel>(Image<TPixel> image, int width, int height) private bool ConvertPixelsToBgra<TPixel>(ImageFrame<TPixel> frame, int width, int height)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
bool nonOpaque = false; bool nonOpaque = false;
Span<uint> bgra = this.Bgra.GetSpan(); Span<uint> bgra = this.Bgra.GetSpan();
Span<byte> bgraBytes = MemoryMarshal.Cast<uint, byte>(bgra); Span<byte> bgraBytes = MemoryMarshal.Cast<uint, byte>(bgra);
@ -682,7 +743,7 @@ internal class Vp8LEncoder : IDisposable
this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes);
// Keep track of the smallest image so far. // Keep track of the smallest image so far.
if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes))
{ {
(bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest); (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
} }
@ -1154,35 +1215,41 @@ internal class Vp8LEncoder : IDisposable
entropyComp[j] = bitEntropy.BitsEntropyRefine(); entropyComp[j] = bitEntropy.BitsEntropyRefine();
} }
entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] + entropy[(int)EntropyIx.Direct] =
entropyComp[(int)HistoIx.HistoRed] + entropyComp[(int)HistoIx.HistoAlpha] +
entropyComp[(int)HistoIx.HistoGreen] + entropyComp[(int)HistoIx.HistoRed] +
entropyComp[(int)HistoIx.HistoBlue]; entropyComp[(int)HistoIx.HistoGreen] +
entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] + entropyComp[(int)HistoIx.HistoBlue];
entropyComp[(int)HistoIx.HistoRedPred] + entropy[(int)EntropyIx.Spatial] =
entropyComp[(int)HistoIx.HistoGreenPred] + entropyComp[(int)HistoIx.HistoAlphaPred] +
entropyComp[(int)HistoIx.HistoBluePred]; entropyComp[(int)HistoIx.HistoRedPred] +
entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] + entropyComp[(int)HistoIx.HistoGreenPred] +
entropyComp[(int)HistoIx.HistoRedSubGreen] + entropyComp[(int)HistoIx.HistoBluePred];
entropyComp[(int)HistoIx.HistoGreen] + entropy[(int)EntropyIx.SubGreen] =
entropyComp[(int)HistoIx.HistoBlueSubGreen]; entropyComp[(int)HistoIx.HistoAlpha] +
entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] + entropyComp[(int)HistoIx.HistoRedSubGreen] +
entropyComp[(int)HistoIx.HistoRedPredSubGreen] + entropyComp[(int)HistoIx.HistoGreen] +
entropyComp[(int)HistoIx.HistoGreenPred] + entropyComp[(int)HistoIx.HistoBlueSubGreen];
entropyComp[(int)HistoIx.HistoBluePredSubGreen]; 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]; entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette];
// When including transforms, there is an overhead in bits from // When including transforms, there is an overhead in bits from
// storing them. This overhead is small but matters for small images. // storing them. This overhead is small but matters for small images.
// For spatial, there are 14 transformations. // For spatial, there are 14 transformations.
entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) * entropy[(int)EntropyIx.Spatial] +=
LosslessUtils.SubSampleSize(height, transformBits) * LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.FastLog2(14); LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(14);
// For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement.
entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) * entropy[(int)EntropyIx.SpatialSubGreen] +=
LosslessUtils.SubSampleSize(height, transformBits) * LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.FastLog2(24); LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(24);
// For palettes, add the cost of storing the palette. // For palettes, add the cost of storing the palette.
// We empirically estimate the cost of a compressed entry as 8 bits. // We empirically estimate the cost of a compressed entry as 8 bits.
@ -1844,9 +1911,9 @@ internal class Vp8LEncoder : IDisposable
/// </summary> /// </summary>
public void ClearRefs() public void ClearRefs()
{ {
for (int i = 0; i < this.Refs.Length; i++) foreach (Vp8LBackwardRefs t in this.Refs)
{ {
this.Refs[i].Refs.Clear(); t.Refs.Clear();
} }
} }
@ -1855,9 +1922,9 @@ internal class Vp8LEncoder : IDisposable
{ {
this.Bgra.Dispose(); this.Bgra.Dispose();
this.EncodedData.Dispose(); this.EncodedData.Dispose();
this.BgraScratch.Dispose(); this.BgraScratch?.Dispose();
this.Palette.Dispose(); this.Palette.Dispose();
this.TransformData.Dispose(); this.TransformData?.Dispose();
this.HashChain.Dispose(); this.HashChain.Dispose();
} }

113
src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs

@ -95,12 +95,10 @@ internal sealed class WebpLosslessDecoder
public void Decode<TPixel>(Buffer2D<TPixel> pixels, int width, int height) public void Decode<TPixel>(Buffer2D<TPixel> pixels, int width, int height)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator)) using Vp8LDecoder decoder = new(width, height, this.memoryAllocator);
{ this.DecodeImageStream(decoder, width, height, true);
this.DecodeImageStream(decoder, width, height, true); this.DecodeImageData(decoder, decoder.Pixels.Memory.Span);
this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); this.DecodePixelValues(decoder, pixels, width, height);
this.DecodePixelValues(decoder, pixels, width, height);
}
} }
public IMemoryOwner<uint> DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) public IMemoryOwner<uint> DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0)
@ -619,12 +617,9 @@ internal sealed class WebpLosslessDecoder
Vp8LTransform transform = new(transformType, xSize, ySize); Vp8LTransform transform = new(transformType, xSize, ySize);
// Each transform is allowed to be used only once. // 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) switch (transformType)
@ -744,61 +739,69 @@ internal sealed class WebpLosslessDecoder
this.bitReader.FillBitWindow(); this.bitReader.FillBitWindow();
int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]);
if (code < WebpConstants.NumLiteralCodes) switch (code)
{ {
// Literal case < WebpConstants.NumLiteralCodes:
data[pos] = (byte)code;
++pos;
++col;
if (col >= width)
{ {
col = 0; // Literal
++row; data[pos] = (byte)code;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) ++pos;
++col;
if (col >= width)
{ {
dec.ExtractPalettedAlphaRows(row); col = 0;
++row;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0)
{
dec.ExtractPalettedAlphaRows(row);
}
} }
}
} break;
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");
} }
pos += length; case < lenCodeLimit:
col += length;
while (col >= width)
{ {
col -= width; // Backward reference
++row; int lengthSym = code - WebpConstants.NumLiteralCodes;
if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) 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) pos += length;
{ col += length;
htreeGroup = GetHTreeGroupForPos(hdr, col, row); 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 default:
{ WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data");
WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); break;
} }
this.bitReader.Eos = this.bitReader.IsEndOfStream(); this.bitReader.Eos = this.bitReader.IsEndOfStream();

11
src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs

@ -50,6 +50,11 @@ internal class Vp8EncIterator
private int uvTopIdx; 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) public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh)
{ {
this.YTop = yTop; this.YTop = yTop;
@ -391,7 +396,7 @@ internal class Vp8EncIterator
this.MakeLuma16Preds(); this.MakeLuma16Preds();
for (mode = 0; mode < maxMode; mode++) 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); histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16);
int alpha = histo.GetAlpha(); int alpha = histo.GetAlpha();
if (alpha > bestAlpha) if (alpha > bestAlpha)
@ -409,7 +414,7 @@ internal class Vp8EncIterator
{ {
Span<byte> modes = stackalloc byte[16]; Span<byte> modes = stackalloc byte[16];
const int maxMode = MaxIntra4Mode; const int maxMode = MaxIntra4Mode;
Vp8Histogram totalHisto = new(); Vp8Histogram totalHisto = new Vp8Histogram();
int curHisto = 0; int curHisto = 0;
this.StartI4(); this.StartI4();
do do
@ -462,7 +467,7 @@ internal class Vp8EncIterator
this.MakeChroma8Preds(); this.MakeChroma8Preds();
for (mode = 0; mode < maxMode; ++mode) 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); histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4);
int alpha = histo.GetAlpha(); int alpha = histo.GetAlpha();
if (alpha > bestAlpha) if (alpha > bestAlpha)

153
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -4,7 +4,9 @@
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -88,7 +90,8 @@ internal class Vp8Encoder : IDisposable
private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; 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; private const int QMin = 0;
@ -165,7 +168,7 @@ internal class Vp8Encoder : IDisposable
// TODO: make partition_limit configurable? // TODO: make partition_limit configurable?
const int limit = 100; // original code: limit = 100 - config->partition_limit; const int limit = 100; // original code: limit = 100 - config->partition_limit;
this.maxI4HeaderBits = 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]; this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh];
for (int i = 0; i < this.MbInfo.Length; i++) for (int i = 0; i < this.MbInfo.Length; i++)
@ -308,27 +311,94 @@ internal class Vp8Encoder : IDisposable
/// </summary> /// </summary>
private int MbHeaderLimit { get; } private int MbHeaderLimit { get; }
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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);
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void EncodeAnimation<TPixel>(ImageFrame<TPixel> frame, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(frame, stream, true, null);
/// <summary> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param> /// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public void EncodeStatic<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> =>
this.Encode(image.Frames.RootFrame, stream, false, image);
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
private void Encode<TPixel>(ImageFrame<TPixel> frame, Stream stream, bool hasAnimation, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = image.Width; int width = frame.Width;
int height = image.Height; int height = frame.Height;
int pixelCount = width * height; int pixelCount = width * height;
Span<byte> y = this.Y.GetSpan(); Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan(); Span<byte> u = this.U.GetSpan();
Span<byte> v = this.V.GetSpan(); Span<byte> 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 yStride = width;
int uvStride = (yStride + 1) >> 1; 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<int> alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; Span<int> alphas = stackalloc int[WebpConstants.MaxAlpha + 1];
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
int totalMb = this.Mbw * this.Mbw; int totalMb = this.Mbw * this.Mbw;
@ -375,13 +445,6 @@ internal class Vp8Encoder : IDisposable
// Store filter stats. // Store filter stats.
this.AdjustFilterStrength(); 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. // Extract and encode alpha channel data, if present.
int alphaDataSize = 0; int alphaDataSize = 0;
bool alphaCompressionSucceeded = false; bool alphaCompressionSucceeded = false;
@ -393,7 +456,7 @@ internal class Vp8Encoder : IDisposable
{ {
// TODO: This can potentially run in an separate task. // TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha( encodedAlphaData = AlphaEncoder.EncodeAlpha(
image, frame,
this.configuration, this.configuration,
this.memoryAllocator, this.memoryAllocator,
this.skipMetadata, this.skipMetadata,
@ -408,16 +471,39 @@ internal class Vp8Encoder : IDisposable
} }
} }
this.bitWriter.WriteEncodedImageToStream( this.bitWriter.Finish();
stream,
exifProfile, long prevPosition = 0;
xmpProfile,
metadata.IccProfile, if (hasAnimation)
(uint)width, {
(uint)height, WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
hasAlpha,
alphaData[..alphaDataSize], // TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
this.alphaCompression && alphaCompressionSucceeded); prevPosition = new WebpFrameData(
0,
0,
(uint)frame.Width,
(uint)frame.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
.WriteHeaderTo(stream);
}
if (hasAlpha)
{
Span<byte> data = alphaData[..alphaDataSize];
bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded;
BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed);
}
this.bitWriter.WriteEncodedImageToStream(stream);
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}
} }
finally finally
{ {
@ -520,7 +606,7 @@ internal class Vp8Encoder : IDisposable
Span<byte> y = this.Y.GetSpan(); Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan(); Span<byte> u = this.U.GetSpan();
Span<byte> v = this.V.GetSpan(); Span<byte> 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 size = 0;
long sizeP0 = 0; long sizeP0 = 0;
long distortion = 0; long distortion = 0;
@ -862,10 +948,11 @@ internal class Vp8Encoder : IDisposable
this.ResetSegments(); this.ResetSegments();
} }
this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + this.SegmentHeader.Size =
(p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
(p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
(p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); (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 else
{ {
@ -1027,7 +1114,7 @@ internal class Vp8Encoder : IDisposable
it.NzToBytes(); it.NzToBytes();
int pos1 = this.bitWriter.NumBytes(); int pos1 = this.bitWriter.NumBytes;
if (i16) if (i16)
{ {
residual.Init(0, 1, this.Proba); 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 // U/V
residual.Init(0, 2, this.Proba); 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.LumaBits = pos2 - pos1;
it.UvBits = pos3 - pos2; it.UvBits = pos3 - pos2;
it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits;

187
src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs

@ -76,47 +76,48 @@ internal sealed class WebpLossyDecoder
Vp8Proba proba = new(); Vp8Proba proba = new();
Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba);
using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator)) using Vp8Decoder decoder = new(
{ info.Vp8FrameHeader,
Vp8Io io = InitializeVp8Io(decoder, pictureHeader); pictureHeader,
vp8SegmentHeader,
proba,
this.memoryAllocator);
Vp8Io io = InitializeVp8Io(decoder, pictureHeader);
// Paragraph 9.4: Parse the filter specs. // Paragraph 9.4: Parse the filter specs.
this.ParseFilterHeader(decoder); this.ParseFilterHeader(decoder);
decoder.PrecomputeFilterStrengths(); decoder.PrecomputeFilterStrengths();
// Paragraph 9.5: Parse partitions. // Paragraph 9.5: Parse partitions.
this.ParsePartitions(decoder); this.ParsePartitions(decoder);
// Paragraph 9.6: Dequantization Indices. // Paragraph 9.6: Dequantization Indices.
this.ParseDequantizationIndices(decoder); this.ParseDequantizationIndices(decoder);
// Ignore the value of update probabilities. // Ignore the value of update probabilities.
this.bitReader.ReadBool(); this.bitReader.ReadBool();
// Paragraph 13.4: Parse probabilities. // Paragraph 13.4: Parse probabilities.
this.ParseProbabilities(decoder); this.ParseProbabilities(decoder);
// Decode image data. // Decode image data.
this.ParseFrame(decoder, io); this.ParseFrame(decoder, io);
if (info.Features?.Alpha == true) if (info.Features?.Alpha == true)
{ {
using (AlphaDecoder alphaDecoder = new( using AlphaDecoder alphaDecoder = new(
width, width,
height, height,
alphaData, alphaData,
info.Features.AlphaChunkHeader, info.Features.AlphaChunkHeader,
this.memoryAllocator, this.memoryAllocator,
this.configuration)) this.configuration);
{ alphaDecoder.Decode();
alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha);
DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); }
} else
} {
else this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
{
this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels);
}
} }
} }
@ -194,8 +195,8 @@ internal sealed class WebpLossyDecoder
{ {
// Hardcoded tree parsing. // Hardcoded tree parsing.
block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0 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[1])
: (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2);
} }
else else
{ {
@ -590,57 +591,65 @@ internal sealed class WebpLossyDecoder
return; return;
} }
if (dec.Filter == LoopFilter.Simple) switch (dec.Filter)
{ {
int offset = dec.CacheYOffset + (mbx * 16); case LoopFilter.Simple:
if (mbx > 0)
{ {
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) if (filterInfo.UseInnerFiltering)
{ {
LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit);
} }
if (mby > 0) if (mby > 0)
{ {
LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4);
} }
if (filterInfo.UseInnerFiltering) if (filterInfo.UseInnerFiltering)
{ {
LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); 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) break;
{
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) case LoopFilter.Complex:
{ {
LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); int uvBps = dec.CacheUvStride;
LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); 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) if (filterInfo.UseInnerFiltering)
{ {
LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh);
LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, 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) private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz)
{ {
nzCoeffs <<= 2; nzCoeffs <<= 2;
if (nz > 3) nzCoeffs |= nz switch
{ {
nzCoeffs |= 3; > 3 => 3,
} > 1 => 2,
else if (nz > 1) _ => (uint)dcNz
{ };
nzCoeffs |= 2;
}
else
{
nzCoeffs |= (uint)dcNz;
}
return nzCoeffs; return nzCoeffs;
} }
@ -1353,13 +1356,13 @@ internal sealed class WebpLossyDecoder
if (mbx == 0) if (mbx == 0)
{ {
return mby == 0 return mby == 0
? 6 // B_DC_PRED_NOTOPLEFT ? 6 // B_DC_PRED_NOTOPLEFT
: 5; // B_DC_PRED_NOLEFT : 5; // B_DC_PRED_NOLEFT
} }
return mby == 0 return mby == 0
? 4 // B_DC_PRED_NOTOP ? 4 // B_DC_PRED_NOTOP
: 0; // B_DC_PRED : 0; // B_DC_PRED
} }
return mode; return mode;

6
src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs

@ -262,17 +262,17 @@ internal static class YuvConversion
/// Converts the RGB values of the image to YUV. /// Converts the RGB values of the image to YUV.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam> /// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="image">The image to convert.</param> /// <param name="frame">The frame to convert.</param>
/// <param name="configuration">The global configuration.</param> /// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory allocator.</param> /// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="y">Span to store the luma component of the image.</param> /// <param name="y">Span to store the luma component of the image.</param>
/// <param name="u">Span to store the u component of the image.</param> /// <param name="u">Span to store the u component of the image.</param>
/// <param name="v">Span to store the v component of the image.</param> /// <param name="v">Span to store the v component of the image.</param>
/// <returns>true, if the image contains alpha data.</returns> /// <returns>true, if the image contains alpha data.</returns>
public static bool ConvertRgbToYuv<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v) public static bool ConvertRgbToYuv<TPixel>(ImageFrame<TPixel> frame, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int width = imageBuffer.Width; int width = imageBuffer.Width;
int height = imageBuffer.Height; int height = imageBuffer.Height;
int uvWidth = (width + 1) >> 1; int uvWidth = (width + 1) >> 1;

158
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
@ -100,7 +100,7 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= 4; remainingBytes -= 4;
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Animation: case WebpChunkType.FrameData:
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? new Color(new Bgra32(0, 0, 0, 0)) ? new Color(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value; : features.AnimationBackgroundColor!.Value;
@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor) private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
AnimationFrameData frameData = this.ReadFrameHeader(stream); WebpFrameData frameData = WebpFrameData.Parse(stream);
long streamStartPosition = stream.Position; long streamStartPosition = stream.Position;
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
@ -162,6 +162,11 @@ internal class WebpAnimationDecoder : IDisposable
features.AlphaChunkHeader = alphaChunkHeader; features.AlphaChunkHeader = alphaChunkHeader;
break; break;
case WebpChunkType.Vp8L: case WebpChunkType.Vp8L:
if (hasAlpha)
{
WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images.");
}
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
break; break;
default: default:
@ -175,7 +180,7 @@ internal class WebpAnimationDecoder : IDisposable
{ {
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata); image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), this.metadata);
SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData);
imageFrame = image.Frames.RootFrame; 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. 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; imageFrame = currentFrame;
} }
int frameX = (int)(frameData.X * 2); Rectangle regionRectangle = frameData.Bounds;
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);
if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose) if (frameData.DisposalMethod is WebpDisposalMethod.Dispose)
{ {
this.RestoreToBackground(imageFrame, backgroundColor); this.RestoreToBackground(imageFrame, backgroundColor);
} }
using Buffer2D<TPixel> decodedImage = this.DecodeImageData<TPixel>(frameData, webpInfo); using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending;
{ DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight);
}
previousFrame = currentFrame ?? image.Frames.RootFrame; previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle; this.restoreArea = regionRectangle;
@ -217,12 +215,13 @@ internal class WebpAnimationDecoder : IDisposable
/// Sets the frames metadata. /// Sets the frames metadata.
/// </summary> /// </summary>
/// <param name="meta">The metadata.</param> /// <param name="meta">The metadata.</param>
/// <param name="duration">The frame duration.</param> /// <param name="frameData">The frame data.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData)
private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration)
{ {
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDuration = duration; frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod;
} }
/// <summary> /// <summary>
@ -239,7 +238,7 @@ internal class WebpAnimationDecoder : IDisposable
byte alphaChunkHeader = (byte)stream.ReadByte(); byte alphaChunkHeader = (byte)stream.ReadByte();
Span<byte> alphaData = this.alphaData.GetSpan(); Span<byte> alphaData = this.alphaData.GetSpan();
stream.Read(alphaData, 0, alphaDataSize); _ = stream.Read(alphaData, 0, alphaDataSize);
return alphaChunkHeader; return alphaChunkHeader;
} }
@ -251,22 +250,24 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="frameData">The frame data.</param> /// <param name="frameData">The frame data.</param>
/// <param name="webpInfo">The webp information.</param> /// <param name="webpInfo">The webp information.</param>
/// <returns>A decoded image.</returns> /// <returns>A decoded image.</returns>
private Buffer2D<TPixel> DecodeImageData<TPixel>(AnimationFrameData frameData, WebpImageInfo webpInfo) private Buffer2D<TPixel> DecodeImageFrameData<TPixel>(WebpFrameData frameData, WebpImageInfo webpInfo)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel> decodedImage = new((int)frameData.Width, (int)frameData.Height); ImageFrame<TPixel> decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height);
try try
{ {
Buffer2D<TPixel> pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> pixelBufferDecoded = decodedFrame.PixelBuffer;
if (webpInfo.IsLossless) 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); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
} }
else 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); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
} }
@ -274,7 +275,7 @@ internal class WebpAnimationDecoder : IDisposable
} }
catch catch
{ {
decodedImage?.Dispose(); decodedFrame?.Dispose();
throw; throw;
} }
finally finally
@ -287,48 +288,43 @@ internal class WebpAnimationDecoder : IDisposable
/// Draws the decoded image on canvas. The decoded image can be smaller the canvas. /// Draws the decoded image on canvas. The decoded image can be smaller the canvas.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="decodedImage">The decoded image.</param> /// <param name="decodedImageFrame">The decoded image.</param>
/// <param name="imageFrame">The image frame to draw into.</param> /// <param name="imageFrame">The image frame to draw into.</param>
/// <param name="frameX">The frame x coordinate.</param> /// <param name="restoreArea">The area of the frame.</param>
/// <param name="frameY">The frame y coordinate.</param> /// <param name="blend">Whether to blend the decoded frame data onto the target frame.</param>
/// <param name="frameWidth">The width of the frame.</param> private static void DrawDecodedImageFrameOnCanvas<TPixel>(
/// <param name="frameHeight">The height of the frame.</param> Buffer2D<TPixel> decodedImageFrame,
private static void DrawDecodedImageOnCanvas<TPixel>(Buffer2D<TPixel> decodedImage, ImageFrame<TPixel> imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) ImageFrame<TPixel> imageFrame,
Rectangle restoreArea,
bool blend)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> imageFramePixels = imageFrame.PixelBuffer; // Trim the destination frame to match the restore area. The source frame is already trimmed.
int decodedRowIdx = 0; Buffer2DRegion<TPixel> imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea);
for (int y = frameY; y < frameY + frameHeight; y++) if (blend)
{ {
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y); // The destination frame has already been prepopulated with the pixel data from the previous frame
Span<TPixel> decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth]; // so blending will leave the desired result which takes into consideration restoration to the
decodedPixelRow.TryCopyTo(framePixelRow[frameX..]); // background color within the restore area.
PixelBlender<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
}
return;
} }
}
/// <summary> for (int y = 0; y < restoreArea.Height; y++)
/// 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.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="src">The source image.</param>
/// <param name="dst">The destination image.</param>
/// <param name="frameX">The frame x coordinate.</param>
/// <param name="frameY">The frame y coordinate.</param>
/// <param name="frameWidth">The width of the frame.</param>
/// <param name="frameHeight">The height of the frame.</param>
private void AlphaBlend<TPixel>(ImageFrame<TPixel> src, ImageFrame<TPixel> dst, int frameX, int frameY, int frameWidth, int frameHeight)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> srcPixels = src.PixelBuffer;
Buffer2D<TPixel> dstPixels = dst.PixelBuffer;
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
for (int y = frameY; y < frameY + frameHeight; y++)
{ {
Span<TPixel> srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
decodedPixelRow.CopyTo(framePixelRow);
blender.Blend<TPixel>(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f);
} }
} }
@ -353,42 +349,6 @@ internal class WebpAnimationDecoder : IDisposable
pixelRegion.Fill(backgroundPixel); pixelRegion.Fill(backgroundPixel);
} }
/// <summary>
/// Reads the animation frame header.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns>
private AnimationFrameData ReadFrameHeader(BufferedReadStream stream)
{
Span<byte> 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;
}
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() => this.alphaData?.Dispose(); public void Dispose() => this.alphaData?.Dispose();
} }

2
src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs → src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary> /// </summary>
internal enum AnimationBlendingMethod public enum WebpBlendingMethod
{ {
/// <summary> /// <summary>
/// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending.

63
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Drawing;
using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
@ -77,7 +78,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); 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"); WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
} }
@ -91,7 +92,7 @@ internal static class WebpChunkParsingUtils
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
uint width = tmp & 0x3fff; uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6); sbyte xScale = (sbyte)(tmp >> 6);
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)); tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]);
uint height = tmp & 0x3fff; uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6); sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7; remaining -= 7;
@ -105,23 +106,16 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowImageFormatException("bad partition length"); WebpThrowHelper.ThrowImageFormatException("bad partition length");
} }
var vp8FrameHeader = new Vp8FrameHeader() Vp8FrameHeader vp8FrameHeader = new()
{ {
KeyFrame = true, KeyFrame = true,
Profile = (sbyte)version, Profile = (sbyte)version,
PartitionLength = partitionLength PartitionLength = partitionLength
}; };
var bitReader = new Vp8BitReader( Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
stream,
remaining,
memoryAllocator,
partitionLength)
{
Remaining = remaining
};
return new WebpImageInfo() return new WebpImageInfo
{ {
Width = width, Width = width,
Height = height, Height = height,
@ -145,7 +139,7 @@ internal static class WebpChunkParsingUtils
// VP8 data size. // VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer); uint imageDataSize = ReadChunkSize(stream, buffer);
var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f. // One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8); uint signature = bitReader.ReadValue(8);
@ -174,7 +168,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
} }
return new WebpImageInfo() return new WebpImageInfo
{ {
Width = width, Width = width,
Height = height, Height = height,
@ -231,13 +225,13 @@ internal static class WebpChunkParsingUtils
} }
// 3 bytes for the width. // 3 bytes for the width.
uint width = ReadUnsignedInt24Bit(stream, buffer) + 1; uint width = ReadUInt24LittleEndian(stream, buffer) + 1;
// 3 bytes for the height. // 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. // Read all the chunks in the order they occur.
var info = new WebpImageInfo() WebpImageInfo info = new()
{ {
Width = width, Width = width,
Height = height, Height = height,
@ -253,7 +247,7 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param> /// <param name="buffer">The buffer to store the read data into.</param>
/// <returns>A unsigned 24 bit integer.</returns> /// <returns>A unsigned 24 bit integer.</returns>
public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span<byte> buffer) public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer)
{ {
if (stream.Read(buffer, 0, 3) == 3) if (stream.Read(buffer, 0, 3) == 3)
{ {
@ -261,7 +255,28 @@ internal static class WebpChunkParsingUtils
return BinaryPrimitives.ReadUInt32LittleEndian(buffer); 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.");
}
/// <summary>
/// Writes a unsigned 24 bit integer.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="data">The uint24 data to write.</param>
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]);
} }
/// <summary> /// <summary>
@ -271,14 +286,14 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read the data from.</param> /// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param> /// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
public static uint ReadChunkSize(BufferedReadStream stream, Span<byte> buffer) public static uint ReadChunkSize(Stream stream, Span<byte> 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); 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."); throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
@ -298,7 +313,7 @@ internal static class WebpChunkParsingUtils
if (stream.Read(buffer) == 4) if (stream.Read(buffer) == 4)
{ {
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType; return chunkType;
} }

11
src/ImageSharp/Formats/Webp/WebpChunkType.cs

@ -12,45 +12,54 @@ internal enum WebpChunkType : uint
/// <summary> /// <summary>
/// Header signaling the use of the VP8 format. /// Header signaling the use of the VP8 format.
/// </summary> /// </summary>
/// <remarks>VP8 (Single)</remarks>
Vp8 = 0x56503820U, Vp8 = 0x56503820U,
/// <summary> /// <summary>
/// Header signaling the image uses lossless encoding. /// Header signaling the image uses lossless encoding.
/// </summary> /// </summary>
/// <remarks>VP8L (Single)</remarks>
Vp8L = 0x5650384CU, Vp8L = 0x5650384CU,
/// <summary> /// <summary>
/// Header for a extended-VP8 chunk. /// Header for a extended-VP8 chunk.
/// </summary> /// </summary>
/// <remarks>VP8X (Single)</remarks>
Vp8X = 0x56503858U, Vp8X = 0x56503858U,
/// <summary> /// <summary>
/// Chunk contains information about the alpha channel. /// Chunk contains information about the alpha channel.
/// </summary> /// </summary>
/// <remarks>ALPH (Single)</remarks>
Alpha = 0x414C5048U, Alpha = 0x414C5048U,
/// <summary> /// <summary>
/// Chunk which contains a color profile. /// Chunk which contains a color profile.
/// </summary> /// </summary>
/// <remarks>ICCP (Single)</remarks>
Iccp = 0x49434350U, Iccp = 0x49434350U,
/// <summary> /// <summary>
/// Chunk which contains EXIF metadata about the image. /// Chunk which contains EXIF metadata about the image.
/// </summary> /// </summary>
/// <remarks>EXIF (Single)</remarks>
Exif = 0x45584946U, Exif = 0x45584946U,
/// <summary> /// <summary>
/// Chunk contains XMP metadata about the image. /// Chunk contains XMP metadata about the image.
/// </summary> /// </summary>
/// <remarks>XMP (Single)</remarks>
Xmp = 0x584D5020U, Xmp = 0x584D5020U,
/// <summary> /// <summary>
/// For an animated image, this chunk contains the global parameters of the animation. /// For an animated image, this chunk contains the global parameters of the animation.
/// </summary> /// </summary>
/// <remarks>ANIM (Single)</remarks>
AnimationParameter = 0x414E494D, AnimationParameter = 0x414E494D,
/// <summary> /// <summary>
/// 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. /// 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.
/// </summary> /// </summary>
Animation = 0x414E4D46, /// <remarks>ANMF (Multiple)</remarks>
FrameData = 0x414E4D46,
} }

43
src/ImageSharp/Formats/Webp/WebpConstants.cs

@ -33,39 +33,6 @@ internal static class WebpConstants
/// </summary> /// </summary>
public const byte Vp8LHeaderMagicByte = 0x2F; public const byte Vp8LHeaderMagicByte = 0x2F;
/// <summary>
/// Signature bytes identifying a lossy image.
/// </summary>
public static readonly byte[] Vp8MagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x20 // ' '
};
/// <summary>
/// Signature bytes identifying a lossless image.
/// </summary>
public static readonly byte[] Vp8LMagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x4C // L
};
/// <summary>
/// Signature bytes identifying a VP8X header.
/// </summary>
public static readonly byte[] Vp8XMagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x58 // X
};
/// <summary> /// <summary>
/// The header bytes identifying RIFF file. /// The header bytes identifying RIFF file.
/// </summary> /// </summary>
@ -88,6 +55,11 @@ internal static class WebpConstants
0x50 // P 0x50 // P
}; };
/// <summary>
/// The header bytes identifying a Webp.
/// </summary>
public const string WebpFourCc = "WEBP";
/// <summary> /// <summary>
/// 3 bits reserved for version. /// 3 bits reserved for version.
/// </summary> /// </summary>
@ -103,11 +75,6 @@ internal static class WebpConstants
/// </summary> /// </summary>
public const int Vp8FrameHeaderSize = 10; public const int Vp8FrameHeaderSize = 10;
/// <summary>
/// Size of a VP8X chunk in bytes.
/// </summary>
public const int Vp8XChunkSize = 10;
/// <summary> /// <summary>
/// Size of a chunk header. /// Size of a chunk header.
/// </summary> /// </summary>

9
src/ImageSharp/Formats/Webp/WebpDecoder.cs

@ -17,7 +17,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static WebpDecoder Instance { get; } = new(); public static WebpDecoder Instance { get; } = new WebpDecoder();
/// <inheritdoc/> /// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
@ -25,7 +25,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); 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); return decoder.Identify(options.Configuration, stream, cancellationToken);
} }
@ -35,7 +35,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
using WebpDecoderCore decoder = new(options); using WebpDecoderCore decoder = new WebpDecoderCore(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken); Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken);
ScaleToTargetSize(options.GeneralOptions, image); ScaleToTargetSize(options.GeneralOptions, image);
@ -52,6 +52,5 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
=> this.Decode<Rgba32>(options, stream, cancellationToken); => this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options };
=> new() { GeneralOptions = options };
} }

29
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -8,7 +8,9 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
@ -89,25 +91,30 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{ {
if (this.webImageInfo.Features is { Animation: true }) 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<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); return animationDecoder.Decode<TPixel>(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<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); image = new Image<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless) 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); losslessDecoder.Decode(pixels, image.Width, image.Height);
} }
else 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); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
} }
@ -137,7 +144,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{ {
return new ImageInfo( return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), 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); metadata);
} }
} }
@ -332,7 +339,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return; return;
} }
metadata.ExifProfile = new(exifData); metadata.ExifProfile = new ExifProfile(exifData);
} }
} }
@ -359,7 +366,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return; return;
} }
metadata.XmpProfile = new(xmpData); metadata.XmpProfile = new XmpProfile(xmpData);
} }
} }

2
src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public sealed class WebpDecoderOptions : ISpecializedDecoderOptions public sealed class WebpDecoderOptions : ISpecializedDecoderOptions
{ {
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new(); public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
/// <summary> /// <summary>
/// Gets the flag to decide how to handle the background color Animation Chunk. /// Gets the flag to decide how to handle the background color Animation Chunk.

2
src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs → src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary> /// </summary>
internal enum AnimationDisposalMethod public enum WebpDisposalMethod
{ {
/// <summary> /// <summary>
/// Do not dispose. Leave the canvas as is. /// Do not dispose. Leave the canvas as is.

2
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
/// <summary> /// <summary>

62
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless) if (lossless)
{ {
using Vp8LEncoder enc = new( using Vp8LEncoder encoder = new(
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
@ -140,11 +140,38 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.transparentColorMode, this.transparentColorMode,
this.nearLossless, this.nearLossless,
this.nearLosslessQuality); this.nearLosslessQuality);
enc.Encode(image, stream);
bool hasAnimation = image.Frames.Count > 1;
encoder.EncodeHeader(image, stream, hasAnimation);
if (hasAnimation)
{
foreach (ImageFrame<TPixel> 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 else
{ {
using Vp8Encoder enc = new( using Vp8Encoder encoder = new(
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
@ -156,7 +183,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.filterStrength, this.filterStrength,
this.spatialNoiseShaping, this.spatialNoiseShaping,
this.alphaCompression); this.alphaCompression);
enc.Encode(image, stream); if (image.Frames.Count > 1)
{
encoder.EncodeHeader(image, stream, false, true);
foreach (ImageFrame<TPixel> 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);
} }
} }
} }

6
src/ImageSharp/Formats/Webp/WebpFormat.cs

@ -15,7 +15,7 @@ public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static WebpFormat Instance { get; } = new(); public static WebpFormat Instance { get; } = new WebpFormat();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "Webp"; public string Name => "Webp";
@ -30,8 +30,8 @@ public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
public IEnumerable<string> FileExtensions => WebpConstants.FileExtensions; public IEnumerable<string> FileExtensions => WebpConstants.FileExtensions;
/// <inheritdoc/> /// <inheritdoc/>
public WebpMetadata CreateDefaultFormatMetadata() => new(); public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
/// <inheritdoc/> /// <inheritdoc/>
public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata();
} }

19
src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs

@ -19,13 +19,28 @@ public class WebpFrameMetadata : IDeepCloneable
/// Initializes a new instance of the <see cref="WebpFrameMetadata"/> class. /// Initializes a new instance of the <see cref="WebpFrameMetadata"/> class.
/// </summary> /// </summary>
/// <param name="other">The metadata to create an instance from.</param> /// <param name="other">The metadata to create an instance from.</param>
private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; private WebpFrameMetadata(WebpFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
this.BlendMethod = other.BlendMethod;
}
/// <summary>
/// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
public WebpBlendingMethod BlendMethod { get; set; }
/// <summary>
/// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
public WebpDisposalMethod DisposalMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets the frame duration. The time to wait before displaying the next frame, /// 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. /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined.
/// </summary> /// </summary>
public uint FrameDuration { get; set; } public uint FrameDelay { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); public IDeepCloneable DeepClone() => new WebpFrameMetadata(this);

9
src/ImageSharp/Formats/Webp/WebpMetadata.cs

@ -23,6 +23,7 @@ public class WebpMetadata : IDeepCloneable
{ {
this.FileFormat = other.FileFormat; this.FileFormat = other.FileFormat;
this.AnimationLoopCount = other.AnimationLoopCount; this.AnimationLoopCount = other.AnimationLoopCount;
this.AnimationBackground = other.AnimationBackground;
} }
/// <summary> /// <summary>
@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable
/// </summary> /// </summary>
public ushort AnimationLoopCount { get; set; } = 1; public ushort AnimationLoopCount { get; set; } = 1;
/// <summary>
/// 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.
/// </summary>
public Color AnimationBackground { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new WebpMetadata(this); public IDeepCloneable DeepClone() => new WebpMetadata(this);
} }

4
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -158,8 +158,7 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) &&
Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
this.Header.Size >= minSize && this.Header.Size is >= minSize and < maxSize;
this.Header.Size < maxSize;
} }
/// <summary> /// <summary>
@ -175,7 +174,6 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
return copy; return copy;
} }
IccWriter writer = new();
return IccWriter.Write(this); return IccWriter.Write(this);
} }

14
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -308,7 +308,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.Equal(0, webpMetaData.AnimationLoopCount); Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration); Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count); Assert.Equal(12, image.Frames.Count);
} }
@ -325,7 +325,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f)); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
Assert.Equal(0, webpMetaData.AnimationLoopCount); Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration); Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count); Assert.Equal(12, image.Frames.Count);
} }
@ -357,6 +357,16 @@ public class WebpDecoderTests
image.CompareToOriginal(provider, ReferenceDecoder); image.CompareToOriginal(provider, ReferenceDecoder);
} }
[Theory]
[WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_AlphaBlending_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance);
image.DebugSaveMultiFrame(provider);
image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact);
}
[Theory] [Theory]
[WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)]
[WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)]

43
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -17,6 +17,49 @@ public class WebpEncoderTests
{ {
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Encode_AnimatedLossless<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<Rgba32> encoded = Image.Load<Rgba32>(path);
encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp");
}
[Theory] [Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]

4
tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs

@ -143,7 +143,7 @@ public class YuvConversionTests
}; };
// act // act
YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert // assert
Assert.True(expectedY.AsSpan().SequenceEqual(y)); Assert.True(expectedY.AsSpan().SequenceEqual(y));
@ -249,7 +249,7 @@ public class YuvConversionTests
}; };
// act // act
YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v);
// assert // assert
Assert.True(expectedY.AsSpan().SequenceEqual(y)); Assert.True(expectedY.AsSpan().SequenceEqual(y));

1
tests/ImageSharp.Tests/TestImages.cs

@ -681,6 +681,7 @@ public static class TestImages
public static class Lossy public static class Lossy
{ {
public const string AnimatedLandscape = "Webp/landscape.webp";
public const string Earth = "Webp/earth_lossy.webp"; public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp";
public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp";

3
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

3
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

3
tests/Images/Input/Webp/landscape.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e9f8b7ee87ecb59d8cee5e84320da7670eb5e274e1c0a7dd5f13fe3675be62a
size 26892
Loading…
Cancel
Save