Browse Source

Merge branch 'main' into js/faster-png-eof-handling

pull/2561/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
74af9f0c52
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. 60
      src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
  12. 2
      src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
  13. 16
      src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
  14. 193
      src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
  15. 18
      src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
  16. 9
      src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
  17. 6
      src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
  18. 299
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  19. 308
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
  20. 110
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs
  21. 113
      src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs
  22. 11
      src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs
  23. 153
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  24. 187
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  25. 6
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  26. 158
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  27. 2
      src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs
  28. 63
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  29. 11
      src/ImageSharp/Formats/Webp/WebpChunkType.cs
  30. 43
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  31. 9
      src/ImageSharp/Formats/Webp/WebpDecoder.cs
  32. 29
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  33. 2
      src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs
  34. 2
      src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs
  35. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  36. 62
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  37. 6
      src/ImageSharp/Formats/Webp/WebpFormat.cs
  38. 19
      src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs
  39. 9
      src/ImageSharp/Formats/Webp/WebpMetadata.cs
  40. 4
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  41. 13
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  42. 29
      tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
  43. 14
      tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs
  44. 14
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  45. 43
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  46. 4
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  47. 1
      tests/ImageSharp.Tests/TestImages.cs
  48. 3
      tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp
  49. 3
      tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp
  50. 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)
{
Vp8LBitReader bitReader = new(data);
Vp8LBitReader bitReader = new Vp8LBitReader(data);
this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration);
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.
/// </summary>
/// <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="memoryAllocator">The memory manager.</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>
/// <returns>The encoded alpha data.</returns>
public static IMemoryOwner<byte> EncodeAlpha<TPixel>(
Image<TPixel> image,
ImageFrame<TPixel> frame,
Configuration configuration,
MemoryAllocator memoryAllocator,
bool skipMetadata,
@ -35,9 +35,9 @@ internal static class AlphaEncoder
out int size)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
IMemoryOwner<byte> alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator);
int width = frame.Width;
int height = frame.Height;
IMemoryOwner<byte> alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator);
if (compress)
{
@ -58,9 +58,9 @@ internal static class AlphaEncoder
// The transparency information will be stored in the green channel of the ARGB quadruplet.
// The green channel is allowed extra transformation steps in the specification -- unlike the other channels,
// that can improve compression.
using Image<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;
}
@ -73,19 +73,19 @@ internal static class AlphaEncoder
/// Store the transparency in the green channel.
/// </summary>
/// <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>
/// <returns>The transparency image.</returns>
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData)
/// <returns>The transparency frame.</returns>
private static ImageFrame<Rgba32> DispatchAlphaToGreen<TPixel>(ImageFrame<TPixel> frame, Span<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
Image<Rgba32> alphaAsImage = new(width, height);
int width = frame.Width;
int height = frame.Height;
ImageFrame<Rgba32> alphaAsFrame = new ImageFrame<Rgba32>(Configuration.Default, width, height);
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<byte> alphaRow = alphaData.Slice(y * width, width);
for (int x = 0; x < width; x++)
@ -95,23 +95,23 @@ internal static class AlphaEncoder
}
}
return alphaAsImage;
return alphaAsFrame;
}
/// <summary>
/// Extract the alpha data of the image.
/// </summary>
/// <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="memoryAllocator">The memory manager.</param>
/// <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>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
int height = image.Height;
int width = image.Width;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int height = frame.Height;
int width = frame.Width;
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
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.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Diagnostics;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -14,18 +16,11 @@ internal abstract class BitWriterBase
private const ulong MaxCanvasPixels = 4294967295ul;
protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
/// <summary>
/// Buffer to write to.
/// </summary>
private byte[] buffer;
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly
/// <summary>
/// Initializes a new instance of the <see cref="BitWriterBase"/> class.
/// </summary>
@ -41,17 +36,23 @@ internal abstract class BitWriterBase
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>
/// Writes the encoded bytes of the image to the stream. Call Finish() before this.
/// </summary>
/// <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>
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
/// </summary>
/// <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>
/// Resizes the buffer to write to.
@ -59,12 +60,6 @@ internal abstract class BitWriterBase
/// <param name="extraSize">The extra size in bytes needed.</param>
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>
/// Flush leftover bits.
/// </summary>
@ -84,63 +79,89 @@ internal abstract class BitWriterBase
}
/// <summary>
/// Writes the RIFF header to the stream.
/// Write the trunks before data trunk.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="riffSize">The block length.</param>
protected void WriteRiffHeader(Stream stream, uint riffSize)
/// <param name="width">The width of the image.</param>
/// <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);
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
stream.Write(this.scratchBuffer.Span.Slice(0, 4));
stream.Write(WebpConstants.WebpHeader);
// Write file size later
long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
Debug.Assert(pos is 4, "Stream should be written from position 0.");
// Write VP8X, header if necessary.
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
if (isVp8X)
{
WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
if (iccProfile != null)
{
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
}
}
}
/// <summary>
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
protected static uint MetadataChunkSize(byte[] metadataBytes)
{
uint metaSize = (uint)metadataBytes.Length;
return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
}
/// <param name="stream">The stream to write to.</param>
public abstract void WriteEncodedImageToStream(Stream stream);
/// <summary>
/// Calculates the chunk size of a alpha chunk.
/// Write the trunks after data trunk.
/// </summary>
/// <param name="alphaBytes">The alpha chunk bytes.</param>
/// <returns>The alpha data chunk size in bytes.</returns>
protected static uint AlphaChunkSize(Span<byte> alphaBytes)
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
public static void WriteTrunksAfterData(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{
uint alphaSize = (uint)alphaBytes.Length + 1;
return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
if (exifProfile != null)
{
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
}
if (xmpProfile != null)
{
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}
RiffHelper.EndWriteRiffFile(stream, 4);
}
/// <summary>
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// Writes the animation parameter(<see cref="WebpChunkType.AnimationParameter"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="metadataBytes">The metadata profile's bytes.</param>
/// <param name="chunkType">The chuck type to write.</param>
protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
/// <param name="background">
/// 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.
/// </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));
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);
}
WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
chunk.WriteTo(stream);
}
/// <summary>
@ -149,53 +170,19 @@ internal abstract class BitWriterBase
/// <param name="stream">The stream to write to.</param>
/// <param name="dataBytes">The alpha channel data bytes.</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;
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);
long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha);
byte flags = 0;
if (alphaDataIsCompressed)
{
flags |= 1;
// TODO: Filtering and preprocessing
flags = 1;
}
stream.WriteByte(flags);
stream.Write(dataBytes);
// Add padding byte if needed.
if ((size & 1) == 1)
{
stream.WriteByte(0);
}
}
/// <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);
}
RiffHelper.EndWriteChunk(stream, pos);
}
/// <summary>
@ -204,65 +191,17 @@ internal abstract class BitWriterBase
/// <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="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="height">The height of the image.</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)
{
WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}");
}
// The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1.
if (width * height > MaxCanvasPixels)
{
WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1");
}
uint flags = 0;
if (exifProfile != null)
{
// Set exif bit.
flags |= 8;
}
if (xmpProfile != null)
{
// Set xmp bit.
flags |= 4;
}
WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
if (hasAlpha)
{
// Set alpha bit.
flags |= 16;
}
if (iccProfileBytes != null)
{
// Set iccp flag.
flags |= 32;
}
Span<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];
chunk.Validate(MaxDimension, MaxCanvasPixels);
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 SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -72,7 +69,7 @@ internal class Vp8BitWriter : BitWriterBase
}
/// <inheritdoc/>
public override int NumBytes() => (int)this.pos;
public override int NumBytes => (int)this.pos;
public int PutCoeffs(int ctx, Vp8Residual residual)
{
@ -116,7 +113,7 @@ internal class Vp8BitWriter : BitWriterBase
else
{
this.PutBit(v >= 9, 165);
this.PutBit(!((v & 1) != 0), 145);
this.PutBit((v & 1) == 0, 145);
}
}
else
@ -394,87 +391,28 @@ internal class Vp8BitWriter : BitWriterBase
}
}
/// <summary>
/// Writes the encoded image to the 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)
/// <inheritdoc />
public override void WriteEncodedImageToStream(Stream stream)
{
bool isVp8X = false;
byte[]? exifBytes = null;
byte[]? xmpBytes = null;
byte[]? iccProfileBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
exifBytes = exifProfile.ToByteArray();
riffSize += MetadataChunkSize(exifBytes!);
}
if (xmpProfile != null)
{
isVp8X = true;
xmpBytes = xmpProfile.Data;
riffSize += MetadataChunkSize(xmpBytes!);
}
if (iccProfile != null)
{
isVp8X = true;
iccProfileBytes = iccProfile.ToByteArray();
riffSize += MetadataChunkSize(iccProfileBytes);
}
if (hasAlpha)
{
isVp8X = true;
riffSize += AlphaChunkSize(alphaData);
}
if (isVp8X)
{
riffSize += ExtendedFileChunkSize;
}
uint numBytes = (uint)this.NumBytes;
this.Finish();
uint numBytes = (uint)this.NumBytes();
int mbSize = this.enc.Mbw * this.enc.Mbh;
int expectedSize = (int)((uint)mbSize * 7 / 8);
Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc);
Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(expectedSize, this.enc);
// Partition #0 with header and partition sizes.
uint size0 = this.GeneratePartition0(bitWriterPartZero);
uint size0 = bitWriterPartZero.GeneratePartition0();
uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0;
vp8Size += numBytes;
uint pad = vp8Size & 1;
vp8Size += pad;
// Compute RIFF size.
// At the minimum it is: "WEBPVP8 nnnn" + VP8 data size.
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit header and partition #0
this.WriteVp8Header(stream, vp8Size);
this.WriteFrameHeader(stream, size0);
// Emit headers and partition #0
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
@ -483,59 +421,49 @@ internal class Vp8BitWriter : BitWriterBase
{
stream.WriteByte(0);
}
if (exifProfile != null)
{
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}
private uint GeneratePartition0(Vp8BitWriter bitWriter)
private uint GeneratePartition0()
{
bitWriter.PutBitUniform(0); // colorspace
bitWriter.PutBitUniform(0); // clamp type
this.PutBitUniform(0); // colorspace
this.PutBitUniform(0); // clamp type
this.WriteSegmentHeader(bitWriter);
this.WriteFilterHeader(bitWriter);
this.WriteSegmentHeader();
this.WriteFilterHeader();
bitWriter.PutBits(0, 2);
this.PutBits(0, 2);
this.WriteQuant(bitWriter);
bitWriter.PutBitUniform(0);
this.WriteProbas(bitWriter);
this.CodeIntraModes(bitWriter);
this.WriteQuant();
this.PutBitUniform(0);
this.WriteProbas();
this.CodeIntraModes();
bitWriter.Finish();
this.Finish();
return (uint)bitWriter.NumBytes();
return (uint)this.NumBytes;
}
private void WriteSegmentHeader(Vp8BitWriter bitWriter)
private void WriteSegmentHeader()
{
Vp8EncSegmentHeader hdr = this.enc.SegmentHeader;
Vp8EncProba proba = this.enc.Proba;
if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0)
if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0)
{
// We always 'update' the quant and filter strength values.
int updateData = 1;
bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0);
if (bitWriter.PutBitUniform(updateData) != 0)
this.PutBitUniform(hdr.UpdateMap ? 1 : 0);
if (this.PutBitUniform(updateData) != 0)
{
// We always use absolute values, not relative ones.
bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.)
this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.)
for (int s = 0; s < WebpConstants.NumMbSegments; ++s)
{
bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7);
this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7);
}
for (int s = 0; s < WebpConstants.NumMbSegments; ++s)
{
bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6);
this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6);
}
}
@ -543,50 +471,50 @@ internal class Vp8BitWriter : BitWriterBase
{
for (int s = 0; s < 3; ++s)
{
if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0)
if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0)
{
bitWriter.PutBits(proba.Segments[s], 8);
this.PutBits(proba.Segments[s], 8);
}
}
}
}
}
private void WriteFilterHeader(Vp8BitWriter bitWriter)
private void WriteFilterHeader()
{
Vp8FilterHeader hdr = this.enc.FilterHeader;
bool useLfDelta = hdr.I4x4LfDelta != 0;
bitWriter.PutBitUniform(hdr.Simple ? 1 : 0);
bitWriter.PutBits((uint)hdr.FilterLevel, 6);
bitWriter.PutBits((uint)hdr.Sharpness, 3);
if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0)
this.PutBitUniform(hdr.Simple ? 1 : 0);
this.PutBits((uint)hdr.FilterLevel, 6);
this.PutBits((uint)hdr.Sharpness, 3);
if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0)
{
// '0' is the default value for i4x4LfDelta at frame #0.
bool needUpdate = hdr.I4x4LfDelta != 0;
if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0)
if (this.PutBitUniform(needUpdate ? 1 : 0) != 0)
{
// we don't use refLfDelta => emit four 0 bits.
bitWriter.PutBits(0, 4);
this.PutBits(0, 4);
// we use modeLfDelta for i4x4
bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6);
bitWriter.PutBits(0, 3); // all others unused.
this.PutSignedBits(hdr.I4x4LfDelta, 6);
this.PutBits(0, 3); // all others unused.
}
}
}
// Nominal quantization parameters
private void WriteQuant(Vp8BitWriter bitWriter)
private void WriteQuant()
{
bitWriter.PutBits((uint)this.enc.BaseQuant, 7);
bitWriter.PutSignedBits(this.enc.DqY1Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Ac, 4);
bitWriter.PutSignedBits(this.enc.DqUvDc, 4);
bitWriter.PutSignedBits(this.enc.DqUvAc, 4);
this.PutBits((uint)this.enc.BaseQuant, 7);
this.PutSignedBits(this.enc.DqY1Dc, 4);
this.PutSignedBits(this.enc.DqY2Dc, 4);
this.PutSignedBits(this.enc.DqY2Ac, 4);
this.PutSignedBits(this.enc.DqUvDc, 4);
this.PutSignedBits(this.enc.DqUvAc, 4);
}
private void WriteProbas(Vp8BitWriter bitWriter)
private void WriteProbas()
{
Vp8EncProba probas = this.enc.Proba;
for (int t = 0; t < WebpConstants.NumTypes; ++t)
@ -599,25 +527,25 @@ internal class Vp8BitWriter : BitWriterBase
{
byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p];
bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p];
if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p]))
if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p]))
{
bitWriter.PutBits(p0, 8);
this.PutBits(p0, 8);
}
}
}
}
}
if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0)
if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0)
{
bitWriter.PutBits(probas.SkipProba, 8);
this.PutBits(probas.SkipProba, 8);
}
}
// Writes the partition #0 modes (that is: all intra modes)
private void CodeIntraModes(Vp8BitWriter bitWriter)
private void CodeIntraModes()
{
var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh);
Vp8EncIterator it = new Vp8EncIterator(this.enc);
int predsWidth = this.enc.PredsWidth;
do
@ -627,18 +555,18 @@ internal class Vp8BitWriter : BitWriterBase
Span<byte> preds = it.Preds.AsSpan(predIdx);
if (this.enc.SegmentHeader.UpdateMap)
{
bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments);
this.PutSegment(mb.Segment, this.enc.Proba.Segments);
}
if (this.enc.Proba.UseSkipProba)
{
bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba);
this.PutBit(mb.Skip, this.enc.Proba.SkipProba);
}
if (bitWriter.PutBit(mb.MacroBlockType != 0, 145))
if (this.PutBit(mb.MacroBlockType != 0, 145))
{
// i16x16
bitWriter.PutI16Mode(preds[0]);
this.PutI16Mode(preds[0]);
}
else
{
@ -649,7 +577,7 @@ internal class Vp8BitWriter : BitWriterBase
for (int x = 0; x < 4; x++)
{
byte[] probas = WebpLookupTables.ModesProba[topPred[x], left];
left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas);
left = this.PutI4Mode(it.Preds[predIdx + x], probas);
}
topPred = it.Preds.AsSpan(predIdx);
@ -657,56 +585,18 @@ internal class Vp8BitWriter : BitWriterBase
}
}
bitWriter.PutUvMode(mb.UvMode);
this.PutUvMode(mb.UvMode);
}
while (it.Next());
}
private void WriteWebpHeaders(
Stream stream,
uint size0,
uint vp8Size,
uint riffSize,
bool isVp8X,
uint width,
uint height,
ExifProfile? exifProfile,
XmpProfile? xmpProfile,
byte[]? iccProfileBytes,
bool hasAlpha,
Span<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)
{
Span<byte> vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize];
WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader);
BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size);
stream.Write(vp8ChunkHeader);
Span<byte> buf = stackalloc byte[WebpConstants.TagSize];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
}
private void WriteFrameHeader(Stream stream, uint size0)

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

@ -3,9 +3,6 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter;
@ -59,6 +56,9 @@ internal class Vp8LBitWriter : BitWriterBase
this.cur = cur;
}
/// <inheritdoc/>
public override int NumBytes => this.cur + ((this.used + 7) >> 3);
/// <summary>
/// This function writes bits into bytes in increasing addresses (little endian),
/// and within a byte least-significant-bit first. This function can write up to 32 bits in one go.
@ -98,9 +98,6 @@ internal class Vp8LBitWriter : BitWriterBase
this.PutBits((uint)((bits << depth) | symbol), depth + nBits);
}
/// <inheritdoc/>
public override int NumBytes() => this.cur + ((this.used + 7) >> 3);
public Vp8LBitWriter Clone()
{
byte[] clonedBuffer = new byte[this.Buffer.Length];
@ -122,76 +119,20 @@ internal class Vp8LBitWriter : BitWriterBase
this.used = 0;
}
/// <summary>
/// Writes the encoded image to the 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)
/// <inheritdoc />
public override void WriteEncodedImageToStream(Stream stream)
{
bool isVp8X = false;
byte[]? exifBytes = null;
byte[]? xmpBytes = null;
byte[]? iccBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
exifBytes = exifProfile.ToByteArray();
riffSize += MetadataChunkSize(exifBytes!);
}
if (xmpProfile != null)
{
isVp8X = true;
xmpBytes = xmpProfile.Data;
riffSize += MetadataChunkSize(xmpBytes!);
}
if (iccProfile != null)
{
isVp8X = true;
iccBytes = iccProfile.ToByteArray();
riffSize += MetadataChunkSize(iccBytes);
}
if (isVp8X)
{
riffSize += ExtendedFileChunkSize;
}
this.Finish();
uint size = (uint)this.NumBytes();
size++; // One byte extra for the VP8L signature.
// Write RIFF header.
uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature
uint pad = size & 1;
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad;
this.WriteRiffHeader(stream, riffSize);
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha);
if (iccBytes != null)
{
this.WriteColorProfile(stream, iccBytes);
}
}
// Write magic bytes indicating its a lossless webp.
stream.Write(WebpConstants.Vp8LMagicBytes);
Span<byte> scratchBuffer = stackalloc byte[WebpConstants.TagSize];
BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L);
stream.Write(scratchBuffer);
// Write Vp8 Header.
Span<byte> scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size);
stream.Write(scratchBuffer.Slice(0, 4));
stream.Write(scratchBuffer);
stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte);
// Write the encoded bytes of the image to the stream.
@ -200,16 +141,6 @@ internal class Vp8LBitWriter : BitWriterBase
{
stream.WriteByte(0);
}
if (exifProfile != null)
{
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}
/// <summary>
@ -226,7 +157,7 @@ internal class Vp8LBitWriter : BitWriterBase
Span<byte> scratchBuffer = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits);
scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur));
scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur));
this.cur += WriterBytes;
this.bits >>= WriterBits;

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);
}
}

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

@ -50,8 +50,10 @@ internal static class BackwardReferenceEncoder
double bitCostBest = -1;
int cacheBitsInitial = cacheBits;
Vp8LHashChain? hashChainBox = null;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
{
int cacheBitsTmp = cacheBitsInitial;
@ -76,21 +78,19 @@ internal static class BackwardReferenceEncoder
}
// Next, try with a color cache and update the references.
cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp);
cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp);
if (cacheBitsTmp > 0)
{
BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst);
}
// Keep the best backward references.
var histo = new Vp8LHistogram(worst, cacheBitsTmp);
using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp);
double bitCost = histo.EstimateBits(stats, bitsEntropy);
if (lz77TypeBest == 0 || bitCost < bitCostBest)
{
Vp8LBackwardRefs tmp = worst;
worst = best;
best = tmp;
(best, worst) = (worst, best);
bitCostBest = bitCost;
cacheBits = cacheBitsTmp;
lz77TypeBest = lz77Type;
@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder
{
Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!;
BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst);
var histo = new Vp8LHistogram(worst, cacheBits);
using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits);
double bitCostTrace = histo.EstimateBits(stats, bitsEntropy);
if (bitCostTrace < bitCostBest)
{
@ -123,7 +123,13 @@ internal static class BackwardReferenceEncoder
/// The local color cache is also disabled for the lower (smaller then 25) quality.
/// </summary>
/// <returns>Best cache size.</returns>
private static int CalculateBestCacheSize(ReadOnlySpan<uint> bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits)
private static int CalculateBestCacheSize(
MemoryAllocator memoryAllocator,
Span<ColorCache> colorCache,
ReadOnlySpan<uint> bgra,
uint quality,
Vp8LBackwardRefs refs,
int bestCacheBits)
{
int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits;
if (cacheBitsMax == 0)
@ -134,11 +140,11 @@ internal static class BackwardReferenceEncoder
double entropyMin = MaxEntropy;
int pos = 0;
var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1];
for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++)
using Vp8LHistogramSet histos = new(memoryAllocator, colorCache.Length, 0);
for (int i = 0; i < colorCache.Length; i++)
{
histos[i] = new Vp8LHistogram(paletteCodeBits: i);
histos[i].PaletteCodeBits = i;
colorCache[i] = new ColorCache(i);
}
@ -149,10 +155,10 @@ internal static class BackwardReferenceEncoder
if (v.IsLiteral())
{
uint pix = bgra[pos++];
uint a = (pix >> 24) & 0xff;
uint r = (pix >> 16) & 0xff;
uint g = (pix >> 8) & 0xff;
uint b = (pix >> 0) & 0xff;
int a = (int)(pix >> 24) & 0xff;
int r = (int)(pix >> 16) & 0xff;
int g = (int)(pix >> 8) & 0xff;
int b = (int)(pix >> 0) & 0xff;
// The keys of the caches can be derived from the longest one.
int key = ColorCache.HashPix(pix, 32 - cacheBitsMax);
@ -218,8 +224,8 @@ internal static class BackwardReferenceEncoder
}
}
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i <= cacheBitsMax; i++)
{
double entropy = histos[i].EstimateBits(stats, bitsEntropy);
@ -266,7 +272,7 @@ internal static class BackwardReferenceEncoder
int pixCount = xSize * ySize;
bool useColorCache = cacheBits > 0;
int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0);
var costModel = new CostModel(literalArraySize);
CostModel costModel = new(memoryAllocator, literalArraySize);
int offsetPrev = -1;
int lenPrev = -1;
double offsetCost = -1;
@ -280,7 +286,7 @@ internal static class BackwardReferenceEncoder
}
costModel.Build(xSize, cacheBits, refs);
using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel);
using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel);
Span<float> costManagerCosts = costManager.Costs.GetSpan();
Span<ushort> distArray = distArrayBuffer.GetSpan();
@ -441,12 +447,12 @@ internal static class BackwardReferenceEncoder
int ix = useColorCache ? colorCache!.Contains(color) : -1;
if (ix >= 0)
{
double mul0 = 0.68;
const double mul0 = 0.68;
costVal += costModel.GetCacheCost((uint)ix) * mul0;
}
else
{
double mul1 = 0.82;
const double mul1 = 0.82;
if (useColorCache)
{
colorCache!.Insert(color);
@ -693,10 +699,8 @@ internal static class BackwardReferenceEncoder
bestLength = MaxLength;
break;
}
else
{
bestLength = currLength;
}
bestLength = currLength;
}
}
}
@ -775,7 +779,7 @@ internal static class BackwardReferenceEncoder
private static void BackwardRefsWithLocalCache(ReadOnlySpan<uint> bgra, int cacheBits, Vp8LBackwardRefs refs)
{
int pixelIndex = 0;
ColorCache colorCache = new(cacheBits);
ColorCache colorCache = new ColorCache(cacheBits);
for (int idx = 0; idx < refs.Refs.Count; idx++)
{
PixOrCopy v = refs.Refs[idx];

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

@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable
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)
{

16
src/ImageSharp/Formats/Webp/Lossless/CostModel.cs

@ -1,18 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal class CostModel
{
private readonly MemoryAllocator memoryAllocator;
private const int ValuesInBytes = 256;
/// <summary>
/// Initializes a new instance of the <see cref="CostModel"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="literalArraySize">The literal array size.</param>
public CostModel(int literalArraySize)
public CostModel(MemoryAllocator memoryAllocator, int literalArraySize)
{
this.memoryAllocator = memoryAllocator;
this.Alpha = new double[ValuesInBytes];
this.Red = new double[ValuesInBytes];
this.Blue = new double[ValuesInBytes];
@ -32,13 +37,12 @@ internal class CostModel
public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs)
{
var histogram = new Vp8LHistogram(cacheBits);
using System.Collections.Generic.List<PixOrCopy>.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator();
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits);
// The following code is similar to HistogramCreate but converts the distance to plane code.
while (refsEnumerator.MoveNext())
for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize);
histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize);
}
ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal);
@ -70,7 +74,7 @@ internal class CostModel
public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff];
private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output)
private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span<uint> populationCounts, double[] output)
{
uint sum = 0;
int nonzeros = 0;

193
src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs

@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@ -27,19 +29,28 @@ internal static class HistogramEncoder
private const ushort InvalidHistogramSymbol = ushort.MaxValue;
public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List<Vp8LHistogram> imageHisto, Vp8LHistogram tmpHisto, Span<ushort> histogramSymbols)
public static void GetHistoImageSymbols(
MemoryAllocator memoryAllocator,
int xSize,
int ySize,
Vp8LBackwardRefs refs,
uint quality,
int histoBits,
int cacheBits,
Vp8LHistogramSet imageHisto,
Vp8LHistogram tmpHisto,
Span<ushort> histogramSymbols)
{
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1;
int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1;
int imageHistoRawSize = histoXSize * histoYSize;
int entropyCombineNumBins = BinSize;
ushort[] mapTmp = new ushort[imageHistoRawSize];
ushort[] clusterMappings = new ushort[imageHistoRawSize];
var origHisto = new List<Vp8LHistogram>(imageHistoRawSize);
for (int i = 0; i < imageHistoRawSize; i++)
{
origHisto.Add(new Vp8LHistogram(cacheBits));
}
const int entropyCombineNumBins = BinSize;
using IMemoryOwner<ushort> tmp = memoryAllocator.Allocate<ushort>(imageHistoRawSize * 2, AllocationOptions.Clean);
Span<ushort> mapTmp = tmp.Slice(0, imageHistoRawSize);
Span<ushort> clusterMappings = tmp.Slice(imageHistoRawSize, imageHistoRawSize);
using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits);
// Construct the histograms from the backward references.
HistogramBuild(xSize, histoBits, refs, origHisto);
@ -50,18 +61,17 @@ internal static class HistogramEncoder
bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100;
if (entropyCombine)
{
ushort[] binMap = mapTmp;
int numClusters = numUsed;
double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality);
HistogramAnalyzeEntropyBin(imageHisto, binMap);
HistogramAnalyzeEntropyBin(imageHisto, mapTmp);
// Collapse histograms with similar entropy.
HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor);
HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, mapTmp, entropyCombineNumBins, combineCostFactor);
OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols);
}
float x = quality / 100.0f;
float x = quality / 100F;
// Cubic ramp between 1 and MaxHistoGreedy:
int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1)));
@ -77,26 +87,25 @@ internal static class HistogramEncoder
HistogramRemap(origHisto, imageHisto, histogramSymbols);
}
private static void RemoveEmptyHistograms(List<Vp8LHistogram> histograms)
private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms)
{
int size = 0;
for (int i = 0; i < histograms.Count; i++)
for (int i = histograms.Count - 1; i >= 0; i--)
{
if (histograms[i] == null)
{
continue;
histograms.RemoveAt(i);
}
histograms[size++] = histograms[i];
}
histograms.RemoveRange(size, histograms.Count - size);
}
/// <summary>
/// Construct the histograms from the backward references.
/// </summary>
private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List<Vp8LHistogram> histograms)
private static void HistogramBuild(
int xSize,
int histoBits,
Vp8LBackwardRefs backwardRefs,
Vp8LHistogramSet histograms)
{
int x = 0, y = 0;
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
@ -119,10 +128,10 @@ internal static class HistogramEncoder
/// Partition histograms to different entropy bins for three dominant (literal,
/// red and blue) symbol costs and compute the histogram aggregate bitCost.
/// </summary>
private static void HistogramAnalyzeEntropyBin(List<Vp8LHistogram> histograms, ushort[] binMap)
private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span<ushort> binMap)
{
int histoSize = histograms.Count;
var costRange = new DominantCostRange();
DominantCostRange costRange = new();
// Analyze the dominant (literal, red and blue) entropy costs.
for (int i = 0; i < histoSize; i++)
@ -148,17 +157,20 @@ internal static class HistogramEncoder
}
}
private static int HistogramCopyAndAnalyze(List<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, Span<ushort> histogramSymbols)
private static int HistogramCopyAndAnalyze(
Vp8LHistogramSet origHistograms,
Vp8LHistogramSet histograms,
Span<ushort> histogramSymbols)
{
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int clusterId = 0, i = 0; i < origHistograms.Count; i++)
{
Vp8LHistogram origHistogram = origHistograms[i];
origHistogram.UpdateHistogramCost(stats, bitsEntropy);
// Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77).
if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4])
if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4))
{
origHistograms[i] = null;
histograms[i] = null;
@ -166,7 +178,7 @@ internal static class HistogramEncoder
}
else
{
histograms[i] = (Vp8LHistogram)origHistogram.DeepClone();
origHistogram.CopyTo(histograms[i]);
histogramSymbols[i] = (ushort)clusterId++;
}
}
@ -184,11 +196,11 @@ internal static class HistogramEncoder
}
private static void HistogramCombineEntropyBin(
List<Vp8LHistogram> histograms,
Vp8LHistogramSet histograms,
Span<ushort> clusters,
ushort[] clusterMappings,
Span<ushort> clusterMappings,
Vp8LHistogram curCombo,
ushort[] binMap,
ReadOnlySpan<ushort> binMap,
int numBins,
double combineCostFactor)
{
@ -205,9 +217,9 @@ internal static class HistogramEncoder
clusterMappings[idx] = (ushort)idx;
}
var indicesToRemove = new List<int>();
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
List<int> indicesToRemove = new();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int idx = 0; idx < histograms.Count; idx++)
{
if (histograms[idx] == null)
@ -236,13 +248,11 @@ internal static class HistogramEncoder
// histogram pairs. In that case, we fallback to combining
// histograms as usual to avoid increasing the header size.
bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym);
int maxCombineFailures = 32;
const int maxCombineFailures = 32;
if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures)
{
// Move the (better) merged histogram to its final slot.
Vp8LHistogram tmp = curCombo;
curCombo = histograms[first];
histograms[first] = tmp;
(histograms[first], curCombo) = (curCombo, histograms[first]);
histograms[idx] = null;
indicesToRemove.Add(idx);
@ -256,9 +266,9 @@ internal static class HistogramEncoder
}
}
foreach (int index in indicesToRemove.OrderByDescending(i => i))
for (int i = indicesToRemove.Count - 1; i >= 0; i--)
{
histograms.RemoveAt(index);
histograms.RemoveAt(indicesToRemove[i]);
}
}
@ -266,7 +276,7 @@ internal static class HistogramEncoder
/// Given a Histogram set, the mapping of clusters 'clusterMapping' and the
/// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values.
/// </summary>
private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span<ushort> symbols)
private static void OptimizeHistogramSymbols(Span<ushort> clusterMappings, int numClusters, Span<ushort> clusterMappingsTmp, Span<ushort> symbols)
{
bool doContinue = true;
@ -293,7 +303,7 @@ internal static class HistogramEncoder
// Create a mapping from a cluster id to its minimal version.
int clusterMax = 0;
clusterMappingsTmp.AsSpan().Clear();
clusterMappingsTmp.Clear();
// Re-map the ids.
for (int i = 0; i < symbols.Length; i++)
@ -318,15 +328,15 @@ internal static class HistogramEncoder
/// Perform histogram aggregation using a stochastic approach.
/// </summary>
/// <returns>true if a greedy approach needs to be performed afterwards, false otherwise.</returns>
private static bool HistogramCombineStochastic(List<Vp8LHistogram> histograms, int minClusterSize)
private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize)
{
uint seed = 1;
int triesWithNoSuccess = 0;
int numUsed = histograms.Count(h => h != null);
int outerIters = numUsed;
int numTriesNoSuccess = (int)((uint)outerIters / 2);
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
if (numUsed < minClusterSize)
{
@ -335,25 +345,25 @@ internal static class HistogramEncoder
// Priority list of histogram pairs. Its size impacts the quality of the compression and the speed:
// the smaller the faster but the worse for the compression.
var histoPriorityList = new List<HistogramPair>();
int maxSize = 9;
List<HistogramPair> histoPriorityList = new();
const int maxSize = 9;
// Fill the initial mapping.
Span<int> mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count];
for (int j = 0, iter = 0; iter < histograms.Count; iter++)
for (int j = 0, i = 0; i < histograms.Count; i++)
{
if (histograms[iter] == null)
if (histograms[i] == null)
{
continue;
}
mappings[j++] = iter;
mappings[j++] = i;
}
// Collapse similar histograms.
for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++)
for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++)
{
double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff;
double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff;
int numTries = (int)((uint)numUsed / 2);
uint randRange = (uint)((numUsed - 1) * numUsed);
@ -398,12 +408,12 @@ internal static class HistogramEncoder
int mappingIndex = mappings.IndexOf(bestIdx2);
Span<int> src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1);
Span<int> dst = mappings.Slice(mappingIndex);
Span<int> dst = mappings[mappingIndex..];
src.CopyTo(dst);
// Merge the histograms and remove bestIdx2 from the list.
HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]);
histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo;
histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo;
histograms[bestIdx2] = null;
numUsed--;
@ -418,7 +428,7 @@ internal static class HistogramEncoder
// check for it all the time nevertheless.
if (isIdx1Best && isIdx2Best)
{
histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@ -439,18 +449,17 @@ internal static class HistogramEncoder
// Make sure the index order is respected.
if (p.Idx1 > p.Idx2)
{
int tmp = p.Idx2;
p.Idx2 = p.Idx1;
p.Idx1 = tmp;
(p.Idx1, p.Idx2) = (p.Idx2, p.Idx1);
}
if (doEval)
{
// Re-evaluate the cost of an updated pair.
HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0.0d, p);
if (p.CostDiff >= 0.0d)
HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p);
if (p.CostDiff >= 0D)
{
histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@ -463,20 +472,18 @@ internal static class HistogramEncoder
triesWithNoSuccess = 0;
}
bool doGreedy = numUsed <= minClusterSize;
return doGreedy;
return numUsed <= minClusterSize;
}
private static void HistogramCombineGreedy(List<Vp8LHistogram> histograms)
private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
{
int histoSize = histograms.Count(h => h != null);
// Priority list of histogram pairs.
var histoPriorityList = new List<HistogramPair>();
List<HistogramPair> histoPriorityList = new();
int maxSize = histoSize * histoSize;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i < histoSize; i++)
{
@ -509,11 +516,11 @@ internal static class HistogramEncoder
// Remove pairs intersecting the just combined best pair.
for (int i = 0; i < histoPriorityList.Count;)
{
HistogramPair p = histoPriorityList.ElementAt(i);
HistogramPair p = histoPriorityList[i];
if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2)
{
// Replace item at pos i with the last one and shrinking the list.
histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[i] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
}
else
@ -536,12 +543,15 @@ internal static class HistogramEncoder
}
}
private static void HistogramRemap(List<Vp8LHistogram> input, List<Vp8LHistogram> output, Span<ushort> symbols)
private static void HistogramRemap(
Vp8LHistogramSet input,
Vp8LHistogramSet output,
Span<ushort> symbols)
{
int inSize = input.Count;
int outSize = output.Count;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
if (outSize > 1)
{
for (int i = 0; i < inSize; i++)
@ -577,11 +587,11 @@ internal static class HistogramEncoder
}
// Recompute each output.
int paletteCodeBits = output.First().PaletteCodeBits;
output.Clear();
int paletteCodeBits = output[0].PaletteCodeBits;
for (int i = 0; i < outSize; i++)
{
output.Add(new Vp8LHistogram(paletteCodeBits));
output[i].Clear();
output[i].PaletteCodeBits = paletteCodeBits;
}
for (int i = 0; i < inSize; i++)
@ -600,20 +610,26 @@ internal static class HistogramEncoder
/// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy.
/// </summary>
/// <returns>The cost of the pair, or 0 if it superior to threshold.</returns>
private static double HistoPriorityListPush(List<HistogramPair> histoList, int maxSize, List<Vp8LHistogram> histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy)
private static double HistoPriorityListPush(
List<HistogramPair> histoList,
int maxSize,
Vp8LHistogramSet histograms,
int idx1,
int idx2,
double threshold,
Vp8LStreaks stats,
Vp8LBitEntropy bitsEntropy)
{
var pair = new HistogramPair();
HistogramPair pair = new();
if (histoList.Count == maxSize)
{
return 0.0d;
return 0D;
}
if (idx1 > idx2)
{
int tmp = idx2;
idx2 = idx1;
idx1 = tmp;
(idx1, idx2) = (idx2, idx1);
}
pair.Idx1 = idx1;
@ -637,9 +653,16 @@ internal static class HistogramEncoder
}
/// <summary>
/// Update the cost diff and combo of a pair of histograms. This needs to be called when the the histograms have been merged with a third one.
/// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been
/// merged with a third one.
/// </summary>
private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double threshold, HistogramPair pair)
private static void HistoListUpdatePair(
Vp8LHistogram h1,
Vp8LHistogram h2,
Vp8LStreaks stats,
Vp8LBitEntropy bitsEntropy,
double threshold,
HistogramPair pair)
{
double sumCost = h1.BitCost + h2.BitCost;
pair.CostCombo = 0.0d;

18
src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs

@ -25,7 +25,7 @@ internal static class HuffmanUtils
0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf
};
public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
public static void CreateHuffmanTree(Span<uint> histogram, int treeDepthLimit, bool[] bufRle, Span<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
{
int numSymbols = huffCode.NumSymbols;
bufRle.AsSpan().Clear();
@ -40,7 +40,7 @@ internal static class HuffmanUtils
/// Change the population counts in a way that the consequent
/// Huffman tree compression, especially its RLE-part, give smaller output.
/// </summary>
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts)
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span<uint> counts)
{
// 1) Let's make the Huffman code more compatible with rle encoding.
for (; length >= 0; --length)
@ -116,7 +116,7 @@ internal static class HuffmanUtils
{
// We don't want to change value at counts[i],
// that is already belonging to the next stride. Thus - 1.
counts[i - k - 1] = count;
counts[(int)(i - k - 1)] = count;
}
}
@ -159,7 +159,7 @@ internal static class HuffmanUtils
/// <param name="histogramSize">The size of the histogram.</param>
/// <param name="treeDepthLimit">The tree depth limit.</param>
/// <param name="bitDepths">How many bits are used for the symbol.</param>
public static void GenerateOptimalTree(Span<HuffmanTree> tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
public static void GenerateOptimalTree(Span<HuffmanTree> tree, Span<uint> histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
{
uint countMin;
int treeSizeOrig = 0;
@ -177,7 +177,7 @@ internal static class HuffmanUtils
return;
}
Span<HuffmanTree> treePool = tree.Slice(treeSizeOrig);
Span<HuffmanTree> treePool = tree[treeSizeOrig..];
// For block sizes with less than 64k symbols we never need to do a
// second iteration of this loop.
@ -202,7 +202,7 @@ internal static class HuffmanUtils
}
// Build the Huffman tree.
Span<HuffmanTree> treeSlice = tree.Slice(0, treeSize);
Span<HuffmanTree> treeSlice = tree[..treeSize];
treeSlice.Sort(HuffmanTree.Compare);
if (treeSize > 1)
@ -357,7 +357,7 @@ internal static class HuffmanUtils
// Special case code with only one value.
if (offsets[WebpConstants.MaxAllowedCodeLength] == 1)
{
var huffmanCode = new HuffmanCode()
HuffmanCode huffmanCode = new()
{
BitsUsed = 0,
Value = (uint)sorted[0]
@ -390,7 +390,7 @@ internal static class HuffmanUtils
for (; countsLen > 0; countsLen--)
{
var huffmanCode = new HuffmanCode()
HuffmanCode huffmanCode = new()
{
BitsUsed = len,
Value = (uint)sorted[symbol++]
@ -432,7 +432,7 @@ internal static class HuffmanUtils
};
}
var huffmanCode = new HuffmanCode
HuffmanCode huffmanCode = new()
{
BitsUsed = len - rootBits,
Value = (uint)sorted[symbol++]

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

@ -15,7 +15,7 @@ internal sealed class PixOrCopy
public uint BgraOrDistance { get; set; }
public static PixOrCopy CreateCacheIdx(int idx) =>
new()
new PixOrCopy
{
Mode = PixOrCopyMode.CacheIdx,
BgraOrDistance = (uint)idx,
@ -23,21 +23,22 @@ internal sealed class PixOrCopy
};
public static PixOrCopy CreateLiteral(uint bgra) =>
new()
new PixOrCopy
{
Mode = PixOrCopyMode.Literal,
BgraOrDistance = bgra,
Len = 1
};
public static PixOrCopy CreateCopy(uint distance, ushort len) => new()
public static PixOrCopy CreateCopy(uint distance, ushort len) =>
new PixOrCopy
{
Mode = PixOrCopyMode.Copy,
BgraOrDistance = distance,
Len = len
};
public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff;
public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF;
public uint CacheIdx() => this.BgraOrDistance;

6
src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs

@ -125,7 +125,7 @@ internal class Vp8LBitEntropy
/// <summary>
/// Get the entropy for the distribution 'X'.
/// </summary>
public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
public void BitsEntropyUnrefined(Span<uint> x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@ -147,7 +147,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats)
public void GetCombinedEntropyUnrefined(Span<uint> x, Span<uint> y, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@ -169,7 +169,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
public void GetEntropyUnrefined(Span<uint> x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;

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

@ -6,7 +6,9 @@ using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -235,26 +237,60 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public Vp8LHashChain HashChain { get; }
/// <summary>
/// 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)
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAnimation)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)image.Width,
(uint)image.Height,
exifProfile,
xmpProfile,
metadata.IccProfile,
false,
hasAnimation);
if (hasAnimation)
{
WebpMetadata webpMetadata = metadata.GetWebpMetadata();
BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount);
}
}
public void EncodeFooter<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.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height);
// Write the image size.
this.WriteImageSize(width, height);
@ -263,35 +299,60 @@ internal class Vp8LEncoder : IDisposable
this.WriteAlphaAndVersion(hasAlpha);
// Encode the main image stream.
this.EncodeStream(image);
this.EncodeStream(frame);
this.bitWriter.Finish();
long prevPosition = 0;
if (hasAnimation)
{
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(
0,
0,
(uint)frame.Width,
(uint)frame.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
.WriteHeaderTo(stream);
}
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha);
this.bitWriter.WriteEncodedImageToStream(stream);
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition);
}
}
/// <summary>
/// Encodes the alpha image data using the webp lossless compression.
/// </summary>
/// <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>
/// <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.
/// </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>
{
int width = image.Width;
int height = image.Height;
int width = frame.Width;
int height = frame.Height;
int pixelCount = width * height;
// Convert image pixels to bgra array.
this.ConvertPixelsToBgra(image, width, height);
this.ConvertPixelsToBgra(frame, width, height);
// The image-stream will NOT contain any headers describing the image dimension, the dimension is already known.
this.EncodeStream(image);
this.EncodeStream(frame);
this.bitWriter.Finish();
int size = this.bitWriter.NumBytes();
int size = this.bitWriter.NumBytes;
if (size >= pixelCount)
{
// Compressing would not yield in smaller data -> leave the data uncompressed.
@ -333,12 +394,12 @@ internal class Vp8LEncoder : IDisposable
/// Encodes the image stream using lossless webp format.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="image">The image to encode.</param>
private void EncodeStream<TPixel>(Image<TPixel> image)
/// <param name="frame">The frame to encode.</param>
private void EncodeStream<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
int width = frame.Width;
int height = frame.Height;
Span<uint> bgra = this.Bgra.GetSpan();
Span<uint> encodedData = this.EncodedData.GetSpan();
@ -425,9 +486,9 @@ internal class Vp8LEncoder : IDisposable
lowEffort);
// If we are better than what we already have.
if (isFirstConfig || this.bitWriter.NumBytes() < bestSize)
if (isFirstConfig || this.bitWriter.NumBytes < bestSize)
{
bestSize = this.bitWriter.NumBytes();
bestSize = this.bitWriter.NumBytes;
BitWriterSwap(ref this.bitWriter, ref bitWriterBest);
}
@ -447,14 +508,14 @@ internal class Vp8LEncoder : IDisposable
/// Converts the pixels of the image to bgra.
/// </summary>
/// <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="height">The height of the image.</param>
/// <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>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
bool nonOpaque = false;
Span<uint> bgra = this.Bgra.GetSpan();
Span<byte> bgraBytes = MemoryMarshal.Cast<uint, byte>(bgra);
@ -589,15 +650,21 @@ internal class Vp8LEncoder : IDisposable
Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0];
this.bitWriter.Reset(bwInit);
Vp8LHistogram tmpHisto = new(cacheBits);
List<Vp8LHistogram> histogramImage = new(histogramImageXySize);
for (int i = 0; i < histogramImageXySize; i++)
{
histogramImage.Add(new Vp8LHistogram(cacheBits));
}
using OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits);
using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits);
// Build histogram image and symbols from backward references.
HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
HistogramEncoder.GetHistoImageSymbols(
this.memoryAllocator,
width,
height,
refsBest,
this.quality,
this.HistoBits,
cacheBits,
histogramImage,
tmpHisto,
histogramSymbols);
// Create Huffman bit lengths and codes for each histogram image.
int histogramImageSize = histogramImage.Count;
@ -676,11 +743,9 @@ internal class Vp8LEncoder : IDisposable
this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes);
// Keep track of the smallest image so far.
if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes()))
if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes))
{
Vp8LBitWriter tmp = this.bitWriter;
this.bitWriter = bitWriterBest;
bitWriterBest = tmp;
(bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
}
isFirstIteration = false;
@ -787,13 +852,8 @@ internal class Vp8LEncoder : IDisposable
refsTmp1,
refsTmp2);
List<Vp8LHistogram> histogramImage = new()
{
new(cacheBits)
};
// Build histogram image and symbols from backward references.
histogramImage[0].StoreRefs(refs);
using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits);
// Create Huffman bit lengths and codes for each histogram image.
GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
@ -833,7 +893,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{
int count = 0;
Span<int> symbols = this.scratch.Span.Slice(0, 2);
Span<int> symbols = this.scratch.Span[..2];
symbols.Clear();
const int maxBits = 8;
const int maxSymbol = 1 << maxBits;
@ -886,6 +946,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreFullHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
{
// TODO: Allocations. This method is called in a loop.
int i;
byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes];
short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes];
@ -996,7 +1057,12 @@ internal class Vp8LEncoder : IDisposable
}
}
private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span<ushort> histogramSymbols, HuffmanTreeCode[] huffmanCodes)
private void StoreImageToBitMask(
int width,
int histoBits,
Vp8LBackwardRefs backwardRefs,
Span<ushort> histogramSymbols,
HuffmanTreeCode[] huffmanCodes)
{
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1;
int tileMask = histoBits == 0 ? 0 : -(1 << histoBits);
@ -1008,10 +1074,10 @@ internal class Vp8LEncoder : IDisposable
int tileY = y & tileMask;
int histogramIx = histogramSymbols[0];
Span<HuffmanTreeCode> codes = huffmanCodes.AsSpan(5 * histogramIx);
using List<PixOrCopy>.Enumerator c = backwardRefs.Refs.GetEnumerator();
while (c.MoveNext())
for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
PixOrCopy v = c.Current;
PixOrCopy v = backwardRefs.Refs[i];
if (tileX != (x & tileMask) || tileY != (y & tileMask))
{
tileX = x & tileMask;
@ -1024,7 +1090,7 @@ internal class Vp8LEncoder : IDisposable
{
for (int k = 0; k < 4; k++)
{
int code = (int)v.Literal(Order[k]);
int code = v.Literal(Order[k]);
this.bitWriter.WriteHuffmanCode(codes[k], code);
}
}
@ -1149,35 +1215,41 @@ internal class Vp8LEncoder : IDisposable
entropyComp[j] = bitEntropy.BitsEntropyRefine();
}
entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] +
entropyComp[(int)HistoIx.HistoRed] +
entropyComp[(int)HistoIx.HistoGreen] +
entropyComp[(int)HistoIx.HistoBlue];
entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] +
entropyComp[(int)HistoIx.HistoRedPred] +
entropyComp[(int)HistoIx.HistoGreenPred] +
entropyComp[(int)HistoIx.HistoBluePred];
entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] +
entropyComp[(int)HistoIx.HistoRedSubGreen] +
entropyComp[(int)HistoIx.HistoGreen] +
entropyComp[(int)HistoIx.HistoBlueSubGreen];
entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] +
entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
entropyComp[(int)HistoIx.HistoGreenPred] +
entropyComp[(int)HistoIx.HistoBluePredSubGreen];
entropy[(int)EntropyIx.Direct] =
entropyComp[(int)HistoIx.HistoAlpha] +
entropyComp[(int)HistoIx.HistoRed] +
entropyComp[(int)HistoIx.HistoGreen] +
entropyComp[(int)HistoIx.HistoBlue];
entropy[(int)EntropyIx.Spatial] =
entropyComp[(int)HistoIx.HistoAlphaPred] +
entropyComp[(int)HistoIx.HistoRedPred] +
entropyComp[(int)HistoIx.HistoGreenPred] +
entropyComp[(int)HistoIx.HistoBluePred];
entropy[(int)EntropyIx.SubGreen] =
entropyComp[(int)HistoIx.HistoAlpha] +
entropyComp[(int)HistoIx.HistoRedSubGreen] +
entropyComp[(int)HistoIx.HistoGreen] +
entropyComp[(int)HistoIx.HistoBlueSubGreen];
entropy[(int)EntropyIx.SpatialSubGreen] =
entropyComp[(int)HistoIx.HistoAlphaPred] +
entropyComp[(int)HistoIx.HistoRedPredSubGreen] +
entropyComp[(int)HistoIx.HistoGreenPred] +
entropyComp[(int)HistoIx.HistoBluePredSubGreen];
entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette];
// When including transforms, there is an overhead in bits from
// storing them. This overhead is small but matters for small images.
// For spatial, there are 14 transformations.
entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(14);
entropy[(int)EntropyIx.Spatial] +=
LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(14);
// For color transforms: 24 as only 3 channels are considered in a ColorTransformElement.
entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(24);
entropy[(int)EntropyIx.SpatialSubGreen] +=
LosslessUtils.SubSampleSize(width, transformBits) *
LosslessUtils.SubSampleSize(height, transformBits) *
LosslessUtils.FastLog2(24);
// For palettes, add the cost of storing the palette.
// We empirically estimate the cost of a compressed entry as 8 bits.
@ -1379,10 +1451,8 @@ internal class Vp8LEncoder : IDisposable
useLut = false;
break;
}
else
{
buffer[ind] = (uint)j;
}
buffer[ind] = (uint)j;
}
if (useLut)
@ -1591,14 +1661,12 @@ internal class Vp8LEncoder : IDisposable
}
// Swap color(palette[bestIdx], palette[i]);
uint best = palette[bestIdx];
palette[bestIdx] = palette[i];
palette[i] = best;
(palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]);
predict = palette[i];
}
}
private static void GetHuffBitLengthsAndCodes(List<Vp8LHistogram> histogramImage, HuffmanTreeCode[] huffmanCodes)
private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes)
{
int maxNumSymbols = 0;
@ -1609,13 +1677,25 @@ internal class Vp8LEncoder : IDisposable
int startIdx = 5 * i;
for (int k = 0; k < 5; k++)
{
int numSymbols =
k == 0 ? histo.NumCodes() :
k == 4 ? WebpConstants.NumDistanceCodes : 256;
int numSymbols;
if (k == 0)
{
numSymbols = histo.NumCodes();
}
else if (k == 4)
{
numSymbols = WebpConstants.NumDistanceCodes;
}
else
{
numSymbols = 256;
}
huffmanCodes[startIdx + k].NumSymbols = numSymbols;
}
}
// TODO: Allocations.
int end = 5 * histogramImage.Count;
for (int i = 0; i < end; i++)
{
@ -1629,8 +1709,9 @@ internal class Vp8LEncoder : IDisposable
}
// Create Huffman trees.
// TODO: Allocations.
bool[] bufRle = new bool[maxNumSymbols];
Span<HuffmanTree> huffTree = stackalloc HuffmanTree[3 * maxNumSymbols];
HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols];
for (int i = 0; i < histogramImage.Count; i++)
{
@ -1682,8 +1763,18 @@ internal class Vp8LEncoder : IDisposable
histoBits++;
}
return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits :
histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits;
if (histoBits < WebpConstants.MinHuffmanBits)
{
return WebpConstants.MinHuffmanBits;
}
else if (histoBits > WebpConstants.MaxHuffmanBits)
{
return WebpConstants.MaxHuffmanBits;
}
else
{
return histoBits;
}
}
/// <summary>
@ -1720,11 +1811,7 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst)
{
Vp8LBitWriter tmp = src;
src = dst;
dst = tmp;
}
=> (dst, src) = (src, dst);
/// <summary>
/// Calculates the bits used for the transformation.
@ -1732,9 +1819,21 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetTransformBits(WebpEncodingMethod method, int histoBits)
{
int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5;
int res = histoBits > maxTransformBits ? maxTransformBits : histoBits;
return res;
int maxTransformBits;
if ((int)method < 4)
{
maxTransformBits = 6;
}
else if (method > WebpEncodingMethod.Level4)
{
maxTransformBits = 4;
}
else
{
maxTransformBits = 5;
}
return histoBits > maxTransformBits ? maxTransformBits : histoBits;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -1812,9 +1911,9 @@ internal class Vp8LEncoder : IDisposable
/// </summary>
public void ClearRefs()
{
for (int i = 0; i < this.Refs.Length; i++)
foreach (Vp8LBackwardRefs t in this.Refs)
{
this.Refs[i].Refs.Clear();
t.Refs.Clear();
}
}
@ -1823,9 +1922,9 @@ internal class Vp8LEncoder : IDisposable
{
this.Bgra.Dispose();
this.EncodedData.Dispose();
this.BgraScratch.Dispose();
this.BgraScratch?.Dispose();
this.Palette.Dispose();
this.TransformData.Dispose();
this.TransformData?.Dispose();
this.HashChain.Dispose();
}

308
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs

@ -1,63 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal sealed class Vp8LHistogram : IDeepCloneable
internal abstract unsafe class Vp8LHistogram
{
private const uint NonTrivialSym = 0xffffffff;
private readonly uint* red;
private readonly uint* blue;
private readonly uint* alpha;
private readonly uint* distance;
private readonly uint* literal;
private readonly uint* isUsed;
private const int RedSize = WebpConstants.NumLiteralCodes;
private const int BlueSize = WebpConstants.NumLiteralCodes;
private const int AlphaSize = WebpConstants.NumLiteralCodes;
private const int DistanceSize = WebpConstants.NumDistanceCodes;
public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1;
private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance
public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to create an instance from.</param>
private Vp8LHistogram(Vp8LHistogram other)
: this(other.PaletteCodeBits)
{
other.Red.AsSpan().CopyTo(this.Red);
other.Blue.AsSpan().CopyTo(this.Blue);
other.Alpha.AsSpan().CopyTo(this.Alpha);
other.Literal.AsSpan().CopyTo(this.Literal);
other.Distance.AsSpan().CopyTo(this.Distance);
other.IsUsed.AsSpan().CopyTo(this.IsUsed);
this.LiteralCost = other.LiteralCost;
this.RedCost = other.RedCost;
this.BlueCost = other.BlueCost;
this.BitCost = other.BitCost;
this.TrivialSymbol = other.TrivialSymbol;
this.PaletteCodeBits = other.PaletteCodeBits;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="basePointer">The base pointer to the backing memory.</param>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits)
: this(paletteCodeBits) => this.StoreRefs(refs);
protected Vp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits)
: this(basePointer, paletteCodeBits) => this.StoreRefs(refs);
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="basePointer">The base pointer to the backing memory.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(int paletteCodeBits)
protected Vp8LHistogram(uint* basePointer, int paletteCodeBits)
{
this.PaletteCodeBits = paletteCodeBits;
this.Red = new uint[WebpConstants.NumLiteralCodes + 1];
this.Blue = new uint[WebpConstants.NumLiteralCodes + 1];
this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1];
this.Distance = new uint[WebpConstants.NumDistanceCodes];
int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits);
this.Literal = new uint[literalSize + 1];
// 5 for literal, red, blue, alpha, distance.
this.IsUsed = new bool[5];
this.red = basePointer;
this.blue = this.red + RedSize;
this.alpha = this.blue + BlueSize;
this.distance = this.alpha + AlphaSize;
this.literal = this.distance + DistanceSize;
this.isUsed = this.literal + LiteralSize;
}
/// <summary>
@ -85,22 +78,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// </summary>
public double BlueCost { get; set; }
public uint[] Red { get; }
public Span<uint> Red => new(this.red, RedSize);
public uint[] Blue { get; }
public Span<uint> Blue => new(this.blue, BlueSize);
public uint[] Alpha { get; }
public Span<uint> Alpha => new(this.alpha, AlphaSize);
public uint[] Literal { get; }
public Span<uint> Distance => new(this.distance, DistanceSize);
public uint[] Distance { get; }
public Span<uint> Literal => new(this.literal, LiteralSize);
public uint TrivialSymbol { get; set; }
public bool[] IsUsed { get; }
private Span<uint> IsUsedSpan => new(this.isUsed, UsedSize);
private Span<uint> TotalSpan => new(this.red, BufferSize);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0;
/// <summary>
/// Creates a copy of the given <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to copy to.</param>
public void CopyTo(Vp8LHistogram other)
{
this.Red.CopyTo(other.Red);
this.Blue.CopyTo(other.Blue);
this.Alpha.CopyTo(other.Alpha);
this.Literal.CopyTo(other.Literal);
this.Distance.CopyTo(other.Distance);
this.IsUsedSpan.CopyTo(other.IsUsedSpan);
other.LiteralCost = this.LiteralCost;
other.RedCost = this.RedCost;
other.BlueCost = this.BlueCost;
other.BitCost = this.BitCost;
other.TrivialSymbol = this.TrivialSymbol;
other.PaletteCodeBits = this.PaletteCodeBits;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new Vp8LHistogram(this);
public void Clear()
{
this.TotalSpan.Clear();
this.PaletteCodeBits = 0;
this.BitCost = 0;
this.LiteralCost = 0;
this.RedCost = 0;
this.BlueCost = 0;
this.TrivialSymbol = 0;
}
/// <summary>
/// Collect all the references into a histogram (without reset).
@ -108,10 +138,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <param name="refs">The backward references.</param>
public void StoreRefs(Vp8LBackwardRefs refs)
{
using List<PixOrCopy>.Enumerator c = refs.Refs.GetEnumerator();
while (c.MoveNext())
for (int i = 0; i < refs.Refs.Count; i++)
{
this.AddSinglePixOrCopy(c.Current, false);
this.AddSinglePixOrCopy(refs.Refs[i], false);
}
}
@ -163,12 +192,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
{
uint notUsed = 0;
return
PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0], stats, bitsEntropy)
+ PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1], stats, bitsEntropy)
+ PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2], stats, bitsEntropy)
+ PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3], stats, bitsEntropy)
+ PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy)
+ ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes)
this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy)
+ this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy)
+ this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy)
+ this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy)
+ this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy)
+ ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes)
+ ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
}
@ -177,12 +206,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
uint alphaSym = 0, redSym = 0, blueSym = 0;
uint notUsed = 0;
double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3], stats, bitsEntropy);
double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy);
double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
int numCodes = this.NumCodes();
this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1], stats, bitsEntropy);
this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2], stats, bitsEntropy);
this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy);
this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy);
this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost;
if ((alphaSym | redSym | blueSym) == NonTrivialSym)
{
@ -234,7 +263,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
for (int i = 0; i < 5; i++)
{
output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i];
output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i));
}
output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol
@ -247,9 +276,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bool trivialAtEnd = false;
cost = costInitial;
cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false, stats, bitEntropy);
cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy);
cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
if (cost > costThreshold)
{
@ -270,155 +299,158 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false, stats, bitEntropy);
cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes);
if (cost > costThreshold)
{
return false;
}
return true;
return cost <= costThreshold;
}
private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize)
{
if (this.IsUsed[0])
if (this.IsUsed(0))
{
if (b.IsUsed[0])
if (b.IsUsed(0))
{
AddVector(this.Literal, b.Literal, output.Literal, literalSize);
}
else
{
this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
this.Literal[..literalSize].CopyTo(output.Literal);
}
}
else if (b.IsUsed[0])
else if (b.IsUsed(0))
{
b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
b.Literal[..literalSize].CopyTo(output.Literal);
}
else
{
output.Literal.AsSpan(0, literalSize).Clear();
output.Literal[..literalSize].Clear();
}
}
private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[1])
if (this.IsUsed(1))
{
if (b.IsUsed[1])
if (b.IsUsed(1))
{
AddVector(this.Red, b.Red, output.Red, size);
}
else
{
this.Red.AsSpan(0, size).CopyTo(output.Red);
this.Red[..size].CopyTo(output.Red);
}
}
else if (b.IsUsed[1])
else if (b.IsUsed(1))
{
b.Red.AsSpan(0, size).CopyTo(output.Red);
b.Red[..size].CopyTo(output.Red);
}
else
{
output.Red.AsSpan(0, size).Clear();
output.Red[..size].Clear();
}
}
private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[2])
if (this.IsUsed(2))
{
if (b.IsUsed[2])
if (b.IsUsed(2))
{
AddVector(this.Blue, b.Blue, output.Blue, size);
}
else
{
this.Blue.AsSpan(0, size).CopyTo(output.Blue);
this.Blue[..size].CopyTo(output.Blue);
}
}
else if (b.IsUsed[2])
else if (b.IsUsed(2))
{
b.Blue.AsSpan(0, size).CopyTo(output.Blue);
b.Blue[..size].CopyTo(output.Blue);
}
else
{
output.Blue.AsSpan(0, size).Clear();
output.Blue[..size].Clear();
}
}
private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[3])
if (this.IsUsed(3))
{
if (b.IsUsed[3])
if (b.IsUsed(3))
{
AddVector(this.Alpha, b.Alpha, output.Alpha, size);
}
else
{
this.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
this.Alpha[..size].CopyTo(output.Alpha);
}
}
else if (b.IsUsed[3])
else if (b.IsUsed(3))
{
b.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
b.Alpha[..size].CopyTo(output.Alpha);
}
else
{
output.Alpha.AsSpan(0, size).Clear();
output.Alpha[..size].Clear();
}
}
private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[4])
if (this.IsUsed(4))
{
if (b.IsUsed[4])
if (b.IsUsed(4))
{
AddVector(this.Distance, b.Distance, output.Distance, size);
}
else
{
this.Distance.AsSpan(0, size).CopyTo(output.Distance);
this.Distance[..size].CopyTo(output.Distance);
}
}
else if (b.IsUsed[4])
else if (b.IsUsed(4))
{
b.Distance.AsSpan(0, size).CopyTo(output.Distance);
b.Distance[..size].CopyTo(output.Distance);
}
else
{
output.Distance.AsSpan(0, size).Clear();
output.Distance[..size].Clear();
}
}
private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
private static double GetCombinedEntropy(
Span<uint> x,
Span<uint> y,
int length,
bool isXUsed,
bool isYUsed,
bool trivialAtEnd,
Vp8LStreaks stats,
Vp8LBitEntropy bitEntropy)
{
stats.Clear();
bitEntropy.Init();
@ -450,18 +482,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bitEntropy.GetEntropyUnrefined(x, length, stats);
}
}
else if (isYUsed)
{
bitEntropy.GetEntropyUnrefined(y, length, stats);
}
else
{
if (isYUsed)
{
bitEntropy.GetEntropyUnrefined(y, length, stats);
}
else
{
stats.Counts[0] = 1;
stats.Streaks[0][length > 3 ? 1 : 0] = length;
bitEntropy.Init();
}
stats.Counts[0] = 1;
stats.Streaks[0][length > 3 ? 1 : 0] = length;
bitEntropy.Init();
}
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
@ -482,7 +511,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <summary>
/// Get the symbol entropy for the distribution 'population'.
/// </summary>
private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
private double PopulationCost(Span<uint> population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
{
bitEntropy.Init();
stats.Clear();
@ -491,7 +520,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym;
// The histogram is used if there is at least one non-zero streak.
isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0;
this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0);
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
}
@ -557,3 +586,56 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
}
internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable
{
private readonly IMemoryOwner<uint> bufferOwner;
private MemoryHandle bufferHandle;
private bool isDisposed;
private OwnedVp8LHistogram(
IMemoryOwner<uint> bufferOwner,
ref MemoryHandle bufferHandle,
uint* basePointer,
int paletteCodeBits)
: base(basePointer, paletteCodeBits)
{
this.bufferOwner = bufferOwner;
this.bufferHandle = bufferHandle;
}
/// <summary>
/// Creates an <see cref="OwnedVp8LHistogram"/> that is not a member of a <see cref="Vp8LHistogramSet"/>.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits)
{
IMemoryOwner<uint> bufferOwner = memoryAllocator.Allocate<uint>(BufferSize, AllocationOptions.Clean);
MemoryHandle bufferHandle = bufferOwner.Memory.Pin();
return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits);
}
/// <summary>
/// Creates an <see cref="OwnedVp8LHistogram"/> that is not a member of a <see cref="Vp8LHistogramSet"/>.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits)
{
OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits);
histogram.StoreRefs(refs);
return histogram;
}
public void Dispose()
{
if (!this.isDisposed)
{
this.bufferHandle.Dispose();
this.bufferOwner.Dispose();
this.isDisposed = true;
}
}
}

110
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs

@ -0,0 +1,110 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Collections;
using System.Diagnostics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal sealed class Vp8LHistogramSet : IEnumerable<Vp8LHistogram>, IDisposable
{
private readonly IMemoryOwner<uint> buffer;
private MemoryHandle bufferHandle;
private readonly List<Vp8LHistogram> items;
private bool isDisposed;
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.bufferHandle = this.buffer.Memory.Pin();
unsafe
{
uint* basePointer = (uint*)this.bufferHandle.Pointer;
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits));
}
}
}
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.bufferHandle = this.buffer.Memory.Pin();
unsafe
{
uint* basePointer = (uint*)this.bufferHandle.Pointer;
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits));
}
}
}
public Vp8LHistogramSet(int capacity) => this.items = new(capacity);
public Vp8LHistogramSet() => this.items = new();
public int Count => this.items.Count;
public Vp8LHistogram this[int index]
{
get => this.items[index];
set => this.items[index] = value;
}
public void RemoveAt(int index)
{
this.CheckDisposed();
this.items.RemoveAt(index);
}
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.buffer.Dispose();
this.bufferHandle.Dispose();
this.items.Clear();
this.isDisposed = true;
}
public IEnumerator<Vp8LHistogram> GetEnumerator() => ((IEnumerable<Vp8LHistogram>)this.items).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator();
[Conditional("DEBUG")]
private void CheckDisposed()
{
if (this.isDisposed)
{
ThrowDisposed();
}
}
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet));
private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram
{
public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits)
: base(basePointer, paletteCodeBits)
{
}
public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits)
: base(basePointer, refs, paletteCodeBits)
{
}
}
}

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

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

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

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

@ -4,7 +4,9 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -88,7 +90,8 @@ internal class Vp8Encoder : IDisposable
private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11;
private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const long HeaderSizeEstimate =
WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const int QMin = 0;
@ -165,7 +168,7 @@ internal class Vp8Encoder : IDisposable
// TODO: make partition_limit configurable?
const int limit = 100; // original code: limit = 100 - config->partition_limit;
this.maxI4HeaderBits =
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh];
for (int i = 0; i < this.MbInfo.Length; i++)
@ -308,27 +311,94 @@ internal class Vp8Encoder : IDisposable
/// </summary>
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>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </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)
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>
{
int width = image.Width;
int height = image.Height;
int width = frame.Width;
int height = frame.Height;
int pixelCount = width * height;
Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.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 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];
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
int totalMb = this.Mbw * this.Mbw;
@ -375,13 +445,6 @@ internal class Vp8Encoder : IDisposable
// Store filter stats.
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
// Extract and encode alpha channel data, if present.
int alphaDataSize = 0;
bool alphaCompressionSucceeded = false;
@ -393,7 +456,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
image,
frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@ -408,16 +471,39 @@ internal class Vp8Encoder : IDisposable
}
}
this.bitWriter.WriteEncodedImageToStream(
stream,
exifProfile,
xmpProfile,
metadata.IccProfile,
(uint)width,
(uint)height,
hasAlpha,
alphaData[..alphaDataSize],
this.alphaCompression && alphaCompressionSucceeded);
this.bitWriter.Finish();
long prevPosition = 0;
if (hasAnimation)
{
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = new WebpFrameData(
0,
0,
(uint)frame.Width,
(uint)frame.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
.WriteHeaderTo(stream);
}
if (hasAlpha)
{
Span<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
{
@ -520,7 +606,7 @@ internal class Vp8Encoder : IDisposable
Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.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 sizeP0 = 0;
long distortion = 0;
@ -862,10 +948,11 @@ internal class Vp8Encoder : IDisposable
this.ResetSegments();
}
this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
(p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
(p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
(p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
this.SegmentHeader.Size =
(p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) +
(p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) +
(p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) +
(p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2])));
}
else
{
@ -1027,7 +1114,7 @@ internal class Vp8Encoder : IDisposable
it.NzToBytes();
int pos1 = this.bitWriter.NumBytes();
int pos1 = this.bitWriter.NumBytes;
if (i16)
{
residual.Init(0, 1, this.Proba);
@ -1054,7 +1141,7 @@ internal class Vp8Encoder : IDisposable
}
}
int pos2 = this.bitWriter.NumBytes();
int pos2 = this.bitWriter.NumBytes;
// U/V
residual.Init(0, 2, this.Proba);
@ -1072,7 +1159,7 @@ internal class Vp8Encoder : IDisposable
}
}
int pos3 = this.bitWriter.NumBytes();
int pos3 = this.bitWriter.NumBytes;
it.LumaBits = pos2 - pos1;
it.UvBits = pos3 - pos2;
it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits;

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

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

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.
/// </summary>
/// <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="memoryAllocator">The memory allocator.</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="v">Span to store the v component of the image.</param>
/// <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>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> imageBuffer = frame.PixelBuffer;
int width = imageBuffer.Width;
int height = imageBuffer.Height;
int uvWidth = (width + 1) >> 1;

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

@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -100,7 +100,7 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= 4;
switch (chunkType)
{
case WebpChunkType.Animation:
case WebpChunkType.FrameData:
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? new Color(new Bgra32(0, 0, 0, 0))
: features.AnimationBackgroundColor!.Value;
@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable
private uint ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, uint width, uint height, Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
AnimationFrameData frameData = this.ReadFrameHeader(stream);
WebpFrameData frameData = WebpFrameData.Parse(stream);
long streamStartPosition = stream.Position;
Span<byte> buffer = stackalloc byte[4];
@ -162,6 +162,11 @@ internal class WebpAnimationDecoder : IDisposable
features.AlphaChunkHeader = alphaChunkHeader;
break;
case WebpChunkType.Vp8L:
if (hasAlpha)
{
WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images.");
}
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features);
break;
default:
@ -175,7 +180,7 @@ internal class WebpAnimationDecoder : IDisposable
{
image = new Image<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;
}
@ -183,29 +188,22 @@ internal class WebpAnimationDecoder : IDisposable
{
currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
SetFrameMetadata(currentFrame.Metadata, frameData.Duration);
SetFrameMetadata(currentFrame.Metadata, frameData);
imageFrame = currentFrame;
}
int frameX = (int)(frameData.X * 2);
int frameY = (int)(frameData.Y * 2);
int frameWidth = (int)frameData.Width;
int frameHeight = (int)frameData.Height;
Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight);
Rectangle regionRectangle = frameData.Bounds;
if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose)
if (frameData.DisposalMethod is WebpDisposalMethod.Dispose)
{
this.RestoreToBackground(imageFrame, backgroundColor);
}
using Buffer2D<TPixel> decodedImage = this.DecodeImageData<TPixel>(frameData, webpInfo);
DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
using Buffer2D<TPixel> decodedImageFrame = this.DecodeImageFrameData<TPixel>(frameData, webpInfo);
if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending)
{
this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight);
}
bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending;
DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend);
previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle;
@ -217,12 +215,13 @@ internal class WebpAnimationDecoder : IDisposable
/// Sets the frames metadata.
/// </summary>
/// <param name="meta">The metadata.</param>
/// <param name="duration">The frame duration.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration)
/// <param name="frameData">The frame data.</param>
private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData)
{
WebpFrameMetadata frameMetadata = meta.GetWebpMetadata();
frameMetadata.FrameDuration = duration;
frameMetadata.FrameDelay = frameData.Duration;
frameMetadata.BlendMethod = frameData.BlendingMethod;
frameMetadata.DisposalMethod = frameData.DisposalMethod;
}
/// <summary>
@ -239,7 +238,7 @@ internal class WebpAnimationDecoder : IDisposable
byte alphaChunkHeader = (byte)stream.ReadByte();
Span<byte> alphaData = this.alphaData.GetSpan();
stream.Read(alphaData, 0, alphaDataSize);
_ = stream.Read(alphaData, 0, alphaDataSize);
return alphaChunkHeader;
}
@ -251,22 +250,24 @@ internal class WebpAnimationDecoder : IDisposable
/// <param name="frameData">The frame data.</param>
/// <param name="webpInfo">The webp information.</param>
/// <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>
{
Image<TPixel> decodedImage = new((int)frameData.Width, (int)frameData.Height);
ImageFrame<TPixel> decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height);
try
{
Buffer2D<TPixel> pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> pixelBufferDecoded = decodedFrame.PixelBuffer;
if (webpInfo.IsLossless)
{
WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
WebpLosslessDecoder losslessDecoder =
new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
}
else
{
WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
WebpLossyDecoder lossyDecoder =
new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData);
}
@ -274,7 +275,7 @@ internal class WebpAnimationDecoder : IDisposable
}
catch
{
decodedImage?.Dispose();
decodedFrame?.Dispose();
throw;
}
finally
@ -287,48 +288,43 @@ internal class WebpAnimationDecoder : IDisposable
/// Draws the decoded image on canvas. The decoded image can be smaller the canvas.
/// </summary>
/// <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="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 static void DrawDecodedImageOnCanvas<TPixel>(Buffer2D<TPixel> decodedImage, ImageFrame<TPixel> imageFrame, int frameX, int frameY, int frameWidth, int frameHeight)
/// <param name="restoreArea">The area of the frame.</param>
/// <param name="blend">Whether to blend the decoded frame data onto the target frame.</param>
private static void DrawDecodedImageFrameOnCanvas<TPixel>(
Buffer2D<TPixel> decodedImageFrame,
ImageFrame<TPixel> imageFrame,
Rectangle restoreArea,
bool blend)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageFramePixels = imageFrame.PixelBuffer;
int decodedRowIdx = 0;
for (int y = frameY; y < frameY + frameHeight; y++)
// Trim the destination frame to match the restore area. The source frame is already trimmed.
Buffer2DRegion<TPixel> imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea);
if (blend)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth];
decodedPixelRow.TryCopyTo(framePixelRow[frameX..]);
// The destination frame has already been prepopulated with the pixel data from the previous frame
// so blending will leave the desired result which takes into consideration restoration to the
// background color within the restore area.
PixelBlender<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>
/// 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++)
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
Span<TPixel> dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth);
blender.Blend<TPixel>(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f);
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
decodedPixelRow.CopyTo(framePixelRow);
}
}
@ -353,42 +349,6 @@ internal class WebpAnimationDecoder : IDisposable
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/>
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>
/// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas.
/// </summary>
internal enum AnimationBlendingMethod
public enum WebpBlendingMethod
{
/// <summary>
/// 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.
using System.Buffers.Binary;
using System.Drawing;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -77,7 +78,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes");
}
if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
@ -91,7 +92,7 @@ internal static class WebpChunkParsingUtils
uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6);
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2));
tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]);
uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7;
@ -105,23 +106,16 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
var vp8FrameHeader = new Vp8FrameHeader()
Vp8FrameHeader vp8FrameHeader = new()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
var bitReader = new Vp8BitReader(
stream,
remaining,
memoryAllocator,
partitionLength)
{
Remaining = remaining
};
Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
return new WebpImageInfo()
return new WebpImageInfo
{
Width = width,
Height = height,
@ -145,7 +139,7 @@ internal static class WebpChunkParsingUtils
// VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer);
var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
@ -174,7 +168,7 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
return new WebpImageInfo()
return new WebpImageInfo
{
Width = width,
Height = height,
@ -231,13 +225,13 @@ internal static class WebpChunkParsingUtils
}
// 3 bytes for the width.
uint width = ReadUnsignedInt24Bit(stream, buffer) + 1;
uint width = ReadUInt24LittleEndian(stream, buffer) + 1;
// 3 bytes for the height.
uint height = ReadUnsignedInt24Bit(stream, buffer) + 1;
uint height = ReadUInt24LittleEndian(stream, buffer) + 1;
// Read all the chunks in the order they occur.
var info = new WebpImageInfo()
WebpImageInfo info = new()
{
Width = width,
Height = height,
@ -253,7 +247,7 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param>
/// <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)
{
@ -261,7 +255,28 @@ internal static class WebpChunkParsingUtils
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
throw new ImageFormatException("Invalid Webp data, could not read unsigned integer.");
throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer.");
}
/// <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>
@ -271,14 +286,14 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <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);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1;
}
throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
@ -298,7 +313,7 @@ internal static class WebpChunkParsingUtils
if (stream.Read(buffer) == 4)
{
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
}

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

@ -12,45 +12,54 @@ internal enum WebpChunkType : uint
/// <summary>
/// Header signaling the use of the VP8 format.
/// </summary>
/// <remarks>VP8 (Single)</remarks>
Vp8 = 0x56503820U,
/// <summary>
/// Header signaling the image uses lossless encoding.
/// </summary>
/// <remarks>VP8L (Single)</remarks>
Vp8L = 0x5650384CU,
/// <summary>
/// Header for a extended-VP8 chunk.
/// </summary>
/// <remarks>VP8X (Single)</remarks>
Vp8X = 0x56503858U,
/// <summary>
/// Chunk contains information about the alpha channel.
/// </summary>
/// <remarks>ALPH (Single)</remarks>
Alpha = 0x414C5048U,
/// <summary>
/// Chunk which contains a color profile.
/// </summary>
/// <remarks>ICCP (Single)</remarks>
Iccp = 0x49434350U,
/// <summary>
/// Chunk which contains EXIF metadata about the image.
/// </summary>
/// <remarks>EXIF (Single)</remarks>
Exif = 0x45584946U,
/// <summary>
/// Chunk contains XMP metadata about the image.
/// </summary>
/// <remarks>XMP (Single)</remarks>
Xmp = 0x584D5020U,
/// <summary>
/// For an animated image, this chunk contains the global parameters of the animation.
/// </summary>
/// <remarks>ANIM (Single)</remarks>
AnimationParameter = 0x414E494D,
/// <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.
/// </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>
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>
/// The header bytes identifying RIFF file.
/// </summary>
@ -88,6 +55,11 @@ internal static class WebpConstants
0x50 // P
};
/// <summary>
/// The header bytes identifying a Webp.
/// </summary>
public const string WebpFourCc = "WEBP";
/// <summary>
/// 3 bits reserved for version.
/// </summary>
@ -103,11 +75,6 @@ internal static class WebpConstants
/// </summary>
public const int Vp8FrameHeaderSize = 10;
/// <summary>
/// Size of a VP8X chunk in bytes.
/// </summary>
public const int Vp8XChunkSize = 10;
/// <summary>
/// Size of a chunk header.
/// </summary>

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

@ -17,7 +17,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
/// <summary>
/// Gets the shared instance.
/// </summary>
public static WebpDecoder Instance { get; } = new();
public static WebpDecoder Instance { get; } = new WebpDecoder();
/// <inheritdoc/>
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(stream, nameof(stream));
using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options });
using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options });
return decoder.Identify(options.Configuration, stream, cancellationToken);
}
@ -35,7 +35,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
Guard.NotNull(options, nameof(options));
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);
ScaleToTargetSize(options.GeneralOptions, image);
@ -52,6 +52,5 @@ public sealed class WebpDecoder : SpecializedImageDecoder<WebpDecoderOptions>
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
=> new() { GeneralOptions = options };
protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { 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.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -89,25 +91,30 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
if (this.webImageInfo.Features is { Animation: true })
{
using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling);
using WebpAnimationDecoder animationDecoder = new(
this.memoryAllocator,
this.configuration,
this.maxFrames,
this.backgroundColorHandling);
return animationDecoder.Decode<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);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
{
WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
WebpLosslessDecoder losslessDecoder = new(
this.webImageInfo.Vp8LBitReader,
this.memoryAllocator,
this.configuration);
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
{
WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
WebpLossyDecoder lossyDecoder = new(
this.webImageInfo.Vp8BitReader,
this.memoryAllocator,
this.configuration);
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}
@ -137,7 +144,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
{
return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
metadata);
}
}
@ -332,7 +339,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return;
}
metadata.ExifProfile = new(exifData);
metadata.ExifProfile = new ExifProfile(exifData);
}
}
@ -359,7 +366,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable
return;
}
metadata.XmpProfile = new(xmpData);
metadata.XmpProfile = new XmpProfile(xmpData);
}
}

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

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public sealed class WebpDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
/// <summary>
/// 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>
/// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas.
/// </summary>
internal enum AnimationDisposalMethod
public enum WebpDisposalMethod
{
/// <summary>
/// Do not dispose. Leave the canvas as is.

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

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

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

@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
if (lossless)
{
using Vp8LEncoder enc = new(
using Vp8LEncoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@ -140,11 +140,38 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.transparentColorMode,
this.nearLossless,
this.nearLosslessQuality);
enc.Encode(image, stream);
bool hasAnimation = image.Frames.Count > 1;
encoder.EncodeHeader(image, stream, hasAnimation);
if (hasAnimation)
{
foreach (ImageFrame<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
{
using Vp8Encoder enc = new(
using Vp8Encoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@ -156,7 +183,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
enc.Encode(image, stream);
if (image.Frames.Count > 1)
{
encoder.EncodeHeader(image, stream, false, true);
foreach (ImageFrame<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>
/// Gets the shared instance.
/// </summary>
public static WebpFormat Instance { get; } = new();
public static WebpFormat Instance { get; } = new WebpFormat();
/// <inheritdoc/>
public string Name => "Webp";
@ -30,8 +30,8 @@ public sealed class WebpFormat : IImageFormat<WebpMetadata, WebpFrameMetadata>
public IEnumerable<string> FileExtensions => WebpConstants.FileExtensions;
/// <inheritdoc/>
public WebpMetadata CreateDefaultFormatMetadata() => new();
public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata();
/// <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.
/// </summary>
/// <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>
/// 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.
/// </summary>
public uint FrameDuration { get; set; }
public uint FrameDelay { get; set; }
/// <inheritdoc/>
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.AnimationLoopCount = other.AnimationLoopCount;
this.AnimationBackground = other.AnimationBackground;
}
/// <summary>
@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable
/// </summary>
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/>
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.ProfileConnectionSpace) &&
Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) &&
this.Header.Size >= minSize &&
this.Header.Size < maxSize;
this.Header.Size is >= minSize and < maxSize;
}
/// <summary>
@ -175,7 +174,6 @@ public sealed class IccProfile : IDeepCloneable<IccProfile>
return copy;
}
IccWriter writer = new();
return IccWriter.Write(this);
}

13
tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs

@ -43,9 +43,9 @@ public class EncodeWebp
[Benchmark(Description = "Magick Webp Lossy")]
public void MagickWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
var defines = new WebPWriteDefines
WebPWriteDefines defines = new()
{
Lossless = false,
Method = 4,
@ -65,7 +65,7 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossy")]
public void ImageSharpWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossy,
@ -80,8 +80,8 @@ public class EncodeWebp
[Benchmark(Baseline = true, Description = "Magick Webp Lossless")]
public void MagickWebpLossless()
{
using var memoryStream = new MemoryStream();
var defines = new WebPWriteDefines
using MemoryStream memoryStream = new();
WebPWriteDefines defines = new()
{
Lossless = true,
Method = 4,
@ -97,12 +97,13 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossless")]
public void ImageSharpWebpLossless()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossless,
Method = WebpEncodingMethod.Level4,
NearLossless = false,
Quality = 75,
// This is equal to exact = false in libwebp, which is the default.
TransparentColorMode = WebpTransparentColorMode.Clear

29
tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs

@ -11,7 +11,7 @@ public class DominantCostRangeTests
[Fact]
public void DominantCost_Constructor()
{
var dominantCostRange = new DominantCostRange();
DominantCostRange dominantCostRange = new();
Assert.Equal(0, dominantCostRange.LiteralMax);
Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin);
Assert.Equal(0, dominantCostRange.RedMax);
@ -24,13 +24,11 @@ public class DominantCostRangeTests
public void UpdateDominantCostRange_Works()
{
// arrange
var dominantCostRange = new DominantCostRange();
var histogram = new Vp8LHistogram(10)
{
LiteralCost = 1.0d,
RedCost = 2.0d,
BlueCost = 3.0d
};
DominantCostRange dominantCostRange = new();
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10);
histogram.LiteralCost = 1.0d;
histogram.RedCost = 2.0d;
histogram.BlueCost = 3.0d;
// act
dominantCostRange.UpdateDominantCostRange(histogram);
@ -50,7 +48,7 @@ public class DominantCostRangeTests
public void GetHistoBinIndex_Works(int partitions, int expectedIndex)
{
// arrange
var dominantCostRange = new DominantCostRange()
DominantCostRange dominantCostRange = new()
{
BlueMax = 253.4625,
BlueMin = 109.0,
@ -59,13 +57,12 @@ public class DominantCostRangeTests
RedMax = 191.0,
RedMin = 109.0
};
var histogram = new Vp8LHistogram(6)
{
LiteralCost = 247.0d,
RedCost = 112.0d,
BlueCost = 202.0d,
BitCost = 733.0d
};
using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6);
histogram.LiteralCost = 247.0d;
histogram.RedCost = 112.0d;
histogram.BlueCost = 202.0d;
histogram.BitCost = 733.0d;
dominantCostRange.UpdateDominantCostRange(histogram);
// act

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Webp;
@ -65,7 +66,7 @@ public class Vp8LHistogramTests
// All remaining values are expected to be zero.
literals.AsSpan().CopyTo(expectedLiterals);
var backwardRefs = new Vp8LBackwardRefs(pixelData.Length);
Vp8LBackwardRefs backwardRefs = new(pixelData.Length);
for (int i = 0; i < pixelData.Length; i++)
{
backwardRefs.Add(new PixOrCopy()
@ -76,15 +77,16 @@ public class Vp8LHistogramTests
});
}
var histogram0 = new Vp8LHistogram(backwardRefs, 3);
var histogram1 = new Vp8LHistogram(backwardRefs, 3);
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3);
for (int i = 0; i < 5; i++)
{
histogram0.IsUsed[i] = true;
histogram1.IsUsed[i] = true;
histogram0.IsUsed(i, true);
histogram1.IsUsed(i, true);
}
var output = new Vp8LHistogram(3);
using OwnedVp8LHistogram output = OwnedVp8LHistogram.Create(memoryAllocator, 3);
// act
histogram0.Add(histogram1, output);

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

@ -308,7 +308,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@ -325,7 +325,7 @@ public class WebpDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f));
Assert.Equal(0, webpMetaData.AnimationLoopCount);
Assert.Equal(150U, frameMetaData.FrameDuration);
Assert.Equal(150U, frameMetaData.FrameDelay);
Assert.Equal(12, image.Frames.Count);
}
@ -357,6 +357,16 @@ public class WebpDecoderTests
image.CompareToOriginal(provider, ReferenceDecoder);
}
[Theory]
[WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)]
public void Decode_AnimatedLossy_AlphaBlending_Works<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]
[WithFile(Lossless.LossLessCorruptImage1, 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);
[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]
[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)]

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

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

1
tests/ImageSharp.Tests/TestImages.cs

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

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