Browse Source

introduce RiffHelper

pull/2569/head
Poker 2 years ago
parent
commit
1b0b877a14
No known key found for this signature in database GPG Key ID: C65A6AD457D5C8F8
  1. 222
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  2. 37
      src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs
  3. 140
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  4. 113
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  5. 23
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  6. 23
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  7. 3
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  8. 18
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  9. 2
      src/ImageSharp/Formats/Webp/WebpChunkType.cs
  10. 10
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  11. 93
      src/ImageSharp/Formats/Webp/WebpFrameData.cs

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

@ -1,8 +1,9 @@
// 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;
@ -15,8 +16,6 @@ internal abstract class BitWriterBase
private const ulong MaxCanvasPixels = 4294967295ul;
protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize;
/// <summary>
/// Buffer to write to.
/// </summary>
@ -79,48 +78,6 @@ internal abstract class BitWriterBase
Array.Resize(ref this.buffer, newSize);
}
/// <summary>
/// Writes the RIFF header to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="riffSize">The block length.</param>
protected static void WriteRiffHeader(Stream stream, uint riffSize)
{
stream.Write(WebpConstants.RiffFourCc);
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>
/// <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);
}
/// <summary>
/// Calculates the chunk size of a alpha chunk.
/// </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)
{
uint alphaSize = (uint)alphaBytes.Length + 1;
return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
}
/// <summary>
/// Overwrites ides the write file size.
/// </summary>
/// <param name="stream">The stream to write to.</param>
protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4);
/// <summary>
/// Write the trunks before data trunk.
/// </summary>
@ -143,7 +100,9 @@ internal abstract class BitWriterBase
bool hasAnimation)
{
// Write file size later
WriteRiffHeader(stream, 0);
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;
@ -153,7 +112,7 @@ internal abstract class BitWriterBase
if (iccProfile != null)
{
WriteColorProfile(stream, iccProfile.ToByteArray());
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray());
}
}
}
@ -177,49 +136,17 @@ internal abstract class BitWriterBase
{
if (exifProfile != null)
{
WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif);
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
}
if (xmpProfile != null)
{
WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp);
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}
OverwriteFileSize(stream);
}
/// <summary>
/// Writes a metadata profile (EXIF or XMP) 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 static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType)
{
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)metadataBytes.Length;
Span<byte> buf = stackalloc byte[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);
}
RiffHelper.EndWriteRiffFile(stream, 4);
}
/// <summary>
/// Writes the color profile(<see cref="WebpChunkType.Iccp"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileBytes">The color profile bytes.</param>
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>
@ -233,55 +160,8 @@ internal abstract class BitWriterBase
/// <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)
{
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort));
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba);
stream.Write(buf);
BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount);
stream.Write(buf[..2]);
}
/// <summary>
/// Writes the animation frame(<see cref="WebpChunkType.Animation"/>) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="animation">Animation frame data.</param>
public static long WriteAnimationFrame(Stream stream, WebpFrameData animation)
{
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation);
stream.Write(buf);
long position = stream.Position;
BinaryPrimitives.WriteUInt32BigEndian(buf, 0);
stream.Write(buf);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration);
byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod);
stream.WriteByte(flag);
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;
WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount);
chunk.WriteTo(stream);
}
/// <summary>
@ -292,27 +172,17 @@ internal abstract class BitWriterBase
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
public static void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{
uint size = (uint)dataBytes.Length + 1;
Span<byte> buf = stackalloc byte[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)
{
// TODO: Filtering and preprocessing
flags = 1;
}
stream.WriteByte(flags);
stream.Write(dataBytes);
// Add padding byte if needed.
if ((size & 1) == 1)
{
stream.WriteByte(0);
}
RiffHelper.EndWriteChunk(stream, pos);
}
/// <summary>
@ -328,66 +198,10 @@ internal abstract class BitWriterBase
/// <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}");
}
WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height);
// 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 (hasAnimation)
{
// Set animated flag.
flags |= 2;
}
if (xmpProfile != null)
{
// Set xmp bit.
flags |= 4;
}
if (hasAlpha)
{
// Set alpha bit.
flags |= 16;
}
if (iccProfile != null)
{
// Set iccp flag.
flags |= 32;
}
Span<byte> buf = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X);
stream.Write(buf);
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);
}
}

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

23
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;
@ -308,16 +310,15 @@ internal class Vp8LEncoder : IDisposable
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData
{
X = 0,
Y = 0,
Width = (uint)frame.Width,
Height = (uint)frame.Height,
Duration = frameMetadata.FrameDelay,
BlendingMethod = frameMetadata.BlendMethod,
DisposalMethod = frameMetadata.DisposalMethod
});
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.
@ -325,7 +326,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
BitWriterBase.OverwriteFrameSize(stream, prevPosition);
RiffHelper.EndWriteChunk(stream, prevPosition);
}
}

23
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;
@ -478,16 +480,15 @@ internal class Vp8Encoder : IDisposable
WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata();
// TODO: If we can clip the indexed frame for transparent bounds we can set properties here.
prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData
{
X = 0,
Y = 0,
Width = (uint)frame.Width,
Height = (uint)frame.Height,
Duration = frameMetadata.FrameDelay,
BlendingMethod = frameMetadata.BlendMethod,
DisposalMethod = frameMetadata.DisposalMethod
});
prevPosition = new WebpFrameData(
0,
0,
(uint)frame.Width,
(uint)frame.Height,
frameMetadata.FrameDelay,
frameMetadata.BlendMethod,
frameMetadata.DisposalMethod)
.WriteHeaderTo(stream);
}
if (hasAlpha)
@ -501,7 +502,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
BitWriterBase.OverwriteFrameSize(stream, prevPosition);
RiffHelper.EndWriteChunk(stream, prevPosition);
}
}
finally

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -99,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;

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

@ -106,14 +106,14 @@ internal static class WebpChunkParsingUtils
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader
Vp8FrameHeader vp8FrameHeader = new()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining };
return new WebpImageInfo
{
@ -139,7 +139,7 @@ internal static class WebpChunkParsingUtils
// VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer);
Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
@ -231,7 +231,7 @@ internal static class WebpChunkParsingUtils
uint height = ReadUInt24LittleEndian(stream, buffer) + 1;
// Read all the chunks in the order they occur.
WebpImageInfo info = new WebpImageInfo
WebpImageInfo info = new()
{
Width = width,
Height = height,
@ -247,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 ReadUInt24LittleEndian(BufferedReadStream stream, Span<byte> buffer)
public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer)
{
if (stream.Read(buffer, 0, 3) == 3)
{
@ -286,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.");

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

@ -61,5 +61,5 @@ internal enum WebpChunkType : uint
/// 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>
/// <remarks>ANMF (Multiple)</remarks>
Animation = 0x414E4D46,
FrameData = 0x414E4D46,
}

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

@ -55,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>
@ -70,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>

93
src/ImageSharp/Formats/Webp/WebpFrameData.cs

@ -1,93 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Webp;
internal struct WebpFrameData
{
/// <summary>
/// The animation chunk size.
/// </summary>
public uint DataSize;
/// <summary>
/// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags.
/// </summary>
public const uint HeaderSize = 16;
/// <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 WebpBlendingMethod 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 WebpDisposalMethod DisposalMethod;
public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height);
/// <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(BufferedReadStream stream)
{
Span<byte> buffer = stackalloc byte[4];
WebpFrameData data = new()
{
DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer),
// 3 bytes for the X coordinate of the upper left corner of the frame.
X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
// 3 bytes for the Y coordinate of the upper left corner of the frame.
Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer),
// Frame width Minus One.
Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
// Frame height Minus One.
Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1,
// Frame duration.
Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer)
};
byte flags = (byte)stream.ReadByte();
data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose;
data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending;
return data;
}
}
Loading…
Cancel
Save