From 9cc92b803550ba5954a67c10882df79b7205a42c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Feb 2022 16:24:47 +0100 Subject: [PATCH] Decode animated lossless webp --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 2 +- .../Formats/Webp/AnimationBlendingMethod.cs | 23 + .../Formats/Webp/AnimationDisposalMethod.cs | 21 + .../Formats/Webp/AnimationFrameData.cs | 49 ++ .../Formats/Webp/WebpAnimationDecoder.cs | 323 +++++++++++++ .../Formats/Webp/WebpChunkParsingUtils.cs | 351 ++++++++++++++ .../Formats/Webp/WebpDecoderCore.cs | 433 ++++-------------- src/ImageSharp/Formats/Webp/WebpFeatures.cs | 11 + src/ImageSharp/Primitives/Rectangle.cs | 28 +- .../Formats/Jpg/JpegDecoderTests.cs | 2 +- 10 files changed, 881 insertions(+), 362 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs create mode 100644 src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs create mode 100644 src/ImageSharp/Formats/Webp/AnimationFrameData.cs create mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs create mode 100644 src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index b6348803a..3c0bf9edf 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Gets the dimensions of the image. /// - 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; diff --git a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs new file mode 100644 index 000000000..643c1959a --- /dev/null +++ b/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 +{ + /// + /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + internal enum AnimationBlendingMethod + { + /// + /// 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. + /// + AlphaBlending = 0, + + /// + /// 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. + /// + DoNotBlend = 1 + } +} diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs new file mode 100644 index 000000000..f6beebf75 --- /dev/null +++ b/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 +{ + /// + /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + internal enum AnimationDisposalMethod + { + /// + /// Do not dispose. Leave the canvas as is. + /// + DoNotDispose = 0, + + /// + /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame with background color specified in the ANIM chunk. + /// + Dispose = 1 + } +} diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs new file mode 100644 index 000000000..ffb1ddc1f --- /dev/null +++ b/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 + { + /// + /// The animation chunk size. + /// + public uint DataSize; + + /// + /// The X coordinate of the upper left corner of the frame is Frame X * 2. + /// + public uint X; + + /// + /// The Y coordinate of the upper left corner of the frame is Frame Y * 2. + /// + public uint Y; + + /// + /// The width of the frame. + /// + public uint Width; + + /// + /// The height of the frame. + /// + public uint Height; + + /// + /// 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. + /// + public uint Duration; + + /// + /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public AnimationBlendingMethod BlendingMethod; + + /// + /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public AnimationDisposalMethod DisposalMethod; + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs new file mode 100644 index 000000000..15d2d0145 --- /dev/null +++ b/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 +{ + /// + /// Decoder for animated webp images. + /// + internal class WebpAnimationDecoder + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// Used for allocating memory during the decoding operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The area to restore. + /// + private Rectangle? restoreArea; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration) + { + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + } + + /// + /// Decodes the animated webp image from the specified stream. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The webp features. + /// The width of the image. + /// The height of the image. + /// The size of the image data in bytes. + public Image Decode(BufferedReadStream stream, WebpFeatures features, uint width, uint height, uint completeDataSize) + where TPixel : unmanaged, IPixel + { + Image image = null; + ImageFrame 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; + } + + /// + /// Reads an individual webp frame. + /// + /// The pixel format. + /// The stream, where the image should be decoded from. Cannot be null. + /// The image to decode the information to. + /// The previous frame. + /// The width of the image. + /// The height of the image. + /// The default background color of the canvas in. + private uint ReadFrame(BufferedReadStream stream, ref Image image, ref ImageFrame previousFrame, uint width, uint height, Color backgroundColor) + where TPixel : unmanaged, IPixel + { + 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 currentFrame = null; + ImageFrame imageFrame; + if (previousFrame is null) + { + image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), 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 decodedImage = this.DecodeImageData(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); + } + + /// + /// Decodes the either lossy or lossless webp image data. + /// + /// The pixel format. + /// The frame data. + /// The webp information. + /// A decoded image. + private Image DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + where TPixel : unmanaged, IPixel + { + var decodedImage = new Image((int)frameData.Width, (int)frameData.Height); + Buffer2D 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; + } + + /// + /// Draws the decoded image on canvas. The decoded image can be smaller the the canvas. + /// + /// The type of the pixel. + /// The decoded image. + /// The image frame to draw into. + /// The frame x coordinate. + /// The frame y coordinate. + /// The width of the frame. + /// The height of the frame. + private void DrawDecodedImageOnCanvas(Image decodedImage, ImageFrame imageFrame, uint frameX, uint frameY, uint frameWidth, uint frameHeight) + where TPixel : unmanaged, IPixel + { + Buffer2D decodedImagePixels = decodedImage.Frames.RootFrame.PixelBuffer; + Buffer2D imageFramePixels = imageFrame.PixelBuffer; + int decodedRowIdx = 0; + for (uint y = frameY; y < frameHeight; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan((int)y); + Span decodedPixelRow = decodedImagePixels.DangerousGetRowSpan(decodedRowIdx++).Slice(0, (int)frameWidth); + decodedPixelRow.TryCopyTo(framePixelRow.Slice((int)frameX)); + } + } + + /// + /// 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. + /// + /// The pixel format. + /// The source image. + /// The destination image. + private void AlphaBlend(ImageFrame src, ImageFrame dst) + where TPixel : unmanaged, IPixel + { + int width = src.Width; + int height = src.Height; + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + Buffer2D srcPixels = src.PixelBuffer; + Buffer2D dstPixels = dst.PixelBuffer; + Rgba32 srcRgba = default; + Rgba32 dstRgba = default; + for (int y = 0; y < height; y++) + { + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span 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); + } + } + } + } + + /// + /// Dispose to background color. Fill the rectangle on the canvas covered by the current frame + /// with background color specified in the ANIM chunk. + /// + /// The pixel format. + /// The image frame. + /// Color of the background. + private void RestoreToBackground(ImageFrame imageFrame, Color backgroundColor) + where TPixel : unmanaged, IPixel + { + if (!this.restoreArea.HasValue) + { + return; + } + + var interest = Rectangle.Intersect(imageFrame.Bounds(), this.restoreArea.Value); + Buffer2DRegion pixelRegion = imageFrame.PixelBuffer.GetRegion(interest); + for (int y = 0; y < pixelRegion.Height; y++) + { + Span pixelRow = pixelRegion.DangerousGetRowSpan(y); + for (int x = 0; x < pixelRow.Length; x++) + { + ref TPixel pixel = ref pixelRow[x]; + pixel.FromRgba32(backgroundColor); + } + } + } + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + 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; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs new file mode 100644 index 000000000..b93b4d66a --- /dev/null +++ b/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 + { + /// + /// Reads the header of a lossy webp image. + /// + /// Information about this webp image. + 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 + }; + } + + /// + /// Reads the header of a lossless webp image. + /// + /// Information about this image. + 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 + }; + } + + /// + /// 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. + /// + /// Information about this webp image. + 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; + } + + /// + /// Reads a unsigned 24 bit integer. + /// + /// The stream to read from. + /// The buffer to store the read data into. + /// A unsigned 24 bit integer. + public static uint ReadUnsignedInt24Bit(Stream stream, byte[] buffer) + { + stream.Read(buffer, 0, 3); + buffer[3] = 0; + return (uint)BinaryPrimitives.ReadInt32LittleEndian(buffer); + } + + /// + /// 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. + /// + /// The stream to read the data from. + /// Buffer to store the data read from the stream. + /// The chunk size in bytes. + 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."); + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// The stream to read the data from. + /// Buffer to store the data read from the stream. + /// + /// Thrown if the input stream is not valid. + /// + 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."); + } + + /// + /// 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. + /// + 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; + } + } + } + + /// + /// Determines if the chunk type is an optional VP8X chunk. + /// + /// The chunk type. + /// True, if its an optional chunk type. + public static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch + { + WebpChunkType.Alpha => true, + WebpChunkType.AnimationParameter => true, + WebpChunkType.Exif => true, + WebpChunkType.Iccp => true, + WebpChunkType.Xmp => true, + _ => false + }; + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 9d18e5d82..447f7f781 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/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]; /// - /// Used for allocating memory during processing operations. + /// Used for allocating memory during the decoding operations. /// 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(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } var image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); Buffer2D 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. - } - } - - /// - /// 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. - /// - /// Information about this webp image. - 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; } /// - /// Reads the header of a lossy webp image. - /// - /// Webp features. - /// Information about this webp image. - 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 - }; - } - - /// - /// Reads the header of a lossless webp image. - /// - /// Webp image features. - /// Information about this image. - 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 - }; - } - - /// - /// 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. /// /// The chunk type. /// The webp image features. - private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features) + /// true, if animation chunk was found. + 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(alphaDataSize); @@ -494,88 +302,27 @@ namespace SixLabors.ImageSharp.Formats.Webp WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); break; } + + return false; } /// - /// 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. /// - /// The webp features. - 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; } - } - } - - /// - /// Identifies the chunk type from the chunk. - /// - /// - /// Thrown if the input stream is not valid. - /// - 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."); - } - - /// - /// 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. - /// - /// The chunk size in bytes. - 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."); } - - /// - /// Determines if the chunk type is an optional VP8X chunk. - /// - /// The chunk type. - /// True, if its an optional chunk type. - private static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch - { - WebpChunkType.Alpha => true, - WebpChunkType.Animation => true, - WebpChunkType.Exif => true, - WebpChunkType.Iccp => true, - WebpChunkType.Xmp => true, - _ => false - }; } } diff --git a/src/ImageSharp/Formats/Webp/WebpFeatures.cs b/src/ImageSharp/Formats/Webp/WebpFeatures.cs index b26e4101e..b0131e07a 100644 --- a/src/ImageSharp/Formats/Webp/WebpFeatures.cs +++ b/src/ImageSharp/Formats/Webp/WebpFeatures.cs @@ -46,6 +46,17 @@ namespace SixLabors.ImageSharp.Formats.Webp /// public bool Animation { get; set; } + /// + /// Gets or sets the animation loop count. 0 means infinitely. + /// + public ushort AnimationLoopCount { get; set; } + + /// + /// 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.. + /// + public Color? AnimationBackgroundColor { get; set; } + /// public void Dispose() => this.AlphaData?.Dispose(); } diff --git a/src/ImageSharp/Primitives/Rectangle.cs b/src/ImageSharp/Primitives/Rectangle.cs index 1904b0979..cd1828249 100644 --- a/src/ImageSharp/Primitives/Rectangle.cs +++ b/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 /// /// The rectangle. [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); /// /// Creates a with the coordinates of the specified . /// /// The rectangle. [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); /// /// Compares two 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)); /// /// Returns the center point of the given . @@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp /// The rectangle. /// The . [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)); /// /// Creates a rectangle that represents the intersection between and @@ -376,7 +376,7 @@ namespace SixLabors.ImageSharp public void Inflate(Size size) => this.Inflate(size.Width, size.Height); /// - /// 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 . /// /// The x-coordinate of the given point. @@ -405,10 +405,10 @@ namespace SixLabors.ImageSharp (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); /// - /// Determines if the specfied intersects the rectangular region defined by + /// Determines if the specified intersects the rectangular region defined by /// this . /// - /// The other Rectange. + /// The other Rectangle. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IntersectsWith(Rectangle rectangle) => @@ -438,16 +438,10 @@ namespace SixLabors.ImageSharp } /// - 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); /// - 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} ]"; /// public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 7a2446959..ccb3e3fb9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/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;