Browse Source

Decode animated lossless webp

pull/1985/head
Brian Popow 4 years ago
parent
commit
9cc92b8035
  1. 2
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  2. 23
      src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs
  3. 21
      src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs
  4. 49
      src/ImageSharp/Formats/Webp/AnimationFrameData.cs
  5. 323
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  6. 351
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  7. 433
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  8. 11
      src/ImageSharp/Formats/Webp/WebpFeatures.cs
  9. 28
      src/ImageSharp/Primitives/Rectangle.cs
  10. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

2
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
public Size Dimensions => new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
public Size Dimensions => new(this.imageDescriptor.Width, this.imageDescriptor.Height);
private MemoryAllocator MemoryAllocator => this.Configuration.MemoryAllocator;

23
src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
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
{
/// <summary>
/// Use alpha blending. 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>
AlphaBlending = 0,
/// <summary>
/// Do not blend. After disposing of the previous frame,
/// render the current frame on the canvas by overwriting the rectangle covered by the current frame.
/// </summary>
DoNotBlend = 1
}
}

21
src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
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
{
/// <summary>
/// Do not dispose. Leave the canvas as is.
/// </summary>
DoNotDispose = 0,
/// <summary>
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk.
/// </summary>
Dispose = 1
}
}

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

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
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;
}
}

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

@ -0,0 +1,323 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
{
/// <summary>
/// Decoder for animated webp images.
/// </summary>
internal class WebpAnimationDecoder
{
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary>
/// Used for allocating memory during the decoding operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The area to restore.
/// </summary>
private Rectangle? restoreArea;
/// <summary>
/// Initializes a new instance of the <see cref="WebpAnimationDecoder"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="configuration">The global configuration.</param>
public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
}
/// <summary>
/// Decodes the animated webp image from the specified stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="features">The webp features.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="completeDataSize">The size of the image data in bytes.</param>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> image = null;
ImageFrame<TPixel> previousFrame = null;
int remainingBytes = (int)completeDataSize;
while (remainingBytes > 0)
{
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
remainingBytes -= 4;
switch (chunkType)
{
case WebpChunkType.Animation:
uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, features.AnimationBackgroundColor.Value);
remainingBytes -= (int)dataSize;
break;
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image.Metadata, false, this.buffer);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
break;
}
if (stream.Position == stream.Length)
{
break;
}
}
return image;
}
/// <summary>
/// Reads an individual webp frame.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be decoded from. Cannot be null.</param>
/// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="backgroundColor">The default background color of the canvas in.</param>
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);
long streamStartPosition = stream.Position;
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
if (chunkType is WebpChunkType.Alpha)
{
// TODO: ignore alpha for now.
stream.Skip(4);
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer);
stream.Skip((int)alphaChunkSize);
chunkType = WebpChunkParsingUtils.ReadChunkType(stream, this.buffer);
}
WebpImageInfo webpInfo = null;
var features = new WebpFeatures();
switch (chunkType)
{
case WebpChunkType.Vp8:
webpInfo = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, stream, this.buffer, features);
break;
case WebpChunkType.Vp8L:
webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, this.buffer, features);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Read unexpected chunk type, should be VP8 or VP8L");
break;
}
var metaData = new ImageMetadata();
ImageFrame<TPixel> currentFrame = null;
ImageFrame<TPixel> imageFrame;
if (previousFrame is null)
{
image = new Image<TPixel>(this.configuration, (int)width, (int)height, backgroundColor.ToPixel<TPixel>(), metaData);
imageFrame = image.Frames.RootFrame;
}
else
{
currentFrame = image.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection.
imageFrame = currentFrame;
}
if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose)
{
this.RestoreToBackground(imageFrame, backgroundColor);
}
uint frameX = frameData.X * 2;
uint frameY = frameData.Y * 2;
uint frameWidth = frameData.Width;
uint frameHeight = frameData.Height;
var regionRectangle = Rectangle.FromLTRB((int)frameX, (int)frameY, (int)(frameX + frameWidth), (int)(frameY + frameHeight));
using Image<TPixel> decodedImage = this.DecodeImageData<TPixel>(frameData, webpInfo);
this.DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight);
if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending)
{
this.AlphaBlend(previousFrame, imageFrame);
}
previousFrame = currentFrame ?? image.Frames.RootFrame;
this.restoreArea = regionRectangle;
return (uint)(stream.Position - streamStartPosition);
}
/// <summary>
/// Decodes the either lossy or lossless webp image data.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameData">The frame data.</param>
/// <param name="webpInfo">The webp information.</param>
/// <returns>A decoded image.</returns>
private Image<TPixel> DecodeImageData<TPixel>(AnimationFrameData frameData, WebpImageInfo webpInfo)
where TPixel : unmanaged, IPixel<TPixel>
{
var decodedImage = new Image<TPixel>((int)frameData.Width, (int)frameData.Height);
Buffer2D<TPixel> pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer;
if (webpInfo.IsLossless)
{
var losslessDecoder = new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration);
losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height);
}
else
{
var lossyDecoder = new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration);
lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo);
}
return decodedImage;
}
/// <summary>
/// Draws the decoded image on canvas. The decoded image can be smaller the the canvas.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="decodedImage">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 void DrawDecodedImageOnCanvas<TPixel>(Image<TPixel> decodedImage, ImageFrame<TPixel> imageFrame, uint frameX, uint frameY, uint frameWidth, uint frameHeight)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> decodedImagePixels = decodedImage.Frames.RootFrame.PixelBuffer;
Buffer2D<TPixel> imageFramePixels = imageFrame.PixelBuffer;
int decodedRowIdx = 0;
for (uint y = frameY; y < frameHeight; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan((int)y);
Span<TPixel> decodedPixelRow = decodedImagePixels.DangerousGetRowSpan(decodedRowIdx++).Slice(0, (int)frameWidth);
decodedPixelRow.TryCopyTo(framePixelRow.Slice((int)frameX));
}
}
/// <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>
private void AlphaBlend<TPixel>(ImageFrame<TPixel> src, ImageFrame<TPixel> dst)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = src.Width;
int height = src.Height;
PixelBlender<Rgba32> blender = PixelOperations<Rgba32>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
Buffer2D<TPixel> srcPixels = src.PixelBuffer;
Buffer2D<TPixel> dstPixels = dst.PixelBuffer;
Rgba32 srcRgba = default;
Rgba32 dstRgba = default;
for (int y = 0; y < height; y++)
{
Span<TPixel> srcPixelRow = srcPixels.DangerousGetRowSpan(y);
Span<TPixel> dstPixelRow = dstPixels.DangerousGetRowSpan(y);
for (int x = 0; x < width; x++)
{
ref TPixel srcPixel = ref srcPixelRow[x];
ref TPixel dstPixel = ref dstPixelRow[x];
srcPixel.ToRgba32(ref srcRgba);
dstPixel.ToRgba32(ref dstRgba);
if (dstRgba.A == 0)
{
Rgba32 blendResult = blender.Blend(srcRgba, dstRgba, 1.0f);
dstPixel.FromRgba32(blendResult);
}
}
}
}
/// <summary>
/// Dispose to background color. Fill the rectangle on the canvas covered by the current frame
/// with background color specified in the ANIM chunk.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="imageFrame">The image frame.</param>
/// <param name="backgroundColor">Color of the background.</param>
private void RestoreToBackground<TPixel>(ImageFrame<TPixel> imageFrame, Color backgroundColor)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.restoreArea.HasValue)
{
return;
}
var interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value);
Buffer2DRegion<TPixel> pixelRegion = imageFrame.PixelBuffer.GetRegion(interest);
for (int y = 0; y < pixelRegion.Height; y++)
{
Span<TPixel> pixelRow = pixelRegion.DangerousGetRowSpan(y);
for (int x = 0; x < pixelRow.Length; x++)
{
ref TPixel pixel = ref pixelRow[x];
pixel.FromRgba32(backgroundColor);
}
}
}
/// <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)
{
var data = new AnimationFrameData
{
DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, this.buffer)
};
// 3 bytes for the X coordinate of the upper left corner of the frame.
data.X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer);
// 3 bytes for the Y coordinate of the upper left corner of the frame.
data.Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer);
// Frame width Minus One.
data.Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1;
// Frame height Minus One.
data.Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.buffer) + 1;
// Frame duration.
data.Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, this.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;
}
}
}

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

@ -0,0 +1,351 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp
{
internal static class WebpChunkParsingUtils
{
/// <summary>
/// Reads the header of a lossy webp image.
/// </summary>
/// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8Header(MemoryAllocator memoryAllocator, Stream stream, byte[] buffer, WebpFeatures features)
{
// VP8 data size (not including this 4 bytes).
stream.Read(buffer, 0, 4);
uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
// remaining counts the available image data payload.
uint remaining = dataSize;
// Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30
// Frame tag that contains four fields:
// - A 1-bit frame type (0 for key frames, 1 for interframes).
// - A 3-bit version number.
// - A 1-bit show_frame flag.
// - A 19-bit field containing the size of the first data partition in bytes.
stream.Read(buffer, 0, 3);
uint frameTag = (uint)(buffer[0] | (buffer[1] << 8) | (buffer[2] << 16));
remaining -= 3;
bool isNoKeyFrame = (frameTag & 0x1) == 1;
if (isNoKeyFrame)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame");
}
uint version = (frameTag >> 1) & 0x7;
if (version > 3)
{
WebpThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}");
}
bool invisibleFrame = ((frameTag >> 4) & 0x1) == 0;
if (invisibleFrame)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible");
}
uint partitionLength = frameTag >> 5;
if (partitionLength > dataSize)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information");
}
// Check for VP8 magic bytes.
stream.Read(buffer, 0, 3);
if (!buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
stream.Read(buffer, 0, 4);
uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(buffer);
uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6);
tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(buffer.AsSpan(2));
uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7;
if (width == 0 || height == 0)
{
WebpThrowHelper.ThrowImageFormatException("width or height can not be zero");
}
if (partitionLength > remaining)
{
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
var vp8FrameHeader = new Vp8FrameHeader()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
var bitReader = new Vp8BitReader(
stream,
remaining,
memoryAllocator,
partitionLength)
{
Remaining = remaining
};
return new WebpImageInfo()
{
Width = width,
Height = height,
XScale = xScale,
YScale = yScale,
BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
IsLossless = false,
Features = features,
Vp8Profile = (sbyte)version,
Vp8FrameHeader = vp8FrameHeader,
Vp8BitReader = bitReader
};
}
/// <summary>
/// Reads the header of a lossless webp image.
/// </summary>
/// <returns>Information about this image.</returns>
public static WebpImageInfo ReadVp8LHeader(MemoryAllocator memoryAllocator, Stream stream, byte[] buffer, WebpFeatures features)
{
// VP8 data size.
uint imageDataSize = ReadChunkSize(stream, buffer);
var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
if (signature != WebpConstants.Vp8LHeaderMagicByte)
{
WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
}
// The first 28 bits of the bitstream specify the width and height of the image.
uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
if (width == 0 || height == 0)
{
WebpThrowHelper.ThrowImageFormatException("invalid width or height read");
}
// The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
// TODO: this flag value is not used yet
bool alphaIsUsed = bitReader.ReadBit();
// The next 3 bits are the version. The version number is a 3 bit code that must be set to 0.
// Any other value should be treated as an error.
uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits);
if (version != 0)
{
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
return new WebpImageInfo()
{
Width = width,
Height = height,
BitsPerPixel = WebpBitsPerPixel.Pixel32,
IsLossless = true,
Features = features,
Vp8LBitReader = bitReader
};
}
/// <summary>
/// Reads an the extended webp file header. An extended file header consists of:
/// - A 'VP8X' chunk with information about features used in the file.
/// - An optional 'ICCP' chunk with color profile.
/// - An optional 'XMP' chunk with metadata.
/// - An optional 'ANIM' chunk with animation control data.
/// - An optional 'ALPH' chunk with alpha channel data.
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
/// </summary>
/// <returns>Information about this webp image.</returns>
public static WebpImageInfo ReadVp8XHeader(Stream stream, byte[] buffer, WebpFeatures features)
{
uint fileSize = ReadChunkSize(stream, buffer);
// The first byte contains information about the image features used.
byte imageFeatures = (byte)stream.ReadByte();
// The first two bit of it are reserved and should be 0.
if (imageFeatures >> 6 != 0)
{
WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero");
}
// If bit 3 is set, a ICC Profile Chunk should be present.
features.IccProfile = (imageFeatures & (1 << 5)) != 0;
// If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk).
features.Alpha = (imageFeatures & (1 << 4)) != 0;
// If bit 5 is set, a EXIF metadata should be present.
features.ExifProfile = (imageFeatures & (1 << 3)) != 0;
// If bit 6 is set, XMP metadata should be present.
features.XmpMetaData = (imageFeatures & (1 << 2)) != 0;
// If bit 7 is set, animation should be present.
features.Animation = (imageFeatures & (1 << 1)) != 0;
// 3 reserved bytes should follow which are supposed to be zero.
stream.Read(buffer, 0, 3);
if (buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0)
{
WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
}
// 3 bytes for the width.
uint width = ReadUnsignedInt24Bit(stream, buffer) + 1;
// 3 bytes for the height.
uint height = ReadUnsignedInt24Bit(stream, buffer) + 1;
// Read all the chunks in the order they occur.
var info = new WebpImageInfo()
{
Width = width,
Height = height,
Features = features
};
return info;
}
/// <summary>
/// Reads a unsigned 24 bit integer.
/// </summary>
/// <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(Stream stream, byte[] buffer)
{
stream.Read(buffer, 0, 3);
buffer[3] = 0;
return (uint)BinaryPrimitives.ReadInt32LittleEndian(buffer);
}
/// <summary>
/// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload,
/// so the chunk size will be increased by 1 in those cases.
/// </summary>
/// <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(Stream stream, byte[] buffer)
{
if (stream.Read(buffer, 0, 4) == 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
}
throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
}
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
public static WebpChunkType ReadChunkType(Stream stream, byte[] buffer)
{
if (stream.Read(buffer, 0, 4) == 4)
{
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
}
throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
}
/// <summary>
/// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
/// If there are more such chunks, readers MAY ignore all except the first one.
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// </summary>
public static void ParseOptionalChunks(Stream stream, WebpChunkType chunkType, ImageMetadata metadata, bool ignoreMetaData, byte[] buffer)
{
long streamLength = stream.Length;
while (stream.Position < streamLength)
{
uint chunkLength = ReadChunkSize(stream, buffer);
if (ignoreMetaData)
{
stream.Skip((int)chunkLength);
}
int bytesRead;
switch (chunkType)
{
case WebpChunkType.Exif:
byte[] exifData = new byte[chunkLength];
bytesRead = stream.Read(exifData, 0, (int)chunkLength);
if (bytesRead != chunkLength)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
if (metadata.ExifProfile != null)
{
metadata.ExifProfile = new ExifProfile(exifData);
}
break;
case WebpChunkType.Xmp:
byte[] xmpData = new byte[chunkLength];
bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
if (bytesRead != chunkLength)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
if (metadata.XmpProfile != null)
{
metadata.XmpProfile = new XmpProfile(xmpData);
}
break;
default:
stream.Skip((int)chunkLength);
break;
}
}
}
/// <summary>
/// Determines if the chunk type is an optional VP8X chunk.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <returns>True, if its an optional chunk type.</returns>
public static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch
{
WebpChunkType.Alpha => true,
WebpChunkType.AnimationParameter => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false
};
}
}

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

@ -1,11 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@ -29,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
private readonly byte[] buffer = new byte[4];
/// <summary>
/// Used for allocating memory during processing operations.
/// Used for allocating memory during the decoding operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
@ -91,11 +89,13 @@ namespace SixLabors.ImageSharp.Formats.Webp
{
if (this.webImageInfo.Features is { Animation: true })
{
WebpThrowHelper.ThrowNotSupportedException("Animations are not supported");
var animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, this.Configuration);
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
var image = new Image<TPixel>(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
{
var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration);
@ -108,10 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
}
// There can be optional chunks after the image data, like EXIF and XMP.
if (this.webImageInfo.Features != null)
{
this.ParseOptionalChunks(this.webImageInfo.Features);
}
this.ReadOptionalMetadata();
return image;
}
@ -141,7 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
// Read file size.
// The size of the file in bytes starting at offset 8.
// The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC.
uint fileSize = this.ReadChunkSize();
uint fileSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
// Skip 'WEBP' from the header.
this.currentStream.Skip(4);
@ -158,278 +155,66 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.Metadata = new ImageMetadata();
this.webpMetadata = this.Metadata.GetFormatMetadata(WebpFormat.Instance);
WebpChunkType chunkType = this.ReadChunkType();
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
var features = new WebpFeatures();
switch (chunkType)
{
case WebpChunkType.Vp8:
return this.ReadVp8Header();
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
return WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8L:
return this.ReadVp8LHeader();
this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
return WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
case WebpChunkType.Vp8X:
return this.ReadVp8XHeader();
WebpImageInfo webpInfos = WebpChunkParsingUtils.ReadVp8XHeader(this.currentStream, this.buffer, features);
while (this.currentStream.Position < this.currentStream.Length)
{
chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
if (chunkType == WebpChunkType.Vp8)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
webpInfos = WebpChunkParsingUtils.ReadVp8Header(this.memoryAllocator, this.currentStream, this.buffer, features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
webpInfos = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, this.currentStream, this.buffer, features);
}
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
{
bool isAnimationChunk = this.ParseOptionalExtendedChunks(chunkType, features);
if (isAnimationChunk)
{
return webpInfos;
}
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
}
return webpInfos;
default:
WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header");
return new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception.
}
}
/// <summary>
/// Reads an the extended webp file header. An extended file header consists of:
/// - A 'VP8X' chunk with information about features used in the file.
/// - An optional 'ICCP' chunk with color profile.
/// - An optional 'XMP' chunk with metadata.
/// - An optional 'ANIM' chunk with animation control data.
/// - An optional 'ALPH' chunk with alpha channel data.
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
/// </summary>
/// <returns>Information about this webp image.</returns>
private WebpImageInfo ReadVp8XHeader()
{
var features = new WebpFeatures();
uint fileSize = this.ReadChunkSize();
// The first byte contains information about the image features used.
byte imageFeatures = (byte)this.currentStream.ReadByte();
// The first two bit of it are reserved and should be 0.
if (imageFeatures >> 6 != 0)
{
WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero");
}
// If bit 3 is set, a ICC Profile Chunk should be present.
features.IccProfile = (imageFeatures & (1 << 5)) != 0;
// If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk).
features.Alpha = (imageFeatures & (1 << 4)) != 0;
// If bit 5 is set, a EXIF metadata should be present.
features.ExifProfile = (imageFeatures & (1 << 3)) != 0;
// If bit 6 is set, XMP metadata should be present.
features.XmpMetaData = (imageFeatures & (1 << 2)) != 0;
// If bit 7 is set, animation should be present.
features.Animation = (imageFeatures & (1 << 1)) != 0;
// 3 reserved bytes should follow which are supposed to be zero.
this.currentStream.Read(this.buffer, 0, 3);
if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0)
{
WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero");
}
// 3 bytes for the width.
this.currentStream.Read(this.buffer, 0, 3);
this.buffer[3] = 0;
uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// 3 bytes for the height.
this.currentStream.Read(this.buffer, 0, 3);
this.buffer[3] = 0;
uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// Read all the chunks in the order they occur.
var info = new WebpImageInfo();
while (this.currentStream.Position < this.currentStream.Length)
{
WebpChunkType chunkType = this.ReadChunkType();
if (chunkType == WebpChunkType.Vp8)
{
info = this.ReadVp8Header(features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
info = this.ReadVp8LHeader(features);
}
else if (IsOptionalVp8XChunk(chunkType))
{
this.ParseOptionalExtendedChunks(chunkType, features);
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
return new WebpImageInfo(); // This return will never be reached, because throw helper will throw an exception.
}
if (features.Animation)
{
// TODO: Animations are not yet supported.
return new WebpImageInfo() { Width = width, Height = height, Features = features };
}
return info;
}
/// <summary>
/// Reads the header of a lossy webp image.
/// </summary>
/// <param name="features">Webp features.</param>
/// <returns>Information about this webp image.</returns>
private WebpImageInfo ReadVp8Header(WebpFeatures features = null)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossy;
// VP8 data size (not including this 4 bytes).
this.currentStream.Read(this.buffer, 0, 4);
uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
// remaining counts the available image data payload.
uint remaining = dataSize;
// Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30
// Frame tag that contains four fields:
// - A 1-bit frame type (0 for key frames, 1 for interframes).
// - A 3-bit version number.
// - A 1-bit show_frame flag.
// - A 19-bit field containing the size of the first data partition in bytes.
this.currentStream.Read(this.buffer, 0, 3);
uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16));
remaining -= 3;
bool isNoKeyFrame = (frameTag & 0x1) == 1;
if (isNoKeyFrame)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame");
}
uint version = (frameTag >> 1) & 0x7;
if (version > 3)
{
WebpThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}");
}
bool invisibleFrame = ((frameTag >> 4) & 0x1) == 0;
if (invisibleFrame)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible");
}
uint partitionLength = frameTag >> 5;
if (partitionLength > dataSize)
{
WebpThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information");
}
// Check for VP8 magic bytes.
this.currentStream.Read(this.buffer, 0, 3);
if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes))
{
WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
this.currentStream.Read(this.buffer, 0, 4);
uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer);
uint width = tmp & 0x3fff;
sbyte xScale = (sbyte)(tmp >> 6);
tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2));
uint height = tmp & 0x3fff;
sbyte yScale = (sbyte)(tmp >> 6);
remaining -= 7;
if (width == 0 || height == 0)
{
WebpThrowHelper.ThrowImageFormatException("width or height can not be zero");
}
if (partitionLength > remaining)
{
WebpThrowHelper.ThrowImageFormatException("bad partition length");
}
var vp8FrameHeader = new Vp8FrameHeader()
{
KeyFrame = true,
Profile = (sbyte)version,
PartitionLength = partitionLength
};
var bitReader = new Vp8BitReader(
this.currentStream,
remaining,
this.memoryAllocator,
partitionLength)
{
Remaining = remaining
};
return new WebpImageInfo()
{
Width = width,
Height = height,
XScale = xScale,
YScale = yScale,
BitsPerPixel = features?.Alpha == true ? WebpBitsPerPixel.Pixel32 : WebpBitsPerPixel.Pixel24,
IsLossless = false,
Features = features,
Vp8Profile = (sbyte)version,
Vp8FrameHeader = vp8FrameHeader,
Vp8BitReader = bitReader
};
}
/// <summary>
/// Reads the header of a lossless webp image.
/// </summary>
/// <param name="features">Webp image features.</param>
/// <returns>Information about this image.</returns>
private WebpImageInfo ReadVp8LHeader(WebpFeatures features = null)
{
this.webpMetadata.FileFormat = WebpFileFormatType.Lossless;
// VP8 data size.
uint imageDataSize = this.ReadChunkSize();
var bitReader = new Vp8LBitReader(this.currentStream, imageDataSize, this.memoryAllocator);
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
if (signature != WebpConstants.Vp8LHeaderMagicByte)
{
WebpThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
}
// The first 28 bits of the bitstream specify the width and height of the image.
uint width = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
uint height = bitReader.ReadValue(WebpConstants.Vp8LImageSizeBits) + 1;
if (width == 0 || height == 0)
{
WebpThrowHelper.ThrowImageFormatException("invalid width or height read");
}
// The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise.
// TODO: this flag value is not used yet
bool alphaIsUsed = bitReader.ReadBit();
// The next 3 bits are the version. The version number is a 3 bit code that must be set to 0.
// Any other value should be treated as an error.
uint version = bitReader.ReadValue(WebpConstants.Vp8LVersionBits);
if (version != 0)
{
WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header");
}
return new WebpImageInfo()
{
Width = width,
Height = height,
BitsPerPixel = WebpBitsPerPixel.Pixel32,
IsLossless = true,
Features = features,
Vp8LBitReader = bitReader
};
}
/// <summary>
/// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks.
/// Parses optional VP8X chunks, which can be ICCP, ANIM or ALPH chunks.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param>
private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features)
/// <returns>true, if animation chunk was found.</returns>
private bool ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features)
{
int bytesRead;
switch (chunkType)
{
case WebpChunkType.Iccp:
uint iccpChunkSize = this.ReadChunkSize();
uint iccpChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)iccpChunkSize);
@ -437,7 +222,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
else
{
byte[] iccpData = new byte[iccpChunkSize];
this.currentStream.Read(iccpData, 0, (int)iccpChunkSize);
bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize);
if (bytesRead != iccpChunkSize)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for ICCP profile");
}
var profile = new IccProfile(iccpData);
if (profile.CheckIsValid())
{
@ -448,7 +238,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
break;
case WebpChunkType.Exif:
uint exifChunkSize = this.ReadChunkSize();
uint exifChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
@ -456,7 +246,12 @@ namespace SixLabors.ImageSharp.Formats.Webp
else
{
byte[] exifData = new byte[exifChunkSize];
this.currentStream.Read(exifData, 0, (int)exifChunkSize);
bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
@ -464,7 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
break;
case WebpChunkType.Xmp:
uint xmpChunkSize = this.ReadChunkSize();
uint xmpChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
@ -472,19 +267,32 @@ namespace SixLabors.ImageSharp.Formats.Webp
else
{
byte[] xmpData = new byte[xmpChunkSize];
this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
break;
case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet.
break;
case WebpChunkType.AnimationParameter:
features.Animation = true;
uint animationChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
byte blue = (byte)this.currentStream.ReadByte();
byte green = (byte)this.currentStream.ReadByte();
byte red = (byte)this.currentStream.ReadByte();
byte alpha = (byte)this.currentStream.ReadByte();
features.AnimationBackgroundColor = new Color(new Rgba32(red, green, blue, alpha));
this.currentStream.Read(this.buffer, 0, 2);
features.AnimationLoopCount = BinaryPrimitives.ReadUInt16LittleEndian(this.buffer);
return true;
case WebpChunkType.Alpha:
uint alphaChunkSize = this.ReadChunkSize();
uint alphaChunkSize = WebpChunkParsingUtils.ReadChunkSize(this.currentStream, this.buffer);
features.AlphaChunkHeader = (byte)this.currentStream.ReadByte();
int alphaDataSize = (int)(alphaChunkSize - 1);
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
@ -494,88 +302,27 @@ namespace SixLabors.ImageSharp.Formats.Webp
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
return false;
}
/// <summary>
/// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP ').
/// If there are more such chunks, readers MAY ignore all except the first one.
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// Reads the optional metadata EXIF of XMP profiles, which can follow the image data.
/// </summary>
/// <param name="features">The webp features.</param>
private void ParseOptionalChunks(WebpFeatures features)
private void ReadOptionalMetadata()
{
if (this.IgnoreMetadata || (features.ExifProfile == false && features.XmpMetaData == false))
if (!this.IgnoreMetadata && this.webImageInfo.Features != null && (this.webImageInfo.Features.ExifProfile || this.webImageInfo.Features.XmpMetaData))
{
return;
}
long streamLength = this.currentStream.Length;
while (this.currentStream.Position < streamLength)
{
// Read chunk header.
WebpChunkType chunkType = this.ReadChunkType();
uint chunkLength = this.ReadChunkSize();
if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null)
{
byte[] exifData = new byte[chunkLength];
this.currentStream.Read(exifData, 0, (int)chunkLength);
this.Metadata.ExifProfile = new ExifProfile(exifData);
}
else
// The spec states, that the EXIF and XMP should come after the image data, but it seems some encoders store them
// in the VP8X chunk before the image data. Make sure there is still data to read here.
if (this.currentStream.Position == this.currentStream.Length)
{
// Skip XMP chunk data or any duplicate EXIF chunk.
this.currentStream.Skip((int)chunkLength);
return;
}
}
}
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
private WebpChunkType ReadChunkType()
{
if (this.currentStream.Read(this.buffer, 0, 4) == 4)
{
var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer);
return chunkType;
}
throw new ImageFormatException("Invalid Webp data.");
}
/// <summary>
/// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload,
/// so the chunk size will be increased by 1 in those cases.
/// </summary>
/// <returns>The chunk size in bytes.</returns>
private uint ReadChunkSize()
{
if (this.currentStream.Read(this.buffer, 0, 4) == 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(this.currentStream, this.buffer);
WebpChunkParsingUtils.ParseOptionalChunks(this.currentStream, chunkType, this.Metadata, this.IgnoreMetadata, this.buffer);
}
throw new ImageFormatException("Invalid Webp data.");
}
/// <summary>
/// Determines if the chunk type is an optional VP8X chunk.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <returns>True, if its an optional chunk type.</returns>
private static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch
{
WebpChunkType.Alpha => true,
WebpChunkType.Animation => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false
};
}
}

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

@ -46,6 +46,17 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// </summary>
public bool Animation { get; set; }
/// <summary>
/// Gets or sets the animation loop count. 0 means infinitely.
/// </summary>
public ushort AnimationLoopCount { get; set; }
/// <summary>
/// Gets or sets default background color of the animation frame canvas.
/// 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..
/// </summary>
public Color? AnimationBackgroundColor { get; set; }
/// <inheritdoc/>
public void Dispose() => this.AlphaData?.Dispose();
}

28
src/ImageSharp/Primitives/Rectangle.cs

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp
public Point Location
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Point(this.X, this.Y);
get => new(this.X, this.Y);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp
public Size Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Size(this.Width, this.Height);
get => new(this.Width, this.Height);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
@ -147,14 +147,14 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="rectangle">The rectangle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator RectangleF(Rectangle rectangle) => new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
public static implicit operator RectangleF(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
/// <summary>
/// Creates a <see cref="Vector4"/> with the coordinates of the specified <see cref="Rectangle"/>.
/// </summary>
/// <param name="rectangle">The rectangle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Vector4(Rectangle rectangle) => new Vector4(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
public static implicit operator Vector4(Rectangle rectangle) => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
/// <summary>
/// Compares two <see cref="Rectangle"/> objects for equality.
@ -188,7 +188,7 @@ namespace SixLabors.ImageSharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once InconsistentNaming
public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top));
public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top));
/// <summary>
/// Returns the center point of the given <see cref="Rectangle"/>.
@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp
/// <param name="rectangle">The rectangle.</param>
/// <returns>The <see cref="Point"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Center(Rectangle rectangle) => new Point(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
public static Point Center(Rectangle rectangle) => new(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
/// <summary>
/// Creates a rectangle that represents the intersection between <paramref name="a"/> and
@ -376,7 +376,7 @@ namespace SixLabors.ImageSharp
public void Inflate(Size size) => this.Inflate(size.Width, size.Height);
/// <summary>
/// Determines if the specfied point is contained within the rectangular region defined by
/// Determines if the specified point is contained within the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="x">The x-coordinate of the given point.</param>
@ -405,10 +405,10 @@ namespace SixLabors.ImageSharp
(this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom);
/// <summary>
/// Determines if the specfied <see cref="Rectangle"/> intersects the rectangular region defined by
/// Determines if the specified <see cref="Rectangle"/> intersects the rectangular region defined by
/// this <see cref="Rectangle"/>.
/// </summary>
/// <param name="rectangle">The other Rectange. </param>
/// <param name="rectangle">The other Rectangle. </param>
/// <returns>The <see cref="bool"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IntersectsWith(Rectangle rectangle) =>
@ -438,16 +438,10 @@ namespace SixLabors.ImageSharp
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.X, this.Y, this.Width, this.Height);
}
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Width, this.Height);
/// <inheritdoc/>
public override string ToString()
{
return $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
}
public override string ToString() => $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
/// <inheritdoc/>
public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other);

2
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -21,7 +21,7 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
// TODO: Scatter test cases into multiple test classes
[Trait("Format", "Jpg")]
[Trait("Format", "Jpg")]
public partial class JpegDecoderTests
{
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector;

Loading…
Cancel
Save