Browse Source

Implement Vp8 encoder

pull/2569/head
Poker 2 years ago
parent
commit
fbc08bd6a6
No known key found for this signature in database GPG Key ID: C65A6AD457D5C8F8
  1. 106
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  2. 5
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  3. 121
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  4. 30
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  5. 7
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

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

@ -87,21 +87,20 @@ internal abstract class BitWriterBase
/// <summary>
/// Writes the RIFF header to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="riffSize">The block length.</param>
protected void WriteRiffHeader(Stream stream, uint riffSize)
protected static void WriteRiffHeader(Stream stream, uint riffSize)
{
stream.Write(WebpConstants.RiffFourCc);
BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize);
stream.Write(this.scratchBuffer.Span[..4]);
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize);
stream.Write(buf);
stream.Write(WebpConstants.WebpHeader);
}
/// <summary>
/// Calculates the chunk size of EXIF, XMP or ICCP metadata.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
protected static uint MetadataChunkSize(byte[] metadataBytes)
@ -125,22 +124,11 @@ internal abstract class BitWriterBase
/// Overwrites ides the write file size.
/// </summary>
/// <param name="stream">The stream to write to.</param>
protected static void OverwriteFileSize(Stream stream)
{
uint position = (uint)stream.Position;
stream.Position = 4;
byte[] buffer = new byte[4];
// "RIFF"(4)+uint32 size(4)
BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize);
stream.Write(buffer);
stream.Position = position;
}
protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4);
/// <summary>
/// Write the trunks before data trunk.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="BitWriterBase.scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
@ -148,9 +136,8 @@ internal abstract class BitWriterBase
/// <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="alphaData">The alpha channel data.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
public void WriteTrunksBeforeData(
/// <param name="hasAnimation">Flag indicating, if an animation parameter is present.</param>
public static void WriteTrunksBeforeData(
Stream stream,
uint width,
uint height,
@ -158,26 +145,20 @@ internal abstract class BitWriterBase
XmpProfile? xmpProfile,
IccProfile? iccProfile,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
bool hasAnimation)
{
// Write file size later
this.WriteRiffHeader(stream, 0);
WriteRiffHeader(stream, 0);
// Write VP8X, header if necessary.
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha;
bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation;
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha);
WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation);
if (iccProfile != null)
{
this.WriteColorProfile(stream, iccProfile.ToByteArray());
}
if (hasAlpha)
{
this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed);
WriteColorProfile(stream, iccProfile.ToByteArray());
}
}
}
@ -191,23 +172,22 @@ internal abstract class BitWriterBase
/// <summary>
/// Write the trunks after data trunk.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="BitWriterBase.scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
public void WriteTrunksAfterData(
public static void WriteTrunksAfterData(
Stream stream,
ExifProfile? exifProfile,
XmpProfile? xmpProfile)
{
if (exifProfile != null)
{
this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif);
WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp);
WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp);
}
OverwriteFileSize(stream);
@ -216,16 +196,15 @@ internal abstract class BitWriterBase
/// <summary>
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <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)
protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
{
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)metadataBytes.Length;
Span<byte> buf = this.scratchBuffer.Span[..4];
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@ -242,15 +221,13 @@ internal abstract class BitWriterBase
/// <summary>
/// Writes the color profile(<see cref="WebpChunkType.Iccp"/>) to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp);
protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp);
/// <summary>
/// Writes the animation parameter(<see cref="WebpChunkType.AnimationParameter"/>) to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="background">
/// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order.
@ -259,9 +236,9 @@ internal abstract class BitWriterBase
/// The background color is also used when the Disposal method is 1.
/// </param>
/// <param name="loopCount">The number of times to loop the animation. If it is 0, this means infinitely.</param>
protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount)
public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount)
{
Span<byte> buf = this.scratchBuffer.Span[..4];
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort));
@ -275,17 +252,15 @@ internal abstract class BitWriterBase
/// <summary>
/// Writes the animation frame(<see cref="WebpChunkType.Animation"/>) to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <param name="stream">The stream to write to.</param>
/// <param name="animation">Animation frame data.</param>
/// <param name="data">Frame data.</param>
protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span<byte> data)
public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation)
{
uint size = AnimationFrameData.HeaderSize + (uint)data.Length;
Span<byte> buf = this.scratchBuffer.Span[..4];
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation);
stream.Write(buf);
BinaryPrimitives.WriteUInt32BigEndian(buf, size);
long position = stream.Position;
BinaryPrimitives.WriteUInt32BigEndian(buf, 0);
stream.Write(buf);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y);
@ -294,20 +269,35 @@ internal abstract class BitWriterBase
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration);
byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod);
stream.WriteByte(flag);
stream.Write(data);
return position;
}
/// <summary>
/// Overwrites ides the write frame size.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="prevPosition">Previous position.</param>
public static void OverwriteFrameSize(Stream stream, long prevPosition)
{
uint position = (uint)stream.Position;
stream.Position = prevPosition;
byte[] buffer = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4));
stream.Write(buffer);
stream.Position = position;
}
/// <summary>
/// Writes the alpha chunk to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <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[..4];
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
@ -332,7 +322,6 @@ internal abstract class BitWriterBase
/// <summary>
/// Writes a VP8X header to the stream.
/// </summary>
/// <remarks>Think of it as a static method — none of the other members are called except for <see cref="scratchBuffer"/></remarks>
/// <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>
@ -340,7 +329,8 @@ internal abstract class BitWriterBase
/// <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, IccProfile? iccProfile, 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)
{
@ -360,13 +350,11 @@ internal abstract class BitWriterBase
flags |= 8;
}
/*
if (isAnimated)
if (hasAnimation)
{
// Set animated flag.
flags |= 2;
}
*/
if (xmpProfile != null)
{
@ -386,7 +374,7 @@ internal abstract class BitWriterBase
flags |= 32;
}
Span<byte> buf = this.scratchBuffer.Span[..4];
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize);

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

@ -267,7 +267,7 @@ internal class Vp8LEncoder : IDisposable
this.EncodeStream(image.Frames.RootFrame);
this.bitWriter.Finish();
this.bitWriter.WriteTrunksBeforeData(
BitWriterBase.WriteTrunksBeforeData(
stream,
(uint)width,
(uint)height,
@ -275,13 +275,12 @@ internal class Vp8LEncoder : IDisposable
xmpProfile,
metadata.IccProfile,
false /*hasAlpha*/,
Span<byte>.Empty,
false);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream);
this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
}
/// <summary>

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

@ -88,7 +88,8 @@ internal class Vp8Encoder : IDisposable
private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11;
private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const long HeaderSizeEstimate =
WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize;
private const int QMin = 0;
@ -165,7 +166,7 @@ internal class Vp8Encoder : IDisposable
// TODO: make partition_limit configurable?
const int limit = 100; // original code: limit = 100 - config->partition_limit;
this.maxI4HeaderBits =
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve.
this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh];
for (int i = 0; i < this.MbInfo.Length; i++)
@ -308,22 +309,88 @@ internal class Vp8Encoder : IDisposable
/// </summary>
private int MbHeaderLimit { get; }
public void EncodeHeader<TPixel>(Image<TPixel> image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0)
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)
{
BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount);
}
}
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.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v);
bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v);
if (!hasAnimation)
{
this.EncodeHeader(image, stream, hasAlpha, false);
}
int yStride = width;
int uvStride = (yStride + 1) >> 1;
@ -375,13 +442,6 @@ internal class Vp8Encoder : IDisposable
// Store filter stats.
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile;
XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile;
// Extract and encode alpha channel data, if present.
int alphaDataSize = 0;
bool alphaCompressionSucceeded = false;
@ -393,7 +453,7 @@ internal class Vp8Encoder : IDisposable
{
// TODO: This can potentially run in an separate task.
encodedAlphaData = AlphaEncoder.EncodeAlpha(
image.Frames.RootFrame,
frame,
this.configuration,
this.memoryAllocator,
this.skipMetadata,
@ -409,20 +469,31 @@ internal class Vp8Encoder : IDisposable
}
this.bitWriter.Finish();
this.bitWriter.WriteTrunksBeforeData(
stream,
(uint)width,
(uint)height,
exifProfile,
xmpProfile,
metadata.IccProfile,
hasAlpha,
alphaData[..alphaDataSize],
this.alphaCompression && alphaCompressionSucceeded);
long prevPosition = 0;
if (hasAnimation)
{
prevPosition = BitWriterBase.WriteAnimationFrame(stream, new()
{
Width = (uint)frame.Width,
Height = (uint)frame.Height
});
}
if (hasAlpha)
{
Span<byte> data = alphaData[..alphaDataSize];
bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded;
BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed);
}
this.bitWriter.WriteEncodedImageToStream(stream);
this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile);
if (hasAnimation)
{
BitWriterBase.OverwriteFrameSize(stream, prevPosition);
}
}
finally
{

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

@ -144,7 +144,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
}
else
{
using Vp8Encoder enc = new(
using Vp8Encoder encoder = new(
this.memoryAllocator,
this.configuration,
image.Width,
@ -156,7 +156,33 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals
this.filterStrength,
this.spatialNoiseShaping,
this.alphaCompression);
enc.Encode(image, stream);
if (image.Frames.Count > 1)
{
encoder.EncodeHeader(image, stream, false, true);
foreach (ImageFrame<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);
}
}
}

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

@ -17,6 +17,13 @@ public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Fact]
public void Encode_AnimatedLossy()
{
Image<Rgba32> image = Image.Load<Rgba32>(@"C:\Users\poker\Desktop\1.webp");
image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp");
}
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]

Loading…
Cancel
Save